diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8b1d22d2d6..6f5a5be862 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -github: [Aircoookie] -custom: ['https://paypal.me/Aircoookie'] +github: [Aircoookie,blazoncek] +custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek'] diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index ac2a12149e..285ad419e4 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -56,6 +56,9 @@ body: options: - ESP8266 - ESP32 + - ESP32-S3 + - ESP32-S2 + - ESP32-C3 - Other validations: required: true @@ -80,4 +83,4 @@ body: description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Aircoookie/WLED/blob/master/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct - required: true \ No newline at end of file + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..29a2f1b510 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: WLED Discord community + url: https://discord.gg/KuqP7NE + about: Please ask and answer questions and discuss setup issues here! + - name: WLED community forum + url: https://wled.discourse.group/ + about: For issues and ideas that might need longer discussion. + - name: kno.wled.ge base + url: https://kno.wled.ge/basics/faq/ + about: Take a look at the frequently asked questions and documentation, perhaps your question is already answered! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 94f92a612a..0000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Question -about: Have a question about using WLED? -title: '' -labels: question -assignees: '' - ---- - -**Take a look at the wiki and FAQ, perhaps your question is already answered!** -[FAQ](https://github.com/Aircoookie/WLED/wiki/FAQ) - -**Please consider asking your question on the WLED forum or Discord** -[Forum](https://wled.discourse.group/) -[Discord](https://discord.gg/KuqP7NE) -[What to post where?](https://github.com/Aircoookie/WLED/issues/658) - -**If you do not like to use these platforms, delete this template and ask away!** -Please keep in mind though that the issue section is generally not the preferred place for general questions. diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 811db619ae..0000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 120 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - keep - - enhancement - - confirmed -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - Hey! This issue has been open for quite some time without any new comments now. - It will be closed automatically in a week if no further activity occurs. - - Thank you for using WLED! -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..1f2557160c --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,30 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '0 12 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 120 + days-before-close: 7 + stale-issue-label: 'stale' + stale-pr-label: 'stale' + exempt-issue-labels: 'pinned,keep,enhancement,confirmed' + exempt-pr-labels: 'pinned,keep,enhancement,confirmed' + exempt-all-milestones: true + operations-per-run: 1000 + stale-issue-message: > + Hey! This issue has been open for quite some time without any new comments now. + It will be closed automatically in a week if no further activity occurs. + + Thank you for using WLED! ✨ + stale-pr-message: > + Hey! This pull request has been open for quite some time without any new comments now. + It will be closed automatically in a week if no further activity occurs. + + Thank you for contributing to WLED! ❤️ diff --git a/.github/workflows/wled-ci.yml b/.github/workflows/wled-ci.yml index c28446e614..2b599e6f66 100644 --- a/.github/workflows/wled-ci.yml +++ b/.github/workflows/wled-ci.yml @@ -8,21 +8,23 @@ jobs: name: Gather Environments runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 + with: + python-version: '3.9' - name: Install PlatformIO run: pip install -r requirements.txt - name: Get default environments id: envs run: | - echo "::set-output name=environments::$(pio project config --json-output | jq -cr '.[0][1][0][1]')" + echo "environments=$(pio project config --json-output | jq -cr '.[0][1][0][1]')" >> $GITHUB_OUTPUT outputs: environments: ${{ steps.envs.outputs.environments }} @@ -32,24 +34,27 @@ jobs: runs-on: ubuntu-latest needs: get_default_envs strategy: + fail-fast: false matrix: environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Cache PlatformIO - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.platformio key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 + with: + python-version: '3.9' - name: Install PlatformIO run: pip install -r requirements.txt - name: Build firmware diff --git a/.gitignore b/.gitignore index 02e648b88b..c85fae0c22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,24 @@ -.pio .cache +.clang-format +.direnv +.DS_Store +.gitignore +.idea +.pio .pioenvs .piolibdeps .vscode -!.vscode/extensions.json -/wled00/Release + +esp01-update.sh +platformio_override.ini +replace_fs.py +wled-update.sh + +/build_output/ +/node_modules/ + /wled00/extLibs -/platformio_override.ini +/wled00/LittleFS /wled00/my_config.h -/build_output -.DS_Store -.gitignore -.clang-format -node_modules -.idea -.direnv +/wled00/Release +/wled00/wled00.ino.cpp diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index 29d75d19da..cab85e35b2 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -1,5 +1,3 @@ FROM gitpod/workspace-full - -USER gitpod -RUN pip3 install -U platformio +USER gitpod diff --git a/.gitpod.yml b/.gitpod.yml index cc416b1c2d..8452f08be7 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,12 +1,11 @@ tasks: - - command: platformio run + - command: pip3 install -U platformio && platformio run image: file: .gitpod.Dockerfile vscode: extensions: - - ms-vscode.cpptools@0.26.3:u3GsZ5PK12Ddr79vh4TWgQ== - - eamodio.gitlens@10.2.1:e0IYyp0efFqVsrZwsIe8CA== - - Atishay-Jain.All-Autocomplete@0.0.23:fbZNfSpnd8XkAHGfAPS2rA== - - 2gua.rainbow-brackets@0.0.6:Tbu8dTz0i+/bgcKQTQ5b8g== + - Atishay-Jain.All-Autocomplete + - esbenp.prettier-vscode + - shardulm94.trailing-spaces diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c25fc32dc..5613f2d0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,313 @@ ## WLED changelog +#### Build 2405180 +- WLED 0.14.4 release +- Fix for #3978 + +#### Build 2404040 +- WLED 0.14.3 release +- Fix for transition 0 (#3854, #3832, #3720) +- Fix for #3855 via #3873 (by @willmmiles) + +#### Build 2403170 +- WLED 0.14.2 release + +#### Build 2403110 +- Beta WLED 0.14.2-b2 +- New AsyncWebServer (improved performance and reduced memory use) +- New builds for ESP8266 with 160MHz CPU clock +- Fixing stairway usermod and adding buildflags (#3758 by @lost-hope) +- Fixing a potential array bounds violation in ESPDMX +- Reduced RAM usage (moved strings and TZ data (by @willmmiles) to PROGMEM) +- LockedJsonResponse: Release early if possible (by @willmmiles) + +#### Build 2402120 +- Beta WLED 0.14.2-b1 +- Possible fix for #3589 & partial fix for #3605 +- Prevent JSON buffer clear after failed lock attempt +- Multiple analog button fix for #3549 +- UM Audioreactive: add two compiler options (#3732 by @wled-install) +- Fix for #3693 + +#### Build 2401141 +- Official release of WLED 0.14.1 +- Fix for #3566, #3665, #3672 +- Sorting of palettes in custom palette editor (#3674 by @WoodyLetsCode) + +#### Build 2401060 +- Version bump: 0.14.1-b3 +- Global JSON buffer guarding (#3648 by @willmmiles, resolves #3641, #3312, #3367, #3637, #3646, #3447) +- Fix for #3632 +- Custom palette editor mobile UI enhancement (#3617 by @imeszaros) +- changelog update + +#### Build 2312290 +- Fix for #3622, #3613, #3609 +- Various tweaks and fixes +- changelog update + +#### Build 2312230 +- Version bump: 0.14.1-b2 +- Fix for Pixel Magic button +- Fix for #2922 (option to force WiFi PHY mode to G on ESP8266) +- Fix for #3601, #3400 (incorrect sunrise/sunset, #3612 by @softhack007) + +#### Build 2312180 +- Bugfixes (#3593, #3490, #3573, #3517, #3561, #3555, #3541, #3536, #3515, #3522, #3533, #3508) +- Various other internal cleanups and optimisations + +#### Build 2311160 +- Version bump: 0.14.1-b1 +- Bugfixes (#3526, #3502, #3496, #3484, #3487, #3445, #3466, #3296, #3382, #3312) +- New feature: Sort presets by ID +- New usermod: LDR sensor (#3490 by @JeffWDH) +- Effect: Twinklefox & Tinklecat metadata fix +- Effect: separate #HH and #MM for Scrolling Text (#3480) +- SSDR usermod enhancements (#3368) +- PWM fan usermod enhancements (#3414) + +#### Build 2310010, build 2310130 +- Release of WLED version 0.14.0 "Hoshi" +- Bugfixes for #3400, #3403, #3405 +- minor HTML optimizations +- audioreactive: bugfix for UDP sound sync (partly initialized packets) + +#### Build 2309240 +- Release of WLED beta version 0.14.0-b6 "Hoshi" +- Effect bugfixes and improvements (Meteor, Meteor Smooth, Scrolling Text) +- audioreactive: bugfixes for ES8388 and ES7243 init; minor improvements for analog inputs + +#### Build 2309100 +- Release of WLED beta version 0.14.0-b5 "Hoshi" +- New standard esp32 build with audioreactive +- Effect blending bugfixes, and minor optimizations + +#### Build 2309050 +- Effect blending (#3311) (finally effect transitions!) + *WARNING*: May not work well with ESP8266, with plenty of segments or usermods (low RAM condition)!!! +- Added receive and send sync groups to JSON API (#3317) (you can change sync groups using preset) +- Internal temperature usermod (#3246) +- MQTT server and topic length overrides (#3354) (new build flags) +- Animated Staircase usermod enhancement (#3348) (on/off toggle/relay control) +- Added local time info to Info page (#3351) +- New effect: Rolling Balls (a.k.a. linear bounce) (#1039) +- Various bug fixes and enhancements. + +#### Build 2308110 +- Release of WLED beta version 0.14.0-b4 "Hoshi" +- Reset effect data immediately upon mode change + +#### Build 2308030 +- Improved random palette handling and blending +- Soap bugfix +- Fix ESP-NOW crash with AP mode Always + +#### Build 2307180 +- Bus-level global buffering (#3280) +- Removed per-segment LED buffer (SEGMENT.leds) +- various fixes and improvements (ESP variants platform 5.3.0, effect optimizations, /json/cfg pin allocation) + +#### Build 2307130 +- larger `oappend()` stack buffer (3.5k) for ESP32 +- Preset cycle bugfix (#3262) +- Rotary encoder ALT fix for large LED count (#3276) +- effect updates (2D Plasmaball), `blur()` speedup +- On/Off toggle from nodes view (may show unknown device type on older versions) (#3291) +- various fixes and improvements (ABL, crashes when changing presets with different segments) + +#### Build 2306270 +- ESP-NOW remote support (#3237) +- Pixel Magic tool (display pixel art) (#3249) +- Websocket (peek) fallback when connection cannot be established, WS retries (#3267) +- Add WiFi network scan RPC command to Improv Serial (#3271) +- Longer (custom option available) segment name for ESP32 +- various fixes and improvements + +#### Build 2306210 +- 0.14.0-b3 release +- respect global I2C in all usermods (no local initialization of I2C bus) +- Multi relay usermod compile-time enabled option (-D MULTI_RELAY_ENABLED=true|false) + +#### Build 2306180 +- Added client-side option for applying effect defaults from metadata +- Improved ESP8266 stability by reducing WebSocket response resends +- Updated ESP8266 core to 3.1.2 + +#### Build 2306141 +- Lissajous improvements +- Scrolling Text improvements (leading 0) + +#### Build 2306140 +- Add settings PIN (un)locking to JSON post API + +#### Build 2306130 +- Bumped version to 0.14-b3 (beta 3) +- added pin dropdowns in LED preferences (not for LED pins) and usermods +- introduced (unused ATM) NeoGammaWLEDMethod class +- Reverse proxy support +- PCF8754 support for Rotary encoder (requires wiring INT pin to ESP GPIO) +- Rely on global I2C pins for usermods (breaking change) +- various fixes and enhancements + +#### Build 2306020 +- Support for segment sets (PR #3171) +- Reduce sound simulation modes to 2 to facilitate segment sets +- Trigger button immediately on press if all configured presets are the same (PR #3226) +- Changes for allowing Alexa to change light color to White when auto-calculating from RGB (PR #3211) + +#### Build 2305280 +- DDP protocol update (#3193) +- added PCF8574 I2C port expander support for Multi relay usermod +- MQTT multipacket (fragmented) message fix +- added option to retain MQTT brightness and color messages +- new ethernet board: @srg74 Ethernet Shield +- new 2D effects: Soap (#3184) & Octopus & Waving cell (credit @St3P40 https://github.com/80Stepko08) +- various fixes and enhancements + +#### Build 2305090 +- new ethernet board: @Wladi ABC! WLED Eth +- Battery usermod voltage calculation (#3116) +- custom palette editor (#3164) +- improvements in Dancing Shadows and Tartan effects +- UCS389x support +- switched to NeoPixelBus 2.7.5 (replaced NeoPixelBrightnessBus with NeoPixelBusLg) +- SPI bus clock selection (for LEDs) (#3173) +- DMX mode preset fix (#3134) +- iOS fix for scroll (#3182) +- Wordclock "Norddeutsch" fix (#3161) +- various fixes and enhancements + +#### Build 2304090 +- updated Arduino ESP8266 core to 4.1.0 (newer compiler) +- updated NeoPixelBus to 2.7.3 (with support for UCS890x chipset) +- better support for ESP32-C3, ESP32-S2 and ESP32-S3 (Arduino ESP32 core 5.2.0) +- iPad/tablet with 1024 pixels width in landscape orientation PC mode support (#3153) +- fix for Pixel Art Converter (#3155) + +#### Build 2303240 +- Peek scaling of large 2D matrices +- Added 0D (1 pixel) metadata for effects & enhance 0D (analog strip) UI handling +- Added ability to disable ADAlight (-D WLED_DISABLE_ADALIGHT) +- Fixed APA102 output on Ethernet enabled controllers +- Added ArtNet virtual/network output (#3121) +- Klipper usermod (#3106) +- Remove DST from CST timezone +- various fixes and enhancements + +#### Build 2302180 + +- Removed Blynk support (servers shut down on 31st Dec 2022) +- Added `ledgap.json` to complement ledmaps for 2D matrices +- Added support for white addressable strips (#3073) +- Ability to use SHT temperature usermod with PWM fan usermod +- Added `onStateChange()` callback to usermods (#3081) +- Refactored `bus_manager` [internal] +- Dual 1D & 2D mode (add 1D strip after the matrix) +- Removed 1D -> 2D mapping for individual pixel control +- effect tweak: Fireworks 1D +- various bugfixes + +#### Build 2301240 + +- Version bump to v0.14.0-b2 "Hoshi" +- PixelArt converter (convert any image to pixel art and display it on a matrix) (PR #3042) +- various effect updates and optimisations + - added Overlay option to some effects (allows overlapping segments) + - added gradient text on Scrolling Text + - added #DDMM, #MMDD & #HHMM date and time options for Scrolling Text effect (PR #2990) + - deprecated: Dynamic Smooth, Dissolve Rnd, Solid Glitter + - optimised & enhanced loading of default values + - new effect: Distortion Waves (2D) + - 2D support for Ripple effect + - slower minimum speed for Railway effect +- DMX effect mode & segment controls (PR #2891) +- Optimisations for conditional compiles (further reduction of code size) +- better UX with effect sliders (PR #3012) +- enhanced support for ESP32 variants: C3, S2 & S3 +- usermod enhancements (PIR, Temperature, Battery (PR #2975), Analog Clock (PR #2993)) +- new usermod SHT (PR #2963) +- 2D matrix set up with gaps or irregular panels (breaking change!) (PR #2892) +- palette blending/transitions +- random palette smooth changes +- hex color notations in custom palettes +- allow more virtual buses +- plethora of bugfixes + +### WLED release 0.14.0-b1 + +#### Build 2212222 + +- Version bump to v0.14.0-b1 "Hoshi" +- 2D matrix support (including mapping 1D effects to 2D and 2D peek) +- [internal] completely rewritten Segment & WS2812FX handling code +- [internal] ability to add custom effects via usermods +- [internal] set of 2D drawing functions +- transitions on every segment (including ESP8266) +- enhanced old and new 2D effects (metadata: default values) +- custom palettes (up to 10; upload palette0.json, palette1.json, ...) +- custom effect sliders and options, quick filters +- global I2C and SPI GPIO allocation (for usermods) +- usermod settings page enhancements (dropdown & info) +- asynchronous preset loading (and added "pd" JSON API call for direct preset apply) +- new usermod Boblight (PR #2917) +- new usermod PWM Outputs (PR #2912) +- new usermod Audioreactive +- new usermod Word Clock Matrix (PR #2743) +- new usermod Ping Pong Clock (PR #2746) +- new usermod ADS1115 (PR #2752) +- new usermod Analog Clock (PR #2736) +- various usermod enhancements and updates +- allow disabling pull-up resistors on buttons +- SD card support (PR #2877) +- enhanced HTTP API to support custom effect sliders & options (X1, X2, X3, M1, M2, M3) +- multiple UDP sync message retries (PR #2830) +- network debug printer (PR #2870) +- automatic UI PC mode on large displays +- removed support for upgrading from pre-0.10 (EEPROM) +- support for setting GPIO level when LEDs are off (RMT idle level, ESP32 only) (PR #2478) +- Pakistan time-zone (PKT) +- ArtPoll support +- TM1829 LED support +- experimental support for ESP32 S2, S3 and C3 +- general improvements and bugfixes + +### WLED release 0.13.3 + +- Version bump to v0.13.3 "Toki" +- Disable ESP watchdog by default (fixes flickering and boot issues on a fresh install) +- Added support for LPD6803 + +### WLED release 0.13.2 + +#### Build 2208140 + +- Version bump to v0.13.2 "Toki" +- Added option to receive live data on the main segment only (PR #2601) +- Enable ESP watchdog by default (PR #2657) +- Fixed race condition when saving bus config +- Better potentiometer filtering (PR #2693) +- More suitable DMX libraries (PR #2652) +- Fixed outgoing serial TPM2 message length (PR #2628) +- Fixed next universe overflow and Art-Net DMX start address (PR #2607) +- Fixed relative segment brightness (PR #2665) + +### Builds between releases 0.13.1 and 0.13.2 + +#### Build 2203191 + +- Fixed sunrise/set calculation (once again) + +#### Build 2203190 + +- Fixed `/json/cfg` unable to set busses (#2589) +- Fixed Peek with odd LED counts > 255 (#2586) + +#### Build 2203160 + +- Version bump to v0.13.2-a0 "Toki" +- Add ability to skip up to 255 LEDs +- Dependency version bumps + ### WLED release 0.13.1 #### Build 2203150 @@ -180,7 +488,7 @@ - Added application level pong websockets reply (#2139) - Use AsyncTCP 1.0.3 as it mitigates the flickering issue from 0.13.0-b2 -- Fixed transition manually updated in preset overriden by field value +- Fixed transition manually updated in preset overridden by field value #### Build 2108050 @@ -709,7 +1017,7 @@ #### Build 2011040 -- Inversed Rain direction (fixes #1147) +- Inverted Rain direction (fixes #1147) #### Build 2011010 @@ -920,7 +1228,7 @@ - Added module info page to web UI - Added realtime override functionality to web UI -- Added individial segment power and brightness to web UI +- Added individual segment power and brightness to web UI - Added feature to one-click select single segment only by tapping segment name - Removed palette jumping to default if color is changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 560a709731..168131160f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,20 @@ Here are a few suggestions to make it easier for you to contribute! +### Describe your PR + +Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing. + +A good description helps us to review and understand your proposed changes. For example, you could say a few words about +* what you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.) +* how your code works (short technical summary - focus on important aspects that might not be obvious when reading the code) +* testing you performed, known limitations, open ends you possibly could not solve. +* any areas where you like to get help from an experienced maintainer (yes WLED has become big 😉) + +### Target branch for pull requests + +Please make all PRs against the `0_15` branch. + ### Code style When in doubt, it is easiest to replicate the code style you find in the files you want to edit :) @@ -73,6 +87,6 @@ Good: ``` -There is no set character limit for a comment within a line, -though as a rule of thumb you should wrap your comment if it exceeds the width of your editor window. -Inline comments are OK if they describe that line only and are not exceedingly wide. \ No newline at end of file +There is no hard character limit for a comment within a line, +though as a rule of thumb consider wrapping after 120 characters. +Inline comments are OK if they describe that line only and are not exceedingly wide. diff --git a/package-lock.json b/package-lock.json index 9b73790af7..7b57cebc03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,441 +1,362 @@ { "name": "wled", - "version": "0.13.1", - "lockfileVersion": 1, + "version": "0.14.4", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "requires": { - "defer-to-connect": "^1.0.1" + "packages": { + "": { + "name": "wled", + "version": "0.14.4", + "license": "ISC", + "dependencies": { + "clean-css": "^4.2.3", + "html-minifier-terser": "^5.1.1", + "inliner": "^1.13.1", + "nodemon": "^2.0.20", + "zlib": "^1.0.5" } }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, - "abbrev": { + "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, - "ajv": { + "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { + "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "align-text": { + "node_modules/align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "requires": { + "dependencies": { "kind-of": "^3.0.2", "longest": "^1.0.1", "repeat-string": "^1.5.2" - } - }, - "ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", - "requires": { - "string-width": "^3.0.0" }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } + "engines": { + "node": ">=0.10.0" } }, - "ansi-escapes": { + "node_modules/ansi-escapes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "engines": { + "node": ">=0.10.0" + } }, - "ansi-regex": { + "node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } }, - "ansi-styles": { + "node_modules/ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "engines": { + "node": ">=0.10.0" + } }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "requires": { + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "argparse": { + "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { + "dependencies": { "sprintf-js": "~1.0.2" } }, - "asap": { + "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, - "asn1": { + "node_modules/asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { + "dependencies": { "safer-buffer": "~2.1.0" } }, - "assert-plus": { + "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } }, - "asynckit": { + "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "aws-sign2": { + "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "engines": { + "node": "*" + } }, - "aws4": { + "node_modules/aws4": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "bcrypt-pbkdf": { + "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { + "dependencies": { "tweetnacl": "^0.14.3" } }, - "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } }, - "boolbase": { + "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, - "boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "brace-expansion": { + "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "braces": { + "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { + "dependencies": { "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" } }, - "buffer-from": { + "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, - "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" - } - } - }, - "camelcase": { + "node_modules/camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "engines": { + "node": ">=0.10.0" + } }, - "caseless": { + "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, - "center-align": { + "node_modules/center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "requires": { + "dependencies": { "align-text": "^0.1.3", "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" } }, - "chalk": { + "node_modules/chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { + "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "charset": { + "node_modules/charset": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", - "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==" + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", + "engines": { + "node": ">=4.0.0" + } }, - "cheerio": { + "node_modules/cheerio": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz", "integrity": "sha1-dy5wFfLuKZZQltcepBdbdas1SSU=", - "requires": { + "dependencies": { "css-select": "~1.0.0", "dom-serializer": "~0.1.0", "entities": "~1.1.1", "htmlparser2": "~3.8.1", "lodash": "^3.2.0" - } - }, - "chokidar": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", - "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", - "requires": { - "anymatch": "~3.1.1", + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" - }, - "clap": { + "node_modules/clap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", - "requires": { + "dependencies": { "chalk": "^1.1.3" + }, + "engines": { + "node": ">=0.10.0" } }, - "clean-css": { + "node_modules/clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "requires": { + "dependencies": { "source-map": "~0.6.0" }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } + "engines": { + "node": ">= 4.0" } }, - "cli-boxes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", - "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==" + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } }, - "cliui": { + "node_modules/cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "requires": { + "dependencies": { "center-align": "^0.1.1", "right-align": "^0.1.1", "wordwrap": "0.0.2" } }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "coa": { + "node_modules/coa": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", - "requires": { + "dependencies": { "q": "^1.1.2" + }, + "engines": { + "node": ">= 0.8.0" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "colors": { + "node_modules/colors": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "engines": { + "node": ">=0.1.90" + } }, - "combined-stream": { + "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { + "dependencies": { "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "commander": { + "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "configstore": { + "node_modules/configstore": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/configstore/-/configstore-1.4.0.tgz", "integrity": "sha1-w1eB0FAdJowlxUuLF/YkDopPsCE=", - "requires": { + "dependencies": { "graceful-fs": "^4.1.2", "mkdirp": "^0.5.0", "object-assign": "^4.0.1", @@ -445,342 +366,336 @@ "write-file-atomic": "^1.1.2", "xdg-basedir": "^2.0.0" }, - "dependencies": { - "uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" - } + "engines": { + "node": ">=0.10.0" } }, - "core-util-is": { + "node_modules/configstore/node_modules/uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details." + }, + "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" - }, - "css-select": { + "node_modules/css-select": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", "integrity": "sha1-sRIcpRhI3SZOIkTQWM7iVN7rRLA=", - "requires": { + "dependencies": { "boolbase": "~1.0.0", "css-what": "1.0", "domutils": "1.4", "nth-check": "~1.0.0" } }, - "css-what": { + "node_modules/css-what": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-1.0.0.tgz", - "integrity": "sha1-18wt9FGAZm+Z0rFEYmOUaeAPc2w=" + "integrity": "sha1-18wt9FGAZm+Z0rFEYmOUaeAPc2w=", + "engines": { + "node": "*" + } }, - "csso": { + "node_modules/csso": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/csso/-/csso-2.0.0.tgz", "integrity": "sha1-F4tDpEYhIhwndWCG9THgL0KQDug=", - "requires": { + "dependencies": { "clap": "^1.0.9", "source-map": "^0.5.3" + }, + "bin": { + "csso": "bin/csso" + }, + "engines": { + "node": ">=0.10.0" } }, - "dashdash": { + "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { + "dependencies": { "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" } }, - "debug": { + "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { + "dependencies": { "ms": "2.0.0" } }, - "decamelize": { + "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "requires": { - "mimic-response": "^1.0.0" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" } }, - "deep-extend": { + "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } }, - "delayed-stream": { + "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } }, - "dom-serializer": { + "node_modules/dom-serializer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "requires": { + "dependencies": { "domelementtype": "^1.3.0", "entities": "^1.1.1" } }, - "domelementtype": { + "node_modules/domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" }, - "domhandler": { + "node_modules/domhandler": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "requires": { + "dependencies": { "domelementtype": "1" } }, - "domutils": { + "node_modules/domutils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", - "requires": { + "dependencies": { "domelementtype": "1" } }, - "dot-case": { + "node_modules/dot-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz", "integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==", - "requires": { + "dependencies": { "no-case": "^3.0.3", "tslib": "^1.10.0" - }, - "dependencies": { - "lower-case": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", - "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", - "requires": { - "tslib": "^1.10.0" - } - }, - "no-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", - "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", - "requires": { - "lower-case": "^2.0.1", - "tslib": "^1.10.0" - } - } } }, - "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "requires": { - "is-obj": "^2.0.0" + "node_modules/dot-case/node_modules/lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dependencies": { + "tslib": "^1.10.0" } }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "node_modules/dot-case/node_modules/no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dependencies": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } }, - "duplexify": { + "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "requires": { + "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" - }, + } + }, + "node_modules/duplexify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "ecc-jsbn": { + "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { + "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "end-of-stream": { + "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { + "dependencies": { "once": "^1.4.0" } }, - "entities": { + "node_modules/entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" }, - "es6-promise": { + "node_modules/es6-promise": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.3.0.tgz", "integrity": "sha1-lu258v2wGZWCKyY92KratnSBgbw=" }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" - }, - "escape-string-regexp": { + "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } }, - "esprima": { + "node_modules/esprima": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.10.0" + } }, - "extend": { + "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "extsprintf": { + "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] }, - "fast-deep-equal": { + "node_modules/fast-deep-equal": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, - "fast-json-stable-stringify": { + "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, - "fill-range": { + "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { + "dependencies": { "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "forever-agent": { + "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } }, - "form-data": { + "node_modules/form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { + "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" } }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "optional": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "getpass": { + "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { + "dependencies": { "assert-plus": "^1.0.0" } }, - "glob-parent": { + "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { + "dependencies": { "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "global-dirs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", - "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", - "requires": { - "ini": "^1.3.5" - } - }, - "got": { + "node_modules/got": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/got/-/got-3.3.1.tgz", "integrity": "sha1-5dDtSvVfw+701WAHdp2YGSvLLso=", - "requires": { + "dependencies": { "duplexify": "^3.2.0", "infinity-agent": "^2.0.0", "is-redirect": "^1.0.0", @@ -792,61 +707,76 @@ "read-all-stream": "^3.0.0", "timed-out": "^2.0.0" }, - "dependencies": { - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" - } + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/got/node_modules/object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "engines": { + "node": ">=0.10.0" } }, - "graceful-fs": { + "node_modules/graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, - "har-schema": { + "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "engines": { + "node": ">=4" + } }, - "har-validator": { + "node_modules/har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { + "deprecated": "this library is no longer supported", + "dependencies": { "ajv": "^6.5.5", "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" } }, - "has-ansi": { + "node_modules/has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { + "dependencies": { "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "has-flag": { + "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } }, - "he": { + "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } }, - "html-minifier-terser": { + "node_modules/html-minifier-terser": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", - "requires": { + "dependencies": { "camel-case": "^4.1.1", "clean-css": "^4.2.3", "commander": "^4.1.1", @@ -855,118 +785,123 @@ "relateurl": "^0.2.7", "terser": "^4.6.3" }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/html-minifier-terser/node_modules/camel-case": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", + "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", "dependencies": { - "camel-case": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", - "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", - "requires": { - "pascal-case": "^3.1.1", - "tslib": "^1.10.0" - } - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" - }, - "param-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz", - "integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==", - "requires": { - "dot-case": "^3.0.3", - "tslib": "^1.10.0" - } - } + "pascal-case": "^3.1.1", + "tslib": "^1.10.0" } }, - "htmlparser2": { + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/html-minifier-terser/node_modules/param-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz", + "integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==", + "dependencies": { + "dot-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "node_modules/htmlparser2": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "requires": { + "dependencies": { "domelementtype": "1", "domhandler": "2.3", "domutils": "1.5", "entities": "1.0", "readable-stream": "1.1" - }, + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "dependencies": { - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" - } + "dom-serializer": "0", + "domelementtype": "1" } }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "node_modules/htmlparser2/node_modules/entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" }, - "http-signature": { + "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { + "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" } }, - "iconv-lite": { + "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { + "dependencies": { "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "ignore-by-default": { + "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" }, - "imurmurhash": { + "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "engines": { + "node": ">=0.8.19" + } }, - "infinity-agent": { + "node_modules/infinity-agent": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/infinity-agent/-/infinity-agent-2.0.3.tgz", "integrity": "sha1-ReDi/3qesDCyfWK3SzdEt6esQhY=" }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "ini": { + "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "inliner": { + "node_modules/inliner": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/inliner/-/inliner-1.13.1.tgz", "integrity": "sha1-5QApgev1Dp2fMTcRSBz/Ei1PP8s=", - "requires": { + "dependencies": { "ansi-escapes": "^1.4.0", "ansi-styles": "^2.2.1", "chalk": "^1.1.3", @@ -986,878 +921,775 @@ "then-fs": "^2.0.0", "uglify-js": "^2.8.0", "update-notifier": "^0.5.0" + }, + "bin": { + "inliner": "cli/index.js" } }, - "is-binary-path": { + "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { + "dependencies": { "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-buffer": { + "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-extglob": { + "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } }, - "is-finite": { + "node_modules/is-finite": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-npm": { + "node_modules/is-npm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "engines": { + "node": ">=0.10.0" + } }, - "is-number": { + "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" - }, - "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } }, - "is-redirect": { + "node_modules/is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "engines": { + "node": ">=0.10.0" + } }, - "is-stream": { + "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "engines": { + "node": ">=0.10.0" + } }, - "is-typedarray": { + "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, - "is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" - }, - "isarray": { + "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, - "isstream": { + "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "js-yaml": { + "node_modules/js-yaml": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", - "requires": { + "dependencies": { "argparse": "^1.0.7", "esprima": "^2.6.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "jsbn": { + "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, - "jschardet": { + "node_modules/jschardet": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.6.0.tgz", - "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==" - }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==", + "engines": { + "node": ">=0.1.90" + } }, - "json-schema": { + "node_modules/json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, - "json-schema-traverse": { + "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, - "json-stringify-safe": { + "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, - "jsprim": { + "node_modules/jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { + "engines": [ + "node >=0.6.0" + ], + "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.2.3", "verror": "1.10.0" } }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "requires": { - "json-buffer": "3.0.0" - } - }, - "kind-of": { + "node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { + "dependencies": { "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "latest-version": { + "node_modules/latest-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-1.0.1.tgz", "integrity": "sha1-cs/Ebj6NG+ZR4eu1Tqn26pbzdLs=", - "requires": { + "dependencies": { "package-json": "^1.0.0" + }, + "bin": { + "latest-version": "cli.js" + }, + "engines": { + "node": ">=0.10.0" } }, - "lazy-cache": { + "node_modules/lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "engines": { + "node": ">=0.10.0" + } }, - "lodash": { + "node_modules/lodash": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" }, - "lodash._arrayeach": { + "node_modules/lodash._arrayeach": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=" }, - "lodash._baseassign": { + "node_modules/lodash._baseassign": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "requires": { + "dependencies": { "lodash._basecopy": "^3.0.0", "lodash.keys": "^3.0.0" } }, - "lodash._basecopy": { + "node_modules/lodash._basecopy": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" }, - "lodash._baseeach": { + "node_modules/lodash._baseeach": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", "integrity": "sha1-z4cGVyyhROjZ11InyZDamC+TKvM=", - "requires": { + "dependencies": { "lodash.keys": "^3.0.0" } }, - "lodash._bindcallback": { + "node_modules/lodash._bindcallback": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" }, - "lodash._createassigner": { + "node_modules/lodash._createassigner": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", - "requires": { + "dependencies": { "lodash._bindcallback": "^3.0.0", "lodash._isiterateecall": "^3.0.0", "lodash.restparam": "^3.0.0" } }, - "lodash._getnative": { + "node_modules/lodash._getnative": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" }, - "lodash._isiterateecall": { + "node_modules/lodash._isiterateecall": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" }, - "lodash.assign": { + "node_modules/lodash.assign": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", - "requires": { + "dependencies": { "lodash._baseassign": "^3.0.0", "lodash._createassigner": "^3.0.0", "lodash.keys": "^3.0.0" } }, - "lodash.defaults": { + "node_modules/lodash.defaults": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=", - "requires": { + "dependencies": { "lodash.assign": "^3.0.0", "lodash.restparam": "^3.0.0" } }, - "lodash.foreach": { + "node_modules/lodash.foreach": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-3.0.3.tgz", "integrity": "sha1-b9fvt5aRrs1n/erCdhyY5wHWw5o=", - "requires": { + "dependencies": { "lodash._arrayeach": "^3.0.0", "lodash._baseeach": "^3.0.0", "lodash._bindcallback": "^3.0.0", "lodash.isarray": "^3.0.0" } }, - "lodash.isarguments": { + "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" }, - "lodash.isarray": { + "node_modules/lodash.isarray": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" }, - "lodash.keys": { + "node_modules/lodash.keys": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "requires": { + "dependencies": { "lodash._getnative": "^3.0.0", "lodash.isarguments": "^3.0.0", "lodash.isarray": "^3.0.0" } }, - "lodash.restparam": { + "node_modules/lodash.restparam": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" }, - "longest": { + "node_modules/longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "engines": { + "node": ">=0.10.0" + } }, - "lowercase-keys": { + "node_modules/lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "engines": { + "node": ">=0.10.0" } }, - "mime": { + "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } }, - "mime-db": { + "node_modules/mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "engines": { + "node": ">= 0.6" + } }, - "mime-types": { + "node_modules/mime-types": { "version": "2.1.27", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "requires": { + "dependencies": { "mime-db": "1.44.0" + }, + "engines": { + "node": ">= 0.6" } }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, - "mkdirp": { + "node_modules/mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { + "dependencies": { "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" } }, - "ms": { + "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, - "nested-error-stacks": { + "node_modules/nested-error-stacks": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz", "integrity": "sha1-GfYZWRUZ8JZ2mlupqG5u7sgjw88=", - "requires": { + "dependencies": { "inherits": "~2.0.1" } }, - "nodemon": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz", - "integrity": "sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ==", - "requires": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", + "node_modules/nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", "supports-color": "^5.5.0", "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - } - }, - "is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" - }, - "latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "requires": { - "package-json": "^6.3.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "requires": { - "rc": "^1.2.8" - } - }, - "semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "update-notifier": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz", - "integrity": "sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==", - "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" - } + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" } }, - "nopt": { + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "requires": { + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dependencies": { "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" } }, - "normalize-path": { + "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } }, - "nth-check": { + "node_modules/nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "requires": { + "dependencies": { "boolbase": "~1.0.0" } }, - "oauth-sign": { + "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } }, - "object-assign": { + "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } }, - "once": { + "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { + "dependencies": { "wrappy": "1" } }, - "os-homedir": { + "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "engines": { + "node": ">=0.10.0" + } }, - "os-tmpdir": { + "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "engines": { + "node": ">=0.10.0" + } }, - "osenv": { + "node_modules/osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { + "dependencies": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" } }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" - }, - "package-json": { + "node_modules/package-json": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/package-json/-/package-json-1.2.0.tgz", "integrity": "sha1-yOysCUInzfdqMWh07QXifMk5oOA=", - "requires": { + "dependencies": { "got": "^3.2.0", "registry-url": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "pascal-case": { + "node_modules/pascal-case": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz", "integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==", - "requires": { + "dependencies": { "no-case": "^3.0.3", "tslib": "^1.10.0" - }, + } + }, + "node_modules/pascal-case/node_modules/lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", "dependencies": { - "lower-case": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", - "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", - "requires": { - "tslib": "^1.10.0" - } - }, - "no-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", - "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", - "requires": { - "lower-case": "^2.0.1", - "tslib": "^1.10.0" - } - } + "tslib": "^1.10.0" } }, - "performance-now": { + "node_modules/pascal-case/node_modules/no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dependencies": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + }, + "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "pinkie": { + "node_modules/pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "engines": { + "node": ">=0.10.0" + } }, - "pinkie-promise": { + "node_modules/pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { + "dependencies": { "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "prepend-http": { + "node_modules/prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "engines": { + "node": ">=0.10.0" + } }, - "process-nextick-args": { + "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "promise": { + "node_modules/promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { + "dependencies": { "asap": "~2.0.3" } }, - "psl": { + "node_modules/psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, - "pstree.remy": { + "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { + "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "pupa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", - "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", - "requires": { - "escape-goat": "^2.0.0" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" } }, - "q": { + "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } }, - "rc": { + "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { + "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" } }, - "read-all-stream": { + "node_modules/read-all-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", - "requires": { + "dependencies": { "pinkie-promise": "^2.0.0", "readable-stream": "^2.0.0" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-all-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/read-all-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "readable-stream": { + "node_modules/read-all-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/read-all-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/readable-stream": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { + "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, - "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", - "requires": { + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "registry-auth-token": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.1.1.tgz", - "integrity": "sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA==", - "requires": { - "rc": "^1.2.8" - } - }, - "registry-url": { + "node_modules/registry-url": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "requires": { + "dependencies": { "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "relateurl": { + "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "engines": { + "node": ">= 0.10" + } }, - "repeat-string": { + "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "engines": { + "node": ">=0.10" + } }, - "repeating": { + "node_modules/repeating": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", "integrity": "sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw=", - "requires": { + "dependencies": { "is-finite": "^1.0.0" + }, + "bin": { + "repeating": "cli.js" + }, + "engines": { + "node": ">=0.10.0" } }, - "request": { + "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", @@ -1878,93 +1710,132 @@ "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" } }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "right-align": { + "node_modules/right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "requires": { + "dependencies": { "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "safe-buffer": { + "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "safer-buffer": { + "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sax": { + "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } }, - "semver-diff": { + "node_modules/semver-diff": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "requires": { + "dependencies": { "semver": "^5.0.3" + }, + "engines": { + "node": ">=0.10.0" } }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } }, - "slide": { + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/slide": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "engines": { + "node": "*" + } }, - "source-map": { + "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } }, - "source-map-support": { + "node_modules/source-map-support": { "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "requires": { + "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } } }, - "sprintf-js": { + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, - "sshpk": { + "node_modules/sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { + "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", @@ -1974,84 +1845,70 @@ "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" } }, - "stream-shift": { + "node_modules/stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, - "string-length": { + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "node_modules/string-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", - "requires": { + "dependencies": { "strip-ansi": "^3.0.0" - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - } + "engines": { + "node": ">=0.10.0" } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "strip-ansi": { + "node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { + "dependencies": { "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "strip-json-comments": { + "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "engines": { + "node": ">=0.10.0" + } }, - "supports-color": { + "node_modules/supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "engines": { + "node": ">=0.8.0" + } }, - "svgo": { + "node_modules/svgo": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.6.6.tgz", "integrity": "sha1-s0CIkDbyD5tEdUMHfQ9Vc+0ETAg=", - "requires": { + "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "dependencies": { "coa": "~1.0.1", "colors": "~1.1.2", "csso": "~2.0.0", @@ -2059,141 +1916,143 @@ "mkdirp": "~0.5.1", "sax": "~1.2.1", "whet.extend": "~0.9.9" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=0.10.0" } }, - "term-size": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", - "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" - }, - "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "requires": { + "node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dependencies": { "commander": "^2.20.0", "source-map": "~0.6.1", "source-map-support": "~0.5.12" }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" } }, - "then-fs": { + "node_modules/then-fs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/then-fs/-/then-fs-2.0.0.tgz", "integrity": "sha1-cveS3Z0xcFqRrhnr/Piz+WjIHaI=", - "requires": { + "dependencies": { "promise": ">=3.2 <8" } }, - "timed-out": { + "node_modules/timed-out": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", - "integrity": "sha1-84sK6B03R9YoAB9B2vxlKs5nHAo=" - }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + "integrity": "sha1-84sK6B03R9YoAB9B2vxlKs5nHAo=", + "engines": { + "node": ">=0.10.0" + } }, - "to-regex-range": { + "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { + "dependencies": { "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "touch": { + "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "requires": { + "dependencies": { "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" } }, - "tough-cookie": { + "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { + "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" } }, - "tslib": { + "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, - "tunnel-agent": { + "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { + "dependencies": { "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" } }, - "tweetnacl": { + "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "uglify-js": { + "node_modules/uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "requires": { + "dependencies": { "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", "yargs": "~3.10.0" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" } }, - "uglify-to-browserify": { + "node_modules/uglify-to-browserify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", "optional": true }, - "undefsafe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", - "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", - "requires": { - "debug": "^2.2.0" - } + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "requires": { - "crypto-random-string": "^2.0.0" - } - }, - "update-notifier": { + "node_modules/update-notifier": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-0.5.0.tgz", "integrity": "sha1-B7XcIGazYnqztPUwEw9+3doHpMw=", - "requires": { + "dependencies": { "chalk": "^1.0.0", "configstore": "^1.0.0", "is-npm": "^1.0.0", @@ -2201,112 +2060,115 @@ "repeating": "^1.1.2", "semver-diff": "^2.0.0", "string-length": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "uri-js": { + "node_modules/uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "requires": { - "prepend-http": "^2.0.0" - }, "dependencies": { - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" - } + "punycode": "^2.1.0" } }, - "util-deprecate": { + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "uuid": { + "node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } }, - "verror": { + "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { + "engines": [ + "node >=0.6.0" + ], + "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, - "whet.extend": { + "node_modules/whet.extend": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=" - }, - "widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "requires": { - "string-width": "^4.0.0" + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", + "engines": { + "node": ">=0.6.0" } }, - "window-size": { + "node_modules/window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "engines": { + "node": ">= 0.8.0" + } }, - "wordwrap": { + "node_modules/wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "engines": { + "node": ">=0.4.0" + } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "write-file-atomic": { + "node_modules/write-file-atomic": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", - "requires": { + "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", "slide": "^1.1.5" } }, - "xdg-basedir": { + "node_modules/xdg-basedir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", - "requires": { + "dependencies": { "os-homedir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "yargs": { + "node_modules/yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "requires": { + "dependencies": { "camelcase": "^1.0.2", "cliui": "^2.1.0", "decamelize": "^1.0.0", "window-size": "0.1.0" } }, - "zlib": { + "node_modules/zlib": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", - "integrity": "sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA=" + "integrity": "sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA=", + "hasInstallScript": true, + "engines": { + "node": ">=0.2.0" + } } } } diff --git a/package.json b/package.json index e60731b8c3..8171059d5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.13.1", + "version": "0.14.4", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { @@ -25,7 +25,7 @@ "clean-css": "^4.2.3", "html-minifier-terser": "^5.1.1", "inliner": "^1.13.1", - "nodemon": "^2.0.4", + "nodemon": "^2.0.20", "zlib": "^1.0.5" } } diff --git a/platformio.ini b/platformio.ini index 9fa95e4ad6..cbe13c0dd8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,16 +6,18 @@ # ENVIRONMENTS # # Please uncomment one of the lines below to select your board(s) +# (use `platformio_override.ini` when building for your own board; see `platformio_override.ini.sample` for an example) # ------------------------------------------------------------------------------ -# Travis CI binaries (use `platformio_override.ini` when building for your own board; see `platformio_override.ini.sample` for an example) -; default_envs = travis_esp8266, travis_esp32 +# CI binaries +; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth # ESP32 variant builds are temporarily excluded from CI due to toolchain issues on the GitHub Actions Linux environment +default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi # Release binaries -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32s2_saola, esp32c3 +; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB # Build everything -; default_envs = esp32dev, esp8285_4CH_MagicHome, codm-controller-0.6-rev2, codm-controller-0.6, esp32s2_saola, d1_mini_5CH_Shojo_PCB, d1_mini, sp501e, travis_esp8266, travis_esp32, nodemcuv2, esp32_eth, anavi_miracle_controller, esp07, esp01_1m_full, m5atom, h803wf, d1_mini_ota, heltec_wifi_kit_8, esp8285_H801, d1_mini_debug, wemos_shield_esp32, elekstube_ips +; default_envs = esp32dev, esp8285_4CH_MagicHome, codm-controller-0_6-rev2, codm-controller-0_6, esp32s2_saola, d1_mini_5CH_Shojo_PCB, d1_mini, sp501e, nodemcuv2, esp32_eth, anavi_miracle_controller, esp07, esp01_1m_full, m5atom, h803wf, d1_mini_ota, heltec_wifi_kit_8, esp8285_H801, d1_mini_debug, wemos_shield_esp32, elekstube_ips # Single binaries (uncomment your board) ; default_envs = elekstube_ips @@ -35,8 +37,12 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32s ; default_envs = wemos_shield_esp32 ; default_envs = m5atom ; default_envs = esp32_eth +; default_envs = esp32dev_qio80 ; default_envs = esp32_eth_ota1mapp ; default_envs = esp32s2_saola +; default_envs = esp32c3dev +; default_envs = lolin_s2_mini +; default_envs = esp32s3dev_16MB_PSRAM_opi src_dir = ./wled00 data_dir = ./wled00/data @@ -55,27 +61,39 @@ extra_configs = arduino_core_2_6_3 = espressif8266@2.3.3 arduino_core_2_7_4 = espressif8266@2.6.2 arduino_core_3_0_0 = espressif8266@3.0.0 -arduino_core_3_0_2 = espressif8266@3.2.0 +arduino_core_3_2_0 = espressif8266@3.2.0 +arduino_core_4_1_0 = espressif8266@4.1.0 +arduino_core_3_1_2 = espressif8266@4.2.1 # Development platforms arduino_core_develop = https://github.com/platformio/platform-espressif8266#develop arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/stage # Platform to use for ESP8266 -platform_wled_default = ${common.arduino_core_2_7_4} +platform_wled_default = ${common.arduino_core_3_1_2} # We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization -platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 - platformio/toolchain-xtensa @ ~2.40802.200502 - platformio/tool-esptool @ ~1.413.0 - platformio/tool-esptoolpy @ ~1.30000.0 +#platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 +platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 + platformio/tool-esptool #@ ~1.413.0 + platformio/tool-esptoolpy #@ ~1.30000.0 + +## previous platform for 8266, in case of problems with the new one +## you'll need makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_2_0, which does not support Ucs890x +;; platform_wled_default = ${common.arduino_core_3_2_0} +;; platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 +;; platformio/toolchain-xtensa @ ~2.40802.200502 +;; platformio/tool-esptool @ ~1.413.0 +;; platformio/tool-esptoolpy @ ~1.30000.0 # ------------------------------------------------------------------------------ # FLAGS: DEBUG -# +# esp8266 : see https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level +# esp32 : see https://docs.platformio.org/en/latest/platforms/espressif32.html#debug-level # ------------------------------------------------------------------------------ -debug_flags = -D DEBUG=1 -D WLED_DEBUG -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM -#if needed (for memleaks etc) also add; -DDEBUG_ESP_OOM -include "umm_malloc/umm_malloc_cfg.h" -#-DDEBUG_ESP_CORE is not working right now +debug_flags = -D DEBUG=1 -D WLED_DEBUG + -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM ;; for esp8266 + # if needed (for memleaks etc) also add; -DDEBUG_ESP_OOM -include "umm_malloc/umm_malloc_cfg.h" + # -DDEBUG_ESP_CORE is not working right now # ------------------------------------------------------------------------------ # FLAGS: ldscript (available ldscripts at https://github.com/esp8266/Arduino/tree/master/tools/sdk/ld) @@ -100,11 +118,13 @@ debug_flags = -D DEBUG=1 -D WLED_DEBUG -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT # This reduces the OTA size with ~45KB, so it's especially useful on low memory boards (512k/1m). # ------------------------------------------------------------------------------ build_flags = + -Wno-attributes -DMQTT_MAX_PACKET_SIZE=1024 -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL -DBEARSSL_SSL_BASIC -D CORE_DEBUG_LEVEL=0 -D NDEBUG + -Wno-attributes ;; silence warnings about unknown attribute 'maybe_unused' in NeoPixelBus #build_flags for the IRremoteESP8266 library (enabled decoders have to appear here) -D _IR_ENABLE_DEFAULT_=false -D DECODE_HASH=true @@ -112,20 +132,17 @@ build_flags = -D DECODE_SONY=true -D DECODE_SAMSUNG=true -D DECODE_LG=true + ;-Dregister= # remove warnings in C++17 due to use of deprecated register keyword by the FastLED library ;; warning: this breaks framework code on ESP32-C3 and ESP32-S2 -DWLED_USE_MY_CONFIG ; -D USERMOD_SENSORSTOMQTT + #For ADS1115 sensor uncomment following + ; -D USERMOD_ADS1115 build_unflags = -# enables all features for travis CI -build_flags_all_features = - -D WLED_ENABLE_ADALIGHT - -D WLED_ENABLE_DMX - -D WLED_ENABLE_MQTT - -D WLED_ENABLE_WEBSOCKETS - build_flags_esp8266 = ${common.build_flags} ${esp8266.build_flags} build_flags_esp32 = ${common.build_flags} ${esp32.build_flags} +build_flags_esp32_V4= ${common.build_flags} ${esp32_idf_V4.build_flags} ldscript_1m128k = eagle.flash.1m128.ld ldscript_2m512k = eagle.flash.2m512.ld @@ -155,26 +172,29 @@ upload_speed = 115200 # LIBRARIES: required dependencies # Please note that we don't always use the latest version of a library. # -# The following libraries have been included (and some of them changd) in the source: -# ArduinoJson@5.13.5, Blynk@0.5.4(changed), E131@1.0.0(changed), Time@1.5, Timezone@1.2.1 +# The following libraries have been included (and some of them changed) in the source: +# ArduinoJson@5.13.5, E131@1.0.0(changed), Time@1.5, Timezone@1.2.1 # ------------------------------------------------------------------------------ lib_compat_mode = strict lib_deps = - fastled/FastLED @ 3.5.0 - IRremoteESP8266 @ 2.8.1 - https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.4 + fastled/FastLED @ 3.6.0 + IRremoteESP8266 @ 2.8.2 + makuna/NeoPixelBus @ 2.7.5 + https://github.com/Aircoookie/ESPAsyncWebServer.git @ 2.2.1 #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line #TFT_eSPI - #For use SSD1306 OLED display uncomment following - #U8g2@~2.27.2 - #For Dallas sensor uncomment following 2 lines - #OneWire@~2.3.5 - #milesburton/DallasTemperature@^3.9.0 + #For compatible OLED display uncomment following + #U8g2 #@ ~2.33.15 + #For Dallas sensor uncomment following + #OneWire @ ~2.3.7 #For BME280 sensor uncomment following - #BME280@~3.0.0 + #BME280 @ ~3.0.0 ; adafruit/Adafruit BMP280 Library @ 2.1.0 ; adafruit/Adafruit CCS811 Library @ 1.0.4 ; adafruit/Adafruit Si7021 Library @ 1.4.0 + #For ADS1115 sensor uncomment following + ; adafruit/Adafruit BusIO @ 1.13.2 + ; adafruit/Adafruit ADS1X15 @ 2.4.0 extra_scripts = ${scripts_defaults.extra_scripts} @@ -183,72 +203,124 @@ build_flags = -DESP8266 -DFP_IN_IROM ;-Wno-deprecated-declarations - -Wno-register - -Wno-misleading-indentation -; NONOSDK22x_190703 = 2.2.2-dev(38a443e) + ;-Wno-register ;; leaves some warnings when compiling C files: command-line option '-Wno-register' is valid for C++/ObjC++ but not for C + ;-Dregister= # remove warnings in C++17 due to use of deprecated register keyword by the FastLED library ;; warning: this can be dangerous + -Wno-misleading-indentation + ; NONOSDK22x_190703 = 2.2.2-dev(38a443e) -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 -; lwIP 2 - Higher Bandwidth no Features -; -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH -; lwIP 1.4 - Higher Bandwidth (Aircoookie has) - -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH -; VTABLES in Flash + ; lwIP 2 - Higher Bandwidth no Features + ; -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH + ; lwIP 1.4 - Higher Bandwidth (Aircoookie has) + -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH + ; VTABLES in Flash -DVTABLES_IN_FLASH -; restrict to minimal mime-types + ; restrict to minimal mime-types -DMIMETYPE_MINIMAL + ; other special-purpose framework flags (see https://docs.platformio.org/en/latest/platforms/espressif8266.html) + -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 ;; in case of linker errors like "section `.text1' will not fit in region `iram1_0_seg'" + ; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED ;; (experimental) adds some extra heap, but may cause slowdown -lib_deps = - ${env.lib_deps} +lib_deps = #https://github.com/lorol/LITTLEFS.git - # ESPAsyncTCP @ 1.2.0 + ESPAsyncTCP @ 1.2.2 ESPAsyncUDP - makuna/NeoPixelBus @ 2.6.7 # 2.6.5/2.6.6 and newer do not compile on ESP core < 3.0.0 + ${env.lib_deps} [esp32] #platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.3/platform-espressif32-2.0.2.3.zip platform = espressif32@3.5.0 - platform_packages = framework-arduinoespressif32 @ https://github.com/Aircoookie/arduino-esp32.git#1.0.6.4 - build_flags = -g -DARDUINO_ARCH_ESP32 #-DCONFIG_LITTLEFS_FOR_IDF_3_2 -D CONFIG_ASYNC_TCP_USE_WDT=0 -#use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x + #use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x -D LOROL_LITTLEFS - + ; -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv - lib_deps = - ${env.lib_deps} https://github.com/lorol/LITTLEFS.git - makuna/NeoPixelBus @ 2.6.7 https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${env.lib_deps} +# additional build flags for audioreactive +AR_build_flags = -D USERMOD_AUDIOREACTIVE -D UM_AUDIOREACTIVE_USE_NEW_FFT +AR_lib_deps = https://github.com/kosme/arduinoFFT#419d7b0 + +[esp32_idf_V4] +;; experimental build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 +;; very similar to the normal ESP32 flags, but omitting Lorol LittleFS, as littlefs is included in the new framework already. +;; +;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly. +;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio. +platform = espressif32@5.3.0 +platform_packages = +build_flags = -g + -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one + -DARDUINO_ARCH_ESP32 -DESP32 + #-DCONFIG_LITTLEFS_FOR_IDF_3_2 + -D CONFIG_ASYNC_TCP_USE_WDT=0 + -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 +default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +lib_deps = + https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${env.lib_deps} [esp32s2] +;; generic definitions for all ESP32-S2 boards +platform = espressif32@5.3.0 +platform_packages = build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S2 - -DCONFIG_IDF_TARGET_ESP32S2 + -DCONFIG_IDF_TARGET_ESP32S2=1 -D CONFIG_ASYNC_TCP_USE_WDT=0 + -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 -DCO + -DARDUINO_USB_MODE=0 ;; this flag is mandatory for ESP32-S2 ! + ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: + ;; ARDUINO_USB_CDC_ON_BOOT lib_deps = - ${env.lib_deps} - makuna/NeoPixelBus @ 2.6.7 https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${env.lib_deps} [esp32c3] +;; generic definitions for all ESP32-C3 boards +platform = espressif32@5.3.0 +platform_packages = build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32C3 - -DCONFIG_IDF_TARGET_ESP32C3 + -DCONFIG_IDF_TARGET_ESP32C3=1 -D CONFIG_ASYNC_TCP_USE_WDT=0 -DCO + -DARDUINO_USB_MODE=1 ;; this flag is mandatory for ESP32-C3 + ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: + ;; ARDUINO_USB_CDC_ON_BOOT lib_deps = + https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} - makuna/NeoPixelBus @ 2.6.7 + +[esp32s3] +;; generic definitions for all ESP32-S3 boards +platform = espressif32@5.3.0 +platform_packages = +build_flags = -g + -DESP32 + -DARDUINO_ARCH_ESP32 + -DARDUINO_ARCH_ESP32S3 + -DCONFIG_IDF_TARGET_ESP32S3=1 + -D CONFIG_ASYNC_TCP_USE_WDT=0 + -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_DFU_ON_BOOT=0 + -DCO + ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: + ;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT + +lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${env.lib_deps} + # ------------------------------------------------------------------------------ # WLED BUILDS @@ -260,8 +332,14 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 #-DWLED_DISABLE_2D lib_deps = ${esp8266.lib_deps} +monitor_filters = esp8266_exception_decoder + +[env:nodemcuv2_160] +extends = env:nodemcuv2 +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266_160 #-DWLED_DISABLE_2D [env:esp8266_2m] board = esp_wroom_02 @@ -272,6 +350,11 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02 lib_deps = ${esp8266.lib_deps} +[env:esp8266_2m_160] +extends = env:esp8266_2m +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02_160 + [env:esp01_1m_full] board = esp01_1m platform = ${common.platform_wled_default} @@ -279,8 +362,15 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA + ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM lib_deps = ${esp8266.lib_deps} +[env:esp01_1m_full_160] +extends = env:esp01_1m_full +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01_160 -D WLED_DISABLE_OTA + ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM + [env:esp07] board = esp07 platform = ${common.platform_wled_default} @@ -324,10 +414,51 @@ board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BLYNK #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BROWNOUT_DET +lib_deps = ${esp32.lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.default_partitions} + +[env:esp32dev_audioreactive] +board = esp32dev +platform = ${esp32.platform} +platform_packages = ${esp32.platform_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_audioreactive #-D WLED_DISABLE_BROWNOUT_DET + ${esp32.AR_build_flags} +lib_deps = ${esp32.lib_deps} + ${esp32.AR_lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.default_partitions} +; board_build.f_flash = 80000000L +; board_build.flash_mode = dio + +[env:esp32dev_qio80] +board = esp32dev +platform = ${esp32.platform} +platform_packages = ${esp32.platform_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_qio80 #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} +board_build.f_flash = 80000000L +board_build.flash_mode = qio + +[env:esp32dev_V4_dio80] +;; experimental ESP32 env using ESP-IDF V4.4.x +;; Warning: this build environment is not stable!! +;; please erase your device before installing. +board = esp32dev +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_V4_qio80 #-D WLED_DISABLE_BROWNOUT_DET +lib_deps = ${esp32_idf_V4.lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32_idf_V4.default_partitions} +board_build.f_flash = 80000000L +board_build.flash_mode = dio [env:esp32_eth] board = esp32-poe @@ -335,7 +466,8 @@ platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D WLED_DISABLE_BLYNK +build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 + -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.default_partitions} @@ -348,18 +480,78 @@ board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv board_build.flash_mode = qio upload_speed = 460800 build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32s2.build_flags} #-D WLED_RELEASE_NAME=S2_saola + ;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work + -DARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s2.lib_deps} -[env:esp32c3] -board = esp32-c3-devkitm-1 -platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.2/platform-tasmota-espressif32-2.0.2.zip -platform_packages = +[env:esp32c3dev] +extends = esp32c3 +platform = ${esp32c3.platform} +platform_packages = ${esp32c3.platform_packages} framework = arduino +board = esp32-c3-devkitm-1 board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=ESP32-C3 + -D WLED_WATCHDOG_TIMEOUT=0 + -DLOLIN_WIFI_FIX ; seems to work much better with this + -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB + ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} +[env:esp32s3dev_8MB] +;; ESP32-S3-DevKitC-1 development board, with 8MB FLASH, no PSRAM (flash_mode: qio) +board = esp32-s3-devkitc-1 +platform = ${esp32s3.platform} +platform_packages = ${esp32s3.platform_packages} +upload_speed = 921600 ; or 460800 +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB + -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip + ;-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + ;-D WLED_DEBUG +lib_deps = ${esp32s3.lib_deps} +board_build.partitions = tools/WLED_ESP32_8MB.csv +board_build.f_flash = 80000000L +board_build.flash_mode = qio +; board_build.flash_mode = dio ;; try this if you have problems at startup +monitor_filters = esp32_exception_decoder + +[env:esp32s3dev_8MB_PSRAM_opi] +;; ESP32-S3 development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) +board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support +board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB +platform = ${esp32s3.platform} +platform_packages = ${esp32s3.platform_packages} +upload_speed = 921600 +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32s3.build_flags} + -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip + -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + ; -D WLED_RELEASE_NAME=ESP32-S3_PSRAM + -D WLED_USE_PSRAM -DBOARD_HAS_PSRAM ; tells WLED that PSRAM shall be used +lib_deps = ${esp32s3.lib_deps} +board_build.partitions = tools/WLED_ESP32_8MB.csv +board_build.f_flash = 80000000L +board_build.flash_mode = qio +monitor_filters = esp32_exception_decoder + +[env:esp32s3dev_16MB_PSRAM_opi] +extends = env:esp32s3dev_8MB_PSRAM_opi +board_build.partitions = tools/WLED_ESP32_16MB.csv +board_upload.flash_size = 16MB + +[env:esp32s3dev_8MB_PSRAM_qspi] +;; ESP32-TinyS3 development board, with 8MB FLASH and PSRAM (memory_type: qio_qspi) +extends = env:esp32s3dev_8MB_PSRAM_opi +;board = um_tinys3 ; -> needs workaround from https://github.com/Aircoookie/WLED/pull/2905#issuecomment-1328049860 +board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support +board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB + [env:esp8285_4CH_MagicHome] board = esp8285 platform = ${common.platform_wled_default} @@ -422,13 +614,62 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} -D LEDPIN=12 -D IRPIN=-1 -D RLYPIN=2 lib_deps = ${esp8266.lib_deps} +[env:lolin_s2_mini] +platform = ${esp32s2.platform} +platform_packages = ${esp32s2.platform_packages} +board = lolin_s2_mini +board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +build_unflags = ${common.build_unflags} #-DARDUINO_USB_CDC_ON_BOOT=1 +build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=ESP32-S2 + -DBOARD_HAS_PSRAM + -DARDUINO_USB_CDC_ON_BOOT=1 # try disabling and enabling unflag above in case of board-specific issues, will disable Serial + -DARDUINO_USB_MSC_ON_BOOT=0 + -DARDUINO_USB_DFU_ON_BOOT=0 + -DLOLIN_WIFI_FIX ; seems to work much better with this + -D WLED_USE_PSRAM + ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 6792 bytes FLASH + -D WLED_WATCHDOG_TIMEOUT=0 + -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D LEDPIN=16 + -D BTNPIN=18 + -D RLYPIN=9 + -D IRPIN=7 + -D HW_PIN_SCL=35 + -D HW_PIN_SDA=33 + -D HW_PIN_CLOCKSPI=7 + -D HW_PIN_DATASPI=11 + -D HW_PIN_MISOSPI=9 +; -D STATUSLED=15 +lib_deps = ${esp32s2.lib_deps} + # ------------------------------------------------------------------------------ # custom board configurations # ------------------------------------------------------------------------------ +[env:esp32c3dev_2MB] +;; for ESP32-C3 boards with 2MB flash (instead of 4MB). +;; this board need a specific partition file. OTA not possible. +extends = esp32c3 +platform = ${esp32c3.platform} +platform_packages = ${esp32c3.platform_packages} +board = esp32-c3-devkitm-1 +build_flags = ${common.build_flags} ${esp32c3.build_flags} #-D WLED_RELEASE_NAME=ESP32-C3 + -D WLED_WATCHDOG_TIMEOUT=0 + -D WLED_DISABLE_OTA + ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB + -DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip +build_unflags = ${common.build_unflags} +upload_speed = 115200 +lib_deps = ${esp32c3.lib_deps} +board_build.partitions = tools/WLED_ESP32_2MB_noOTA.csv +board_build.flash_mode = dio +board_upload.flash_size = 2MB +board_upload.maximum_size = 2097152 + [env:wemos_shield_esp32] board = esp32dev -platform = espressif32@3.2 +platform = ${esp32.platform} +platform_packages = ${esp32.platform_packages} upload_speed = 460800 build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp32} @@ -436,13 +677,16 @@ build_flags = ${common.build_flags_esp32} -D RLYPIN=19 -D BTNPIN=17 -D IRPIN=18 - -D UWLED_USE_MY_CONFIG + -U WLED_USE_MY_CONFIG -D USERMOD_DALLASTEMPERATURE -D USERMOD_FOUR_LINE_DISPLAY -D TEMPERATURE_PIN=23 + -D USE_ALT_DISPlAY ; new versions of USERMOD_FOUR_LINE_DISPLAY and USERMOD_ROTARY_ENCODER_UI + -D USERMOD_AUDIOREACTIVE lib_deps = ${esp32.lib_deps} OneWire@~2.3.5 olikraus/U8g2 @ ^2.28.8 + https://github.com/blazoncek/arduinoFFT.git board_build.partitions = ${esp32.default_partitions} [env:m5atom] @@ -450,7 +694,8 @@ board = esp32dev build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp32} -D LEDPIN=27 -D BTNPIN=39 lib_deps = ${esp32.lib_deps} -platform = espressif32@3.2 +platform = ${esp32.platform} +platform_packages = ${esp32.platform_packages} board_build.partitions = ${esp32.default_partitions} [env:sp501e] @@ -467,42 +712,73 @@ board_build.ldscript = ${common.ldscript_2m512k} build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3 lib_deps = ${esp8266.lib_deps} -[env:athom7w] -board = esp_wroom_02 +[env:Athom_RGBCW] ;7w and 5w(GU10) bulbs +board = esp8285 platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} -build_flags = ${common.build_flags_esp8266} -D WLED_MAX_CCT_BLEND=0 -D BTNPIN=-1 -D IRPIN=-1 -D WLED_DISABLE_INFRARED +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 + -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 lib_deps = ${esp8266.lib_deps} -[env:athom15w] -board = esp_wroom_02 + +[env:Athom_15w_RGBCW] ;15w bulb +board = esp8285 +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_2m512k} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 + -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT +lib_deps = ${esp8266.lib_deps} + + +[env:Athom_3Pin_Controller] ;small controller with only data +board = esp8285 platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} -build_flags = ${common.build_flags_esp8266} -D WLED_USE_IC_CCT -D BTNPIN=-1 -D IRPIN=-1 -D WLED_DISABLE_INFRARED +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=-1 -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} -# ------------------------------------------------------------------------------ -# travis test board configurations -# ------------------------------------------------------------------------------ -[env:travis_esp8266] -extends = env:d1_mini -build_type = debug +[env:Athom_4Pin_Controller] ; With clock and data interface +board = esp8285 +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_2m512k} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=12 -D LEDPIN=1 -D WLED_DISABLE_INFRARED +lib_deps = ${esp8266.lib_deps} + + +[env:Athom_5Pin_Controller] ;Analog light strip controller +board = esp8285 +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} ${common.debug_flags} ${common.build_flags_all_features} +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED +lib_deps = ${esp8266.lib_deps} -[env:travis_esp32] -extends = env:esp32dev -; build_type = debug + +[env:MY9291] +board = esp01_1m +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} ${common.debug_flags} ${common.build_flags_all_features} +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA -D USERMOD_MY9291 +lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ # codm pixel controller board configurations -# codm-controller-0.6 can also be used for the TYWE3S controller +# codm-controller-0_6 can also be used for the TYWE3S controller # ------------------------------------------------------------------------------ -[env:codm-controller-0.6] +[env:codm-controller-0_6] board = esp_wroom_02 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} @@ -511,7 +787,7 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} lib_deps = ${esp8266.lib_deps} -[env:codm-controller-0.6-rev2] +[env:codm-controller-0_6-rev2] board = esp_wroom_02 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} @@ -525,7 +801,8 @@ lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ [env:elekstube_ips] board = esp32dev -platform = espressif32@3.2 +platform = ${esp32.platform} +platform_packages = ${esp32.platform_packages} upload_speed = 921600 build_flags = ${common.build_flags_esp32} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED -D USERMOD_RTC @@ -533,7 +810,6 @@ build_flags = ${common.build_flags_esp32} -D WLED_DISABLE_BROWNOUT_DET -D WLED_D -D LEDPIN=12 -D RLYPIN=27 -D BTNPIN=34 - -D WLED_DISABLE_BLYNK -D DEFAULT_LED_COUNT=6 # Display config -D ST7789_DRIVER diff --git a/platformio_override.ini.sample b/platformio_override.ini.sample deleted file mode 100644 index 422a5f7e96..0000000000 --- a/platformio_override.ini.sample +++ /dev/null @@ -1,63 +0,0 @@ -# Example PlatformIO Project Configuration Override -# ------------------------------------------------------------------------------ -# Copy to platformio_override.ini to activate overrides -# ------------------------------------------------------------------------------ -# Please visit documentation: https://docs.platformio.org/page/projectconf.html - -[platformio] -default_envs = WLED_tasmota_1M - -[env:WLED_tasmota_1M] -board = esp01_1m -platform = ${common.platform_wled_default} -platform_packages = ${common.platform_packages} -board_build.ldscript = ${common.ldscript_1m128k} -lib_deps = ${esp8266.lib_deps} -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -; ********************************************************************* -; *** Use custom settings from file my_config.h - -DWLED_USE_MY_CONFIG -; ********************************************************************* -; -; -; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. -; -; disable specific features -; -D WLED_DISABLE_OTA -; -D WLED_DISABLE_ALEXA -; -D WLED_DISABLE_BLYNK -; -D WLED_DISABLE_HUESYNC -; -D WLED_DISABLE_INFRARED -; -D WLED_DISABLE_WEBSOCKETS -; PIN defines - uncomment and change, if needed: -; -D LEDPIN=2 -; -D BTNPIN=0 -; -D TOUCHPIN=T0 -; -D IRPIN=4 -; -D RLYPIN=12 -; -D RLYMDE=1 -; digital LED strip types - uncomment only one ! - this will disable WS281x / SK681x support -; -D USE_APA102 -; -D USE_WS2801 -; -D USE_LPD8806 -; PIN defines for 2 wire LEDs - -D CLKPIN=0 - -D DATAPIN=2 -; to drive analog LED strips (aka 5050) hardware configuration is no longer necessary -; configure the settings in the UI as follows (hard): -; for the Magic Home LED Controller use PWM pins 5,12,13,15 -; for the H801 controller use PINs 15,13,12,14 (W2 = 04) -; for the BW-LT11 controller use PINs 12,4,14,5 -; -; set the name of the module - make sure there is a quote-backslash-quote before the name and a backslash-quote-quote after the name -; -D SERVERNAME="\"WLED\"" -; -; set the number of LEDs -; -D DEFAULT_LED_COUNT=30 -; -; set milliampere limit when using ESP pin to power leds -; -D ABL_MILLIAMPS_DEFAULT =850 -; -; enable IR by setting remote type -; -D IRTYPE=0 //0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote diff --git a/readme.md b/readme.md index 9ae90625dc..dbb32c961f 100644 --- a/readme.md +++ b/readme.md @@ -18,28 +18,27 @@ Fork of the WLED project where the Adalight USB serial protocol @115200 speed is

WLED is receiving data from the USB serial port at @2000000 baud:


-## ⚙️ Features of WLED 0.12 -- WS2812FX library integrated for over 100 special effects +## ⚙️ Features +- WS2812FX library with more than 100 special effects - FastLED noise effects and 50 palettes - Modern UI with color, effect and segment controls -- Segments to set different effects and colors to parts of the LEDs -- Settings page - configuration over network +- Segments to set different effects and colors to user defined parts of the LED string +- Settings page - configuration via the network - Access Point and station mode - automatic failsafe AP - Up to 10 LED outputs per instance - Support for RGBW strips - Up to 250 user presets to save and load colors/effects easily, supports cycling through them. - Presets can be used to automatically execute API calls - Nightlight function (gradually dims down) -- Full OTA software updatability (HTTP + ArduinoOTA), password protectable +- Full OTA software updateability (HTTP + ArduinoOTA), password protectable - Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods) -- Configurable Auto Brightness limit for safer operation +- Configurable Auto Brightness limit for safe operation - Filesystem-based config for easier backup of presets and settings ## 💡 Supported light control interfaces - WLED app for [Android](https://play.google.com/store/apps/details?id=com.aircoookie.WLED) and [iOS](https://apps.apple.com/us/app/wled/id1475695033) - JSON and HTTP request APIs -- MQTT -- Blynk IoT +- MQTT - E1.31, Art-Net, DDP and TPM2.net - [diyHue](https://github.com/diyhue/diyHue) (Wled is supported by diyHue, including Hue Sync Entertainment under udp. Thanks to [Gregory Mallios](https://github.com/gmallios)) - UDP realtime @@ -54,7 +53,7 @@ Fork of the WLED project where the Adalight USB serial protocol @115200 speed is See the [documentation on our official site](https://kno.wled.ge)! -[On this page](https://kno.wled.ge/basics/tutorials/) you can find excellent tutorials made by the community and helpful tools to help you get your new lamp up and running! +[On this page](https://kno.wled.ge/basics/tutorials/) you can find excellent tutorials and tools to help you get your new project up and running! ## 🖼️ User interface @@ -70,15 +69,19 @@ Credits [here](https://kno.wled.ge/about/contributors/)! Join the Discord server to discuss everything about WLED! - + Check out the WLED [Discourse forum](https://wled.discourse.group)! -You can also send me mails to [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com), but please only do so if you want to talk to me privately. -If WLED really brightens up your every day, you can [![](https://img.shields.io/badge/send%20me%20a%20small%20gift-paypal-blue.svg?style=flat-square)](https://paypal.me/aircoookie) + +You can also send me mails to [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com), but please, only do so if you want to talk to me privately. + +If WLED really brightens up your day, you can [![](https://img.shields.io/badge/send%20me%20a%20small%20gift-paypal-blue.svg?style=flat-square)](https://paypal.me/aircoookie) *Disclaimer:* -If you are sensitive to photosensitive epilepsy it is not recommended that you use this software. -In case you still want to try, don't use strobe, lighting or noise modes or high effect speed settings. + +If you are prone to photosensitive epilepsy, we recommended you do **not** use this software. +If you still want to try, don't use strobe, lighting or noise modes or high effect speed settings. + As per the MIT license, I assume no liability for any damage to you or any other person or equipment. diff --git a/requirements.txt b/requirements.txt index c3ed11fc16..c56efad498 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,54 +1,60 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile # -aiofiles==0.6.0 +aiofiles==22.1.0 # via platformio -ajsonrpc==1.1.0 +ajsonrpc==1.2.0 # via platformio -bottle==0.12.19 +anyio==3.6.2 + # via starlette +bottle==0.12.25 # via platformio -certifi==2020.12.5 +certifi==2023.7.22 # via requests -chardet==4.0.0 +charset-normalizer==3.1.0 # via requests -click==7.1.2 +click==8.1.3 # via # platformio # uvicorn -colorama==0.4.4 +colorama==0.4.6 # via platformio -h11==0.12.0 +h11==0.14.0 # via # uvicorn # wsproto -idna==2.10 - # via requests -ifaddr==0.1.7 - # via zeroconf -marshmallow==3.11.1 +idna==3.7 + # via + # anyio + # requests +marshmallow==3.19.0 # via platformio -platformio==5.1.1 +packaging==23.1 + # via marshmallow +platformio==6.1.6 # via -r requirements.in -pyelftools==0.27 +pyelftools==0.29 # via platformio pyserial==3.5 # via platformio -requests==2.25.1 +requests==2.31.0 # via platformio -semantic-version==2.8.5 +semantic-version==2.10.0 # via platformio -starlette==0.14.2 +sniffio==1.3.0 + # via anyio +starlette==0.23.1 # via platformio -tabulate==0.8.9 +tabulate==0.9.0 # via platformio -urllib3==1.26.5 +typing-extensions==4.11.0 + # via starlette +urllib3==1.26.18 # via requests -uvicorn==0.13.4 - # via platformio -wsproto==1.0.0 +uvicorn==0.20.0 # via platformio -zeroconf==0.28.8 +wsproto==1.2.0 # via platformio diff --git a/tools/WLED_ESP32-wrover_4MB.csv b/tools/WLED_ESP32-wrover_4MB.csv new file mode 100644 index 0000000000..a179a89d0e --- /dev/null +++ b/tools/WLED_ESP32-wrover_4MB.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x180000, +app1, app, ota_1, 0x190000,0x180000, +spiffs, data, spiffs, 0x310000,0xF0000, diff --git a/tools/WLED_ESP32_16MB_9MB_FS.csv b/tools/WLED_ESP32_16MB_9MB_FS.csv new file mode 100644 index 0000000000..f2f3f7783a --- /dev/null +++ b/tools/WLED_ESP32_16MB_9MB_FS.csv @@ -0,0 +1,8 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x300000, +app1, app, ota_1, 0x310000,0x300000, +spiffs, data, spiffs, 0x610000,0x9E0000, +coredump, data, coredump,,64K +# to create/use ffat, see https://github.com/marcmerlin/esp32_fatfsimage \ No newline at end of file diff --git a/tools/WLED_ESP32_2MB_noOTA.csv b/tools/WLED_ESP32_2MB_noOTA.csv new file mode 100644 index 0000000000..7a1cf15f89 --- /dev/null +++ b/tools/WLED_ESP32_2MB_noOTA.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 20K, +otadata, data, ota, 0xe000, 8K, +app0, app, ota_0, 0x10000, 1536K, +spiffs, data, spiffs, 0x190000, 384K, diff --git a/tools/WLED_ESP32_8MB.csv b/tools/WLED_ESP32_8MB.csv index 5e930b89a2..3cf3afc342 100644 --- a/tools/WLED_ESP32_8MB.csv +++ b/tools/WLED_ESP32_8MB.csv @@ -3,4 +3,5 @@ nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x200000, app1, app, ota_1, 0x210000,0x200000, -spiffs, data, spiffs, 0x410000,0x3F0000, \ No newline at end of file +spiffs, data, spiffs, 0x410000,0x3E0000, +coredump, data, coredump,,64K diff --git a/tools/cdata.js b/tools/cdata.js index 15455a4284..90619ba678 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -16,20 +16,31 @@ */ const fs = require("fs"); +const inliner = require("inliner"); +const zlib = require("zlib"); +const CleanCSS = require("clean-css"); +const MinifyHTML = require("html-minifier-terser").minify; const packageJson = require("../package.json"); /** * */ -function hexdump(buffer) { +function hexdump(buffer,isHex=false) { let lines = []; - for (let i = 0; i < buffer.length; i += 16) { - let block = buffer.slice(i, i + 16); // cut buffer into blocks of 16 + for (let i = 0; i < buffer.length; i +=(isHex?32:16)) { + var block; let hexArray = []; - - for (let value of block) { - hexArray.push("0x" + value.toString(16).padStart(2, "0")); + if (isHex) { + block = buffer.slice(i, i + 32) + for (let j = 0; j < block.length; j +=2 ) { + hexArray.push("0x" + block.slice(j,j+2)) + } + } else { + block = buffer.slice(i, i + 16); // cut buffer into blocks of 16 + for (let value of block) { + hexArray.push("0x" + value.toString(16).padStart(2, "0")); + } } let hexString = hexArray.join(", "); @@ -40,9 +51,6 @@ function hexdump(buffer) { return lines.join(",\n"); } -const inliner = require("inliner"); -const zlib = require("zlib"); - function strReplace(str, search, replacement) { return str.split(search).join(replacement); } @@ -56,16 +64,52 @@ function adoptVersionAndRepo(html) { html = strReplace(html, "https://github.com/atuline/WLED", repoUrl); html = strReplace(html, "https://github.com/Aircoookie/WLED", repoUrl); } - let version = packageJson.version; if (version) { html = strReplace(html, "##VERSION##", version); } - return html; } -function writeHtmlGzipped(sourceFile, resultFile) { +function filter(str, type) { + str = adoptVersionAndRepo(str); + if (type === undefined) { + return str; + } else if (type == "css-minify") { + return new CleanCSS({}).minify(str).styles; + } else if (type == "js-minify") { + return MinifyHTML('', { + collapseWhitespace: true, + minifyJS: true, + continueOnParseError: false, + removeComments: true, + }).replace(/<[\/]*script>/g,''); + } else if (type == "html-minify") { + return MinifyHTML(str, { + collapseWhitespace: true, + maxLineLength: 80, + minifyCSS: true, + minifyJS: true, + continueOnParseError: false, + removeComments: true, + }); + } else if (type == "html-minify-ui") { + return MinifyHTML(str, { + collapseWhitespace: true, + conservativeCollapse: true, + maxLineLength: 80, + minifyCSS: true, + minifyJS: true, + continueOnParseError: false, + removeComments: true, + }); + } else { + console.warn("Unknown filter: " + type); + return str; + } +} + +function writeHtmlGzipped(sourceFile, resultFile, page) { console.info("Reading " + sourceFile); new inliner(sourceFile, function (error, html) { console.info("Inlined " + html.length + " characters"); @@ -95,8 +139,8 @@ function writeHtmlGzipped(sourceFile, resultFile) { */ // Autogenerated from ${sourceFile}, do not edit!! -const uint16_t PAGE_index_L = ${result.length}; -const uint8_t PAGE_index[] PROGMEM = { +const uint16_t PAGE_${page}_L = ${result.length}; +const uint8_t PAGE_${page}[] PROGMEM = { ${array} }; `; @@ -106,41 +150,6 @@ ${array} }); } -const CleanCSS = require("clean-css"); -const MinifyHTML = require("html-minifier-terser").minify; - -function filter(str, type) { - str = adoptVersionAndRepo(str); - - if (type === undefined) { - return str; - } else if (type == "css-minify") { - return new CleanCSS({}).minify(str).styles; - } else if (type == "html-minify") { - return MinifyHTML(str, { - collapseWhitespace: true, - maxLineLength: 80, - minifyCSS: true, - minifyJS: true, - continueOnParseError: false, - removeComments: true, - }); - } else if (type == "html-minify-ui") { - return MinifyHTML(str, { - collapseWhitespace: true, - conservativeCollapse: true, - maxLineLength: 80, - minifyCSS: true, - minifyJS: true, - continueOnParseError: false, - removeComments: true, - }); - } else { - console.warn("Unknown filter: " + type); - return str; - } -} - function specToChunk(srcDir, s) { if (s.method == "plaintext") { const buf = fs.readFileSync(srcDir + "/" + s.file); @@ -153,6 +162,21 @@ const char ${s.name}[] PROGMEM = R"${s.prepend || ""}${filter(str, s.filter)}${ `; return s.mangle ? s.mangle(chunk) : chunk; + } else if (s.method == "gzip") { + const buf = fs.readFileSync(srcDir + "/" + s.file); + var str = buf.toString('utf-8'); + if (s.mangle) str = s.mangle(str); + const zip = zlib.gzipSync(filter(str, s.filter), { level: zlib.constants.Z_BEST_COMPRESSION }); + const result = hexdump(zip.toString('hex'), true); + const chunk = ` +// Autogenerated from ${srcDir}/${s.file}, do not edit!! +const uint16_t ${s.name}_length = ${zip.length}; +const uint8_t ${s.name}[] PROGMEM = { +${result} +}; + +`; + return chunk; } else if (s.method == "binary") { const buf = fs.readFileSync(srcDir + "/" + s.file); const result = hexdump(buf); @@ -164,7 +188,7 @@ ${result} }; `; - return s.mangle ? s.mangle(chunk) : chunk; + return chunk; } else { console.warn("Unknown method: " + s.method); return undefined; @@ -194,160 +218,114 @@ function writeChunks(srcDir, specs, resultFile) { fs.writeFileSync(resultFile, src); } -writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h"); - +writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index'); +writeHtmlGzipped("wled00/data/simple.htm", "wled00/html_simple.h", 'simple'); +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'); +/* +writeChunks( + "wled00/data", + [ + { + file: "simple.css", + name: "PAGE_simpleCss", + method: "gzip", + filter: "css-minify", + }, + { + file: "simple.js", + name: "PAGE_simpleJs", + method: "gzip", + filter: "js-minify", + }, + { + file: "simple.htm", + name: "PAGE_simple", + method: "gzip", + filter: "html-minify-ui", + } + ], + "wled00/html_simplex.h" +); +*/ writeChunks( "wled00/data", [ { file: "style.css", name: "PAGE_settingsCss", - prepend: "=====()=====", - method: "plaintext", + method: "gzip", filter: "css-minify", + mangle: (str) => + str + .replace("%%","%") }, { file: "settings.htm", name: "PAGE_settings", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", - mangle: (str) => - str - .replace("%", "%%") - .replace(/Usermods\<\/button\>\<\/form\>/gms, "Usermods\<\/button\>\<\/form\>%DMXMENU%"), }, { file: "settings_wifi.htm", name: "PAGE_settings_wifi", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", - mangle: (str) => - str - .replace(/\/gms, "") - .replace(/\.*\<\/style\>/gms, "%CSS%%SCSS%") - .replace( - /function GetV().*\<\/script\>/gms, - "function GetV() {var d=document;\n" - ), }, { file: "settings_leds.htm", name: "PAGE_settings_leds", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", - mangle: (str) => - str - .replace(/\/gms, "") - .replace(/\.*\<\/style\>/gms, "%CSS%%SCSS%") - .replace( - /function GetV().*\<\/script\>/gms, - "function GetV() {var d=document;\n" - ), }, { file: "settings_dmx.htm", name: "PAGE_settings_dmx", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", - mangle: (str) => { - const nocss = str - .replace(/\/gms, "") - .replace(/\.*\<\/style\>/gms, "%CSS%%SCSS%") - .replace( - /function GetV().*\<\/script\>/gms, - "function GetV() {var d=document;\n" - ); - return ` -#ifdef WLED_ENABLE_DMX -${nocss} -#else -const char PAGE_settings_dmx[] PROGMEM = R"=====()====="; -#endif -`; - }, }, { file: "settings_ui.htm", name: "PAGE_settings_ui", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", - mangle: (str) => - str - .replace(/\/gms, "") - .replace(/\.*\<\/style\>/gms, "%CSS%%SCSS%") - .replace( - /function GetV().*\<\/script\>/gms, - "function GetV() {var d=document;\n" - ), }, { file: "settings_sync.htm", name: "PAGE_settings_sync", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", - mangle: (str) => - str - .replace(/\/gms, "") - .replace(/\.*\<\/style\>/gms, "%CSS%%SCSS%") - .replace(/function GetV().*\<\/script\>/gms, "function GetV() {\n"), }, { file: "settings_time.htm", name: "PAGE_settings_time", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", - mangle: (str) => - str - .replace(/\/gms, "") - .replace(/\.*\<\/style\>/gms, "%CSS%%SCSS%") - .replace(/function GetV().*\<\/script\>/gms, "function GetV() {\n"), }, { file: "settings_sec.htm", name: "PAGE_settings_sec", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", - mangle: (str) => - str - .replace(/\/gms, "") - .replace(/\.*\<\/style\>/gms, "%CSS%%SCSS%") - .replace( - /function GetV().*\<\/script\>/gms, - "function GetV() {var d=document;\n" - ), }, { file: "settings_um.htm", name: "PAGE_settings_um", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", - mangle: (str) => - str - .replace(/\/gms, "") - .replace(/\.*\<\/style\>/gms, "%CSS%%SCSS%") - .replace( - /function GetV().*\<\/script\>/gms, - "function GetV() {var d=document;\n" - ), + }, + { + file: "settings_2D.htm", + name: "PAGE_settings_2D", + method: "gzip", + filter: "html-minify", + }, + { + file: "settings_pin.htm", + name: "PAGE_settings_pin", + method: "gzip", + filter: "html-minify" } ], "wled00/html_settings.h" @@ -359,9 +337,7 @@ writeChunks( { file: "usermod.htm", name: "PAGE_usermod", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", mangle: (str) => str.replace(/fetch\("http\:\/\/.*\/win/gms, 'fetch("/win'), @@ -393,41 +369,37 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; { file: "update.htm", name: "PAGE_update", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", + mangle: (str) => + str + .replace( + /function GetV().*\<\/script\>/gms, + "" + ) }, { file: "welcome.htm", name: "PAGE_welcome", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", }, { file: "liveview.htm", name: "PAGE_liveview", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", }, { - file: "liveviewws.htm", - name: "PAGE_liveviewws", - prepend: "=====(", - append: ")=====", - method: "plaintext", + file: "liveviewws2D.htm", + name: "PAGE_liveviewws2D", + method: "gzip", filter: "html-minify", }, { file: "404.htm", name: "PAGE_404", - prepend: "=====(", - append: ")=====", - method: "plaintext", + method: "gzip", filter: "html-minify", }, { @@ -435,6 +407,16 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; name: "favicon", method: "binary", }, + { + file: "iro.js", + name: "iroJs", + method: "gzip" + }, + { + file: "rangetouch.js", + name: "rangetouchJs", + method: "gzip" + } ], "wled00/html_other.h" ); diff --git a/usermods/ADS1115_v2/ChannelSettings.h b/usermods/ADS1115_v2/ChannelSettings.h new file mode 100644 index 0000000000..26538a1426 --- /dev/null +++ b/usermods/ADS1115_v2/ChannelSettings.h @@ -0,0 +1,15 @@ +#include "wled.h" + +namespace ADS1115 +{ + struct ChannelSettings { + const String settingName; + bool isEnabled; + String name; + String units; + const uint16_t mux; + float multiplier; + float offset; + uint8_t decimals; + }; +} \ No newline at end of file diff --git a/usermods/ADS1115_v2/readme.md b/usermods/ADS1115_v2/readme.md new file mode 100644 index 0000000000..44092bc8e7 --- /dev/null +++ b/usermods/ADS1115_v2/readme.md @@ -0,0 +1,10 @@ +# ADS1115 16-Bit ADC with four inputs + +This usermod will read from an ADS1115 ADC. The voltages are displayed in the Info section of the web UI. + +Configuration is performed via the Usermod menu. There are no parameters to set in code! + +## Installation + +Add the build flag `-D USERMOD_ADS1115` to your platformio environment. +Uncomment libraries with comment `#For ADS1115 sensor uncomment following` diff --git a/usermods/ADS1115_v2/usermod_ads1115.h b/usermods/ADS1115_v2/usermod_ads1115.h new file mode 100644 index 0000000000..5e2b4b2703 --- /dev/null +++ b/usermods/ADS1115_v2/usermod_ads1115.h @@ -0,0 +1,255 @@ +#pragma once + +#include "wled.h" +#include +#include + +#include "ChannelSettings.h" + +using namespace ADS1115; + +class ADS1115Usermod : public Usermod { + public: + void setup() { + ads.setGain(GAIN_ONE); // 1x gain +/- 4.096V + + if (!ads.begin()) { + Serial.println("Failed to initialize ADS"); + return; + } + + if (!initChannel()) { + isInitialized = true; + return; + } + + startReading(); + + isEnabled = true; + isInitialized = true; + } + + void loop() { + if (isEnabled && millis() - lastTime > loopInterval) { + lastTime = millis(); + + // If we don't have new data, skip this iteration. + if (!ads.conversionComplete()) { + return; + } + + updateResult(); + moveToNextChannel(); + startReading(); + } + } + + void addToJsonInfo(JsonObject& root) + { + if (!isEnabled) { + return; + } + + JsonObject user = root[F("u")]; + if (user.isNull()) user = root.createNestedObject(F("u")); + + for (uint8_t i = 0; i < channelsCount; i++) { + ChannelSettings* settingsPtr = &(channelSettings[i]); + + if (!settingsPtr->isEnabled) { + continue; + } + + JsonArray lightArr = user.createNestedArray(settingsPtr->name); //name + float value = round((readings[i] + settingsPtr->offset) * settingsPtr->multiplier, settingsPtr->decimals); + lightArr.add(value); //value + lightArr.add(" " + settingsPtr->units); //unit + } + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(F("ADC ADS1115")); + + for (uint8_t i = 0; i < channelsCount; i++) { + ChannelSettings* settingsPtr = &(channelSettings[i]); + JsonObject channel = top.createNestedObject(settingsPtr->settingName); + channel[F("Enabled")] = settingsPtr->isEnabled; + channel[F("Name")] = settingsPtr->name; + channel[F("Units")] = settingsPtr->units; + channel[F("Multiplier")] = settingsPtr->multiplier; + channel[F("Offset")] = settingsPtr->offset; + channel[F("Decimals")] = settingsPtr->decimals; + } + + top[F("Loop Interval")] = loopInterval; + } + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root[F("ADC ADS1115")]; + + bool configComplete = !top.isNull(); + bool hasEnabledChannels = false; + + for (uint8_t i = 0; i < channelsCount && configComplete; i++) { + ChannelSettings* settingsPtr = &(channelSettings[i]); + JsonObject channel = top[settingsPtr->settingName]; + + configComplete &= !channel.isNull(); + + configComplete &= getJsonValue(channel[F("Enabled")], settingsPtr->isEnabled); + configComplete &= getJsonValue(channel[F("Name")], settingsPtr->name); + configComplete &= getJsonValue(channel[F("Units")], settingsPtr->units); + configComplete &= getJsonValue(channel[F("Multiplier")], settingsPtr->multiplier); + configComplete &= getJsonValue(channel[F("Offset")], settingsPtr->offset); + configComplete &= getJsonValue(channel[F("Decimals")], settingsPtr->decimals); + + hasEnabledChannels |= settingsPtr->isEnabled; + } + + configComplete &= getJsonValue(top[F("Loop Interval")], loopInterval); + + isEnabled = isInitialized && configComplete && hasEnabledChannels; + + return configComplete; + } + + uint16_t getId() + { + return USERMOD_ID_ADS1115; + } + + private: + static const uint8_t channelsCount = 8; + + ChannelSettings channelSettings[channelsCount] = { + { + "Differential reading from AIN0 (P) and AIN1 (N)", + false, + "Differential AIN0 AIN1", + "V", + ADS1X15_REG_CONFIG_MUX_DIFF_0_1, + 1, + 0, + 3 + }, + { + "Differential reading from AIN0 (P) and AIN3 (N)", + false, + "Differential AIN0 AIN3", + "V", + ADS1X15_REG_CONFIG_MUX_DIFF_0_3, + 1, + 0, + 3 + }, + { + "Differential reading from AIN1 (P) and AIN3 (N)", + false, + "Differential AIN1 AIN3", + "V", + ADS1X15_REG_CONFIG_MUX_DIFF_1_3, + 1, + 0, + 3 + }, + { + "Differential reading from AIN2 (P) and AIN3 (N)", + false, + "Differential AIN2 AIN3", + "V", + ADS1X15_REG_CONFIG_MUX_DIFF_2_3, + 1, + 0, + 3 + }, + { + "Single-ended reading from AIN0", + false, + "Single-ended AIN0", + "V", + ADS1X15_REG_CONFIG_MUX_SINGLE_0, + 1, + 0, + 3 + }, + { + "Single-ended reading from AIN1", + false, + "Single-ended AIN1", + "V", + ADS1X15_REG_CONFIG_MUX_SINGLE_1, + 1, + 0, + 3 + }, + { + "Single-ended reading from AIN2", + false, + "Single-ended AIN2", + "V", + ADS1X15_REG_CONFIG_MUX_SINGLE_2, + 1, + 0, + 3 + }, + { + "Single-ended reading from AIN3", + false, + "Single-ended AIN3", + "V", + ADS1X15_REG_CONFIG_MUX_SINGLE_3, + 1, + 0, + 3 + }, + }; + float readings[channelsCount] = {0, 0, 0, 0, 0, 0, 0, 0}; + + unsigned long loopInterval = 1000; + unsigned long lastTime = 0; + + Adafruit_ADS1115 ads; + uint8_t activeChannel; + + bool isEnabled = false; + bool isInitialized = false; + + static float round(float value, uint8_t decimals) { + return roundf(value * powf(10, decimals)) / powf(10, decimals); + } + + bool initChannel() { + for (uint8_t i = 0; i < channelsCount; i++) { + if (channelSettings[i].isEnabled) { + activeChannel = i; + return true; + } + } + + activeChannel = 0; + return false; + } + + void moveToNextChannel() { + uint8_t oldActiveChannel = activeChannel; + + do + { + if (++activeChannel >= channelsCount){ + activeChannel = 0; + } + } + while (!channelSettings[activeChannel].isEnabled && oldActiveChannel != activeChannel); + } + + void startReading() { + ads.startADCReading(channelSettings[activeChannel].mux, /*continuous=*/false); + } + + void updateResult() { + int16_t results = ads.getLastConversionResults(); + readings[activeChannel] = ads.computeVolts(results); + } +}; \ No newline at end of file diff --git a/usermods/Analog_Clock/Analog_Clock.h b/usermods/Analog_Clock/Analog_Clock.h new file mode 100644 index 0000000000..596f0acb3b --- /dev/null +++ b/usermods/Analog_Clock/Analog_Clock.h @@ -0,0 +1,256 @@ +#pragma once +#include "wled.h" + +/* + * Usermod for analog clock + */ +extern Timezone* tz; + +class AnalogClockUsermod : public Usermod { +private: + static constexpr uint32_t refreshRate = 50; // per second + static constexpr uint32_t refreshDelay = 1000 / refreshRate; + + struct Segment { + // config + int16_t firstLed = 0; + int16_t lastLed = 59; + int16_t centerLed = 0; + + // runtime + int16_t size; + + Segment() { + update(); + } + + void validateAndUpdate() { + if (firstLed < 0 || firstLed >= strip.getLengthTotal() || + lastLed < firstLed || lastLed >= strip.getLengthTotal()) { + *this = {}; + return; + } + if (centerLed < firstLed || centerLed > lastLed) { + centerLed = firstLed; + } + update(); + } + + void update() { + size = lastLed - firstLed + 1; + } + }; + + // configuration (available in API and stored in flash) + bool enabled = false; + Segment mainSegment; + bool hourMarksEnabled = true; + uint32_t hourMarkColor = 0xFF0000; + uint32_t hourColor = 0x0000FF; + uint32_t minuteColor = 0x00FF00; + bool secondsEnabled = true; + Segment secondsSegment; + uint32_t secondColor = 0xFF0000; + bool blendColors = true; + uint16_t secondsEffect = 0; + + // runtime + bool initDone = false; + uint32_t lastOverlayDraw = 0; + + void validateAndUpdate() { + mainSegment.validateAndUpdate(); + secondsSegment.validateAndUpdate(); + if (secondsEffect < 0 || secondsEffect > 1) { + secondsEffect = 0; + } + } + + int16_t adjustToSegment(double progress, Segment const& segment) { + int16_t led = segment.centerLed + progress * segment.size; + return led > segment.lastLed + ? segment.firstLed + led - segment.lastLed - 1 + : led; + } + + void setPixelColor(uint16_t n, uint32_t c) { + if (!blendColors) { + strip.setPixelColor(n, c); + } else { + uint32_t oldC = strip.getPixelColor(n); + strip.setPixelColor(n, qadd32(oldC, c)); + } + } + + String colorToHexString(uint32_t c) { + char buffer[9]; + sprintf(buffer, "%06X", c); + return buffer; + } + + bool hexStringToColor(String const& s, uint32_t& c, uint32_t def) { + char *ep; + unsigned long long r = strtoull(s.c_str(), &ep, 16); + if (*ep == 0) { + c = r; + return true; + } else { + c = def; + return false; + } + } + + void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) { + uint32_t ms = time.ms % 1000; + uint8_t b0 = (cos8(ms * 64 / 1000) - 128) * 2; + setPixelColor(secondLed, gamma32(scale32(secondColor, b0))); + uint8_t b1 = (sin8(ms * 64 / 1000) - 128) * 2; + setPixelColor(inc(secondLed, 1, secondsSegment), gamma32(scale32(secondColor, b1))); + } + + static inline uint32_t qadd32(uint32_t c1, uint32_t c2) { + return RGBW32( + qadd8(R(c1), R(c2)), + qadd8(G(c1), G(c2)), + qadd8(B(c1), B(c2)), + qadd8(W(c1), W(c2)) + ); + } + + static inline uint32_t scale32(uint32_t c, fract8 scale) { + return RGBW32( + scale8(R(c), scale), + scale8(G(c), scale), + scale8(B(c), scale), + scale8(W(c), scale) + ); + } + + static inline int16_t dec(int16_t n, int16_t i, Segment const& seg) { + return n - seg.firstLed >= i + ? n - i + : seg.lastLed - seg.firstLed - i + n + 1; + } + + static inline int16_t inc(int16_t n, int16_t i, Segment const& seg) { + int16_t r = n + i; + if (r > seg.lastLed) { + return seg.firstLed + n - seg.lastLed; + } + return r; + } + +public: + AnalogClockUsermod() { + } + + void setup() override { + initDone = true; + validateAndUpdate(); + } + + void loop() override { + if (millis() - lastOverlayDraw > refreshDelay) { + strip.trigger(); + } + } + + void handleOverlayDraw() override { + if (!enabled) { + return; + } + + lastOverlayDraw = millis(); + + auto time = toki.getTime(); + double secondP = second(localTime) / 60.0; + double minuteP = minute(localTime) / 60.0; + double hourP = (hour(localTime) % 12) / 12.0 + minuteP / 12.0; + + if (hourMarksEnabled) { + for (int Led = 0; Led <= 55; Led = Led + 5) + { + int16_t hourmarkled = adjustToSegment(Led / 60.0, mainSegment); + setPixelColor(hourmarkled, hourMarkColor); + } + } + + if (secondsEnabled) { + int16_t secondLed = adjustToSegment(secondP, secondsSegment); + + switch (secondsEffect) { + case 0: // no effect + setPixelColor(secondLed, secondColor); + break; + + case 1: // fading seconds + secondsEffectSineFade(secondLed, time); + break; + } + + // TODO: move to secondsTrailEffect + // for (uint16_t i = 1; i < secondsTrail + 1; ++i) { + // uint16_t trailLed = dec(secondLed, i, secondsSegment); + // uint8_t trailBright = 255 / (secondsTrail + 1) * (secondsTrail - i + 1); + // setPixelColor(trailLed, gamma32(scale32(secondColor, trailBright))); + // } + } + + setPixelColor(adjustToSegment(minuteP, mainSegment), minuteColor); + setPixelColor(adjustToSegment(hourP, mainSegment), hourColor); + } + + void addToConfig(JsonObject& root) override { + validateAndUpdate(); + + JsonObject top = root.createNestedObject(F("Analog Clock")); + top[F("Overlay Enabled")] = enabled; + top[F("First LED (Main Ring)")] = mainSegment.firstLed; + top[F("Last LED (Main Ring)")] = mainSegment.lastLed; + top[F("Center/12h LED (Main Ring)")] = mainSegment.centerLed; + top[F("Hour Marks Enabled")] = hourMarksEnabled; + top[F("Hour Mark Color (RRGGBB)")] = colorToHexString(hourMarkColor); + top[F("Hour Color (RRGGBB)")] = colorToHexString(hourColor); + top[F("Minute Color (RRGGBB)")] = colorToHexString(minuteColor); + top[F("Show Seconds")] = secondsEnabled; + top[F("First LED (Seconds Ring)")] = secondsSegment.firstLed; + top[F("Last LED (Seconds Ring)")] = secondsSegment.lastLed; + top[F("Center/12h LED (Seconds Ring)")] = secondsSegment.centerLed; + top[F("Second Color (RRGGBB)")] = colorToHexString(secondColor); + top[F("Seconds Effect (0-1)")] = secondsEffect; + top[F("Blend Colors")] = blendColors; + } + + bool readFromConfig(JsonObject& root) override { + JsonObject top = root[F("Analog Clock")]; + + bool configComplete = !top.isNull(); + + String color; + configComplete &= getJsonValue(top[F("Overlay Enabled")], enabled, false); + configComplete &= getJsonValue(top[F("First LED (Main Ring)")], mainSegment.firstLed, 0); + configComplete &= getJsonValue(top[F("Last LED (Main Ring)")], mainSegment.lastLed, 59); + configComplete &= getJsonValue(top[F("Center/12h LED (Main Ring)")], mainSegment.centerLed, 0); + configComplete &= getJsonValue(top[F("Hour Marks Enabled")], hourMarksEnabled, false); + configComplete &= getJsonValue(top[F("Hour Mark Color (RRGGBB)")], color, F("161616")) && hexStringToColor(color, hourMarkColor, 0x161616); + configComplete &= getJsonValue(top[F("Hour Color (RRGGBB)")], color, F("0000FF")) && hexStringToColor(color, hourColor, 0x0000FF); + configComplete &= getJsonValue(top[F("Minute Color (RRGGBB)")], color, F("00FF00")) && hexStringToColor(color, minuteColor, 0x00FF00); + configComplete &= getJsonValue(top[F("Show Seconds")], secondsEnabled, true); + configComplete &= getJsonValue(top[F("First LED (Seconds Ring)")], secondsSegment.firstLed, 0); + configComplete &= getJsonValue(top[F("Last LED (Seconds Ring)")], secondsSegment.lastLed, 59); + configComplete &= getJsonValue(top[F("Center/12h LED (Seconds Ring)")], secondsSegment.centerLed, 0); + configComplete &= getJsonValue(top[F("Second Color (RRGGBB)")], color, F("FF0000")) && hexStringToColor(color, secondColor, 0xFF0000); + configComplete &= getJsonValue(top[F("Seconds Effect (0-1)")], secondsEffect, 0); + configComplete &= getJsonValue(top[F("Blend Colors")], blendColors, true); + + if (initDone) { + validateAndUpdate(); + } + + return configComplete; + } + + uint16_t getId() override { + return USERMOD_ID_ANALOG_CLOCK; + } +}; diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index 35ea0d7a4a..8953756d35 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -25,6 +25,7 @@ class Animated_Staircase : public Usermod { bool useUSSensorBottom = false; // using PIR or UltraSound sensor? unsigned int topMaxDist = 50; // default maximum measured distance in cm, top unsigned int bottomMaxDist = 50; // default maximum measured distance in cm, bottom + bool togglePower = false; // toggle power on/off with staircase on/off /* runtime variables */ bool initDone = false; @@ -65,7 +66,7 @@ class Animated_Staircase : public Usermod { // The maximum number of configured segments. // Dynamically updated based on user configuration. byte maxSegmentId = 1; - byte mainSegmentId = 0; + byte minSegmentId = 0; // These values are used by the API to read the // last sensor state, or trigger a sensor @@ -90,39 +91,37 @@ class Animated_Staircase : public Usermod { static const char _bottomEcho_pin[]; static const char _topEchoCm[]; static const char _bottomEchoCm[]; - - void publishMqtt(bool bottom, const char* state) - { + static const char _togglePower[]; + + void publishMqtt(bool bottom, const char* state) { +#ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (WLED_MQTT_CONNECTED){ char subuf[64]; sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)bottom); mqtt->publish(subuf, 0, false, state); } +#endif } void updateSegments() { - mainSegmentId = strip.getMainSegmentId(); - WS2812FX::Segment* segments = strip.getSegments(); - for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { - if (!segments->isActive()) { - maxSegmentId = i - 1; - break; - } - + for (int i = minSegmentId; i < maxSegmentId; i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; // skip gaps if (i >= onIndex && i < offIndex) { - segments->setOption(SEG_OPTION_ON, 1, i); - + seg.setOption(SEG_OPTION_ON, true); // We may need to copy mode and colors from segment 0 to make sure // changes are propagated even when the config is changed during a wipe - // segments->mode = mainsegment.mode; - // segments->colors[0] = mainsegment.colors[0]; + // seg.setMode(mainsegment.mode); + // seg.setColor(0, mainsegment.colors[0]); } else { - segments->setOption(SEG_OPTION_ON, 0, i); + seg.setOption(SEG_OPTION_ON, false); } // Always mark segments as "transitional", we are animating the staircase - segments->setOption(SEG_OPTION_TRANSITIONAL, 1, i); + //seg.setOption(SEG_OPTION_TRANSITIONAL, true); // not needed anymore as setOption() does it } + strip.trigger(); // force strip refresh + stateChanged = true; // inform external devices/UI of change colorUpdated(CALL_MODE_DIRECT_CHANGE); } @@ -134,7 +133,7 @@ class Animated_Staircase : public Usermod { * received within this time, an object is detected * and the function will return true. * - * The speed of sound is 343 meters per second at 20 degress Celcius. + * The speed of sound is 343 meters per second at 20 degrees Celsius. * Since the sound has to travel back and forth, the detection * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2. * @@ -199,6 +198,7 @@ class Animated_Staircase : public Usermod { if (on) { lastSensor = topSensorRead; } else { + if (togglePower && onIndex == offIndex && offMode) toggleOnOff(); // toggle power on if off // If the bottom sensor triggered, we need to swipe up, ON swipe = bottomSensorRead; @@ -208,9 +208,9 @@ class Animated_Staircase : public Usermod { if (onIndex == offIndex) { // Position the indices for a correct on-swipe if (swipe == SWIPE_UP) { - onIndex = mainSegmentId; + onIndex = minSegmentId; } else { - onIndex = maxSegmentId+1; + onIndex = maxSegmentId; } offIndex = onIndex; } @@ -222,7 +222,7 @@ class Animated_Staircase : public Usermod { } void autoPowerOff() { - if (on && ((millis() - lastSwitchTime) > on_time_ms)) { + if ((millis() - lastSwitchTime) > on_time_ms) { // if sensors are still on, do nothing if (bottomSensorState || topSensorState) return; @@ -239,10 +239,12 @@ class Animated_Staircase : public Usermod { if ((millis() - lastTime) > segment_delay_ms) { lastTime = millis(); + byte oldOn = onIndex; + byte oldOff = offIndex; if (on) { // Turn on all segments - onIndex = MAX(mainSegmentId, onIndex - 1); - offIndex = MIN(maxSegmentId + 1, offIndex + 1); + onIndex = MAX(minSegmentId, onIndex - 1); + offIndex = MIN(maxSegmentId, offIndex + 1); } else { if (swipe == SWIPE_UP) { onIndex = MIN(offIndex, onIndex + 1); @@ -250,11 +252,14 @@ class Animated_Staircase : public Usermod { offIndex = MAX(onIndex, offIndex - 1); } } - updateSegments(); + if (oldOn != onIndex || oldOff != offIndex) { + updateSegments(); // reduce the number of updates to necessary ones + if (togglePower && onIndex == offIndex && !offMode && !on) toggleOnOff(); // toggle power off for all segments off + } } } - // send sesnor values to JSON API + // send sensor values to JSON API void writeSensorsToJson(JsonObject& staircase) { staircase[F("top-sensor")] = topSensorRead; staircase[F("bottom-sensor")] = bottomSensorRead; @@ -288,16 +293,23 @@ class Animated_Staircase : public Usermod { pinMode(topPIRorTriggerPin, OUTPUT); pinMode(topEchoPin, INPUT); } + onIndex = minSegmentId = strip.getMainSegmentId(); // it may not be the best idea to start with main segment as it may not be the first one + offIndex = maxSegmentId = strip.getLastActiveSegmentId() + 1; + + // shorten the strip transition time to be equal or shorter than segment delay + transitionDelay = segment_delay_ms; + strip.setTransition(segment_delay_ms); + strip.trigger(); } else { + if (togglePower && !on && offMode) toggleOnOff(); // toggle power on if off // Restore segment options - WS2812FX::Segment* segments = strip.getSegments(); - for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { - if (!segments->isActive()) { - maxSegmentId = i - 1; - break; - } - segments->setOption(SEG_OPTION_ON, 1, i); + for (int i = 0; i <= strip.getLastActiveSegmentId(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; // skip vector gaps + seg.setOption(SEG_OPTION_ON, true); } + strip.trigger(); // force strip update + stateChanged = true; // inform external devices/UI of change colorUpdated(CALL_MODE_DIRECT_CHANGE); DEBUG_PRINTLN(F("Animated Staircase disabled.")); } @@ -333,13 +345,16 @@ class Animated_Staircase : public Usermod { void loop() { if (!enabled || strip.isUpdating()) return; + minSegmentId = strip.getMainSegmentId(); // it may not be the best idea to start with main segment as it may not be the first one + maxSegmentId = strip.getLastActiveSegmentId() + 1; checkSensors(); - autoPowerOff(); + if (on) autoPowerOff(); updateSwipe(); } uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; } +#ifndef WLED_DISABLE_MQTT /** * handling of MQTT message * topic only contains stripped topic (part after /wled/MAC) @@ -377,6 +392,7 @@ class Animated_Staircase : public Usermod { mqtt->subscribe(subuf, 0); } } +#endif void addToJsonState(JsonObject& root) { JsonObject staircase = root[FPSTR(_name)]; @@ -393,19 +409,29 @@ class Animated_Staircase : public Usermod { */ void readFromJsonState(JsonObject& root) { if (!initDone) return; // prevent crash on boot applyPreset() + bool en = enabled; JsonObject staircase = root[FPSTR(_name)]; if (!staircase.isNull()) { if (staircase[FPSTR(_enabled)].is()) { - enabled = staircase[FPSTR(_enabled)].as(); + en = staircase[FPSTR(_enabled)].as(); } else { String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on - enabled = (bool)(str!="off"); // off is guaranteed to be present + en = (bool)(str!="off"); // off is guaranteed to be present } + if (en != enabled) enable(en); readSensorsFromJson(staircase); DEBUG_PRINTLN(F("Staircase sensor state read from API.")); } } + void appendConfigData() { + //oappend(SET_F("dd=addDropdown('staircase','selectfield');")); + //oappend(SET_F("addOption(dd,'1st value',0);")); + //oappend(SET_F("addOption(dd,'2nd value',1);")); + //oappend(SET_F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field + } + + /* * Writes the configuration to internal flash memory. */ @@ -425,6 +451,7 @@ class Animated_Staircase : public Usermod { staircase[FPSTR(_bottomEcho_pin)] = useUSSensorBottom ? bottomEchoPin : -1; staircase[FPSTR(_topEchoCm)] = topMaxDist; staircase[FPSTR(_bottomEchoCm)] = bottomMaxDist; + staircase[FPSTR(_togglePower)] = togglePower; DEBUG_PRINTLN(F("Staircase config saved.")); } @@ -465,10 +492,12 @@ class Animated_Staircase : public Usermod { bottomEchoPin = top[FPSTR(_bottomEcho_pin)] | bottomEchoPin; topMaxDist = top[FPSTR(_topEchoCm)] | topMaxDist; - topMaxDist = min(150,max(30,(int)topMaxDist)); // max distnace ~1.5m (a lag of 9ms may be expected) + topMaxDist = min(150,max(30,(int)topMaxDist)); // max distance ~1.5m (a lag of 9ms may be expected) bottomMaxDist = top[FPSTR(_bottomEchoCm)] | bottomMaxDist; bottomMaxDist = min(150,max(30,(int)bottomMaxDist)); // max distance ~1.5m (a lag of 9ms may be expected) + togglePower = top[FPSTR(_togglePower)] | togglePower; // staircase toggles power on/off + DEBUG_PRINT(FPSTR(_name)); if (!initDone) { // first run: reading from cfg.json @@ -492,7 +521,7 @@ class Animated_Staircase : public Usermod { if (changed) setup(); } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return true; + return !top[FPSTR(_togglePower)].isNull(); } /* @@ -500,22 +529,22 @@ class Animated_Staircase : public Usermod { * tab of the web-UI. */ void addToJsonInfo(JsonObject& root) { - JsonObject staircase = root["u"]; - if (staircase.isNull()) { - staircase = root.createNestedObject("u"); + JsonObject user = root["u"]; + if (user.isNull()) { + user = root.createNestedObject("u"); } - JsonArray usermodEnabled = staircase.createNestedArray(F("Staircase")); // name - String btn = F(""); - usermodEnabled.add(btn); // value + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); // name + + String uiDomString = F(""); + infoArr.add(uiDomString); } }; @@ -532,3 +561,4 @@ const char Animated_Staircase::_bottomPIRorTrigger_pin[] PROGMEM = "bottomPIR const char Animated_Staircase::_bottomEcho_pin[] PROGMEM = "bottomEcho_pin"; const char Animated_Staircase::_topEchoCm[] PROGMEM = "top-dist-cm"; const char Animated_Staircase::_bottomEchoCm[] PROGMEM = "bottom-dist-cm"; +const char Animated_Staircase::_togglePower[] PROGMEM = "toggle-on-off"; diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md index 2641e67632..320b744a55 100644 --- a/usermods/Animated_Staircase/README.md +++ b/usermods/Animated_Staircase/README.md @@ -1,17 +1,17 @@ # Usermod Animated Staircase -This usermod makes your staircase look cool by switching it on with an animation. It uses +This usermod makes your staircase look cool by illuminating it with an animation. It uses PIR or ultrasonic sensors at the top and bottom of your stairs to: -- Light up the steps in your walking direction, leading the way. +- Light up the steps in the direction you're walking. - Switch off the steps after you, in the direction of the last detected movement. - Always switch on when one of the sensors detects movement, even if an effect - is still running. It can therewith handle multiple people on the stairs gracefully. + is still running. It can gracefully handle multiple people on the stairs. The Animated Staircase can be controlled by the WLED API. Change settings such as -speed, on/off time and distance settings by sending an HTTP request, see below. +speed, on/off time and distance by sending an HTTP request, see below. ## WLED integration -To include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://github.com/Aircoookie/WLED/wiki/Compiling-WLED). +To include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://kno.wled.ge/advanced/compiling-wled/). Before compiling, you have to make the following modifications: @@ -20,17 +20,16 @@ Edit `usermods_list.cpp`: 2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file 3. add `usermods.add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. -You can configure usermod using Usermods settings page. -Please enter GPIO pins for PIR sensors or ultrasonic sensor (trigger and echo). +You can configure usermod using the Usermods settings page. +Please enter GPIO pins for PIR or ultrasonic sensors (trigger and echo). If you use PIR sensor enter -1 for echo pin. -Maximum distance for ultrasonic sensor can be configured as a time needed for echo (see below). +Maximum distance for ultrasonic sensor can be configured as the time needed for an echo (see below). ## Hardware installation -1. Stick the LED strip under each step of the stairs. -2. Connect the ESP8266 pin D4 or ESP32 pin D2 to the first LED data pin at the bottom step - of your stairs. +1. Attach the LED strip to each step of the stairs. +2. Connect the ESP8266 pin D4 or ESP32 pin D2 to the first LED data pin at the bottom step. 3. Connect the data-out pin at the end of each strip per step to the data-in pin on the - other end of the next step, creating one large virtual LED strip. + next step, creating one large virtual LED strip. 4. Mount sensors of choice at the bottom and top of the stairs and connect them to the ESP. 5. To make sure all LEDs get enough power and have your staircase lighted evenly, power each step from one side, using at least AWG14 or 2.5mm^2 cable. Don't connect them serial as you @@ -39,7 +38,7 @@ Maximum distance for ultrasonic sensor can be configured as a time needed for ec You _may_ need to use 10k pull-down resistors on the selected PIR pins, depending on the sensor. ## WLED configuration -1. In the WLED UI, confgure a segment for each step. The lowest step of the stairs is the +1. In the WLED UI, configure a segment for each step. The lowest step of the stairs is the lowest segment id. 2. Save your segments into a preset. 3. Ideally, add the preset in the config > LED setup menu to the "apply @@ -62,7 +61,7 @@ or remove them and put everything on one line. To read the current settings, open a browser to `http://xxx.xxx.xxx.xxx/json/state` (use your WLED device IP address). The device will respond with a json object containing all WLED settings. -The staircase settings and sensor states are inside the WLED status element: +The staircase settings and sensor states are inside the WLED "state" element: ```json { @@ -70,14 +69,14 @@ The staircase settings and sensor states are inside the WLED status element: "staircase": { "enabled": true, "bottom-sensor": false, - "tops-ensor": false + "top-sensor": false }, } ``` ### Enable/disable the usermod By disabling the usermod you will be able to keep the LED's on, independent from the sensor -activity. This enables to play with the lights without the usermod switching them on or off. +activity. This enables you to play with the lights without the usermod switching them on or off. To disable the usermod: @@ -92,17 +91,17 @@ To enable the usermod again, use `"enabled":true`. Alternatively you can use _Usermod_ Settings page where you can change other parameters as well. ### Changing animation parameters and detection range of the ultrasonic HC-SR04 sensor -Using _Usermod_ Settings page you can define different usermod parameters, includng sensor pins, delay between segment activation and so on. +Using _Usermod_ Settings page you can define different usermod parameters, including sensor pins, delay between segment activation etc. When an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors. -**Please note:** that using an HC-SR04 sensor, particularly when detecting echos at longer -distances creates delays in the WLED software, and _might_ introduce timing hickups in your animations or +**Please note:** using an HC-SR04 sensor, particularly when detecting echos at longer +distances creates delays in the WLED software, _might_ introduce timing hiccups in your animation or a less responsive web interface. It is therefore advised to keep the detection distance as short as possible. ### Animation triggering through the API -Instead of stairs activation by one of the sensors, you can also trigger the animation through -the API. To simulate triggering the bottom sensor, use: +In addition to activation by one of the stair sensors, you can also trigger the animation manually +via the API. To simulate triggering the bottom sensor, use: ```bash curl -X POST -H "Content-Type: application/json" \ @@ -110,7 +109,7 @@ curl -X POST -H "Content-Type: application/json" \ xxx.xxx.xxx.xxx/json/state ``` -Likewise, to trigger the top sensor, use: +Likewise, to trigger the top sensor: ```bash curl -X POST -H "Content-Type: application/json" \ @@ -119,7 +118,7 @@ curl -X POST -H "Content-Type: application/json" \ ``` **MQTT** You can publish a message with either `up` or `down` on topic `/swipe` to trigger animation. -You can also use `on` or `off` for enabling or disabling usermod. +You can also use `on` or `off` for enabling or disabling the usermod. Have fun with this usermod.
www.rolfje.com @@ -128,4 +127,4 @@ Modifications @blazoncek ## Change log 2021-04 -* Adaptation for runtime configuration. \ No newline at end of file +* Adaptation for runtime configuration. diff --git a/usermods/BH1750_v2/platformio_override.ini b/usermods/BH1750_v2/platformio_override.ini deleted file mode 100644 index 7b981e50a1..0000000000 --- a/usermods/BH1750_v2/platformio_override.ini +++ /dev/null @@ -1,16 +0,0 @@ -; Options -; ------- -; USERMOD_BH1750 - define this to have this user mod included wled00\usermods_list.cpp -; USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - the max number of milliseconds between measurements, defaults to 10000ms -; USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL - the min number of milliseconds between measurements, defaults to 500ms -; USERMOD_BH1750_FIRST_MEASUREMENT_AT - the number of milliseconds after boot to take first measurement, defaults to 10 seconds -; USERMOD_BH1750_OFFSET_VALUE - the offset value to report on, defaults to 1 -; -[env:usermod_BH1750_d1_mini] -extends = env:d1_mini -build_flags = - ${common.build_flags_esp8266} - -D USERMOD_BH1750 -lib_deps = - ${env.lib_deps} - claws/BH1750 @ ^1.2.0 diff --git a/usermods/BH1750_v2/readme.md b/usermods/BH1750_v2/readme.md index a350777009..6e6c693d45 100644 --- a/usermods/BH1750_v2/readme.md +++ b/usermods/BH1750_v2/readme.md @@ -1,24 +1,49 @@ # BH1750 usermod -This usermod will read from an ambient light sensor like the BH1750 sensor. -The luminance is displayed both in the Info section of the web UI as well as published to the `/luminance` MQTT topic if enabled. - -## Installation - -Copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. - -### Define Your Options - -* `USERMOD_BH1750` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL` - the max number of milliseconds between measurements, defaults to 10000ms -* `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL` - the min number of milliseconds between measurements, defaults to 500ms -* `USERMOD_BH1750_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 10 seconds -* `USERMOD_BH1750_OFFSET_VALUE` - the offset value to report on, defaults to 1 - -All parameters can be configured at runtime using Usermods settings page. - -### PlatformIO requirements - -If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:usermod_BH1750_d1_mini`. +This usermod will read from an ambient light sensor like the BH1750. +The luminance is displayed in both the Info section of the web UI, as well as published to the `/luminance` MQTT topic if enabled. + +## Dependencies +- Libraries + - `claws/BH1750 @^1.2.0` + - This must be added under `lib_deps` in your `platformio.ini` (or `platformio_override.ini`). +- Data is published over MQTT - make sure you've enabled the MQTT sync interface. + +## Compilation + +To enable, compile with `USERMOD_BH1750` defined (e.g. in `platformio_override.ini`) +```ini +[env:usermod_BH1750_d1_mini] +extends = env:d1_mini +build_flags = + ${common.build_flags_esp8266} + -D USERMOD_BH1750 +lib_deps = + ${esp8266.lib_deps} + claws/BH1750 @ ^1.2.0 +``` + +### Configuration Options +The following settings can be set at compile-time but are configurable on the usermod menu (except First Measurement time): +* `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL` - the max number of milliseconds between measurements, defaults to 10000ms +* `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL` - the min number of milliseconds between measurements, defaults to 500ms +* `USERMOD_BH1750_OFFSET_VALUE` - the offset value to report on, defaults to 1 +* `USERMOD_BH1750_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 10000 ms + +In addition, the Usermod screen allows you to: +- enable/disable the usermod +- Enable Home Assistant Discovery of usermod +- Configure the SCL/SDA pins + +## API +The following method is available to interact with the usermod from other code modules: +- `getIlluminance` read the brightness from the sensor ## Change Log +Jul 2022 +- Added Home Assistant Discovery +- Implemented PinManager to register pins +- Made pins configurable in usermod menu +- Added API call to read luminance from other modules +- Enhanced info-screen outputs +- Updated `readme.md` diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h index fb0b1c5af0..d758005d6f 100644 --- a/usermods/BH1750_v2/usermod_bh1750.h +++ b/usermods/BH1750_v2/usermod_bh1750.h @@ -1,7 +1,13 @@ +// force the compiler to show a warning to confirm that this file is included +#warning **** Included USERMOD_BH1750 **** + +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + #pragma once #include "wled.h" -#include #include // the max frequency to check photoresistor, 10 seconds @@ -19,7 +25,7 @@ #define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 #endif -// only report if differance grater than offset value +// only report if difference grater than offset value #ifndef USERMOD_BH1750_OFFSET_VALUE #define USERMOD_BH1750_OFFSET_VALUE 1 #endif @@ -39,7 +45,7 @@ class Usermod_BH1750 : public Usermod bool getLuminanceComplete = false; // flag set at startup - bool disabled = false; + bool enabled = true; // strings to reduce flash memory usage (used more than twice) static const char _name[]; @@ -47,6 +53,15 @@ class Usermod_BH1750 : public Usermod static const char _maxReadInterval[]; static const char _minReadInterval[]; static const char _offset[]; + static const char _HomeAssistantDiscovery[]; + + bool initDone = false; + bool sensorFound = false; + + // Home Assistant and MQTT + String mqttLuminanceTopic = F(""); + bool mqttInitialized = false; + bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages BH1750 lightMeter; float lastLux = -1000; @@ -55,17 +70,57 @@ class Usermod_BH1750 : public Usermod { return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0); } + + // set up Home Assistant discovery entries + void _mqttInitialize() + { + mqttLuminanceTopic = String(mqttDeviceTopic) + F("/brightness"); + + if (HomeAssistantDiscovery) _createMqttSensor(F("Brightness"), mqttLuminanceTopic, F("Illuminance"), F(" lx")); + } + + // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. + void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) + { + String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = String(serverDescription) + F(" ") + name; + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + if (deviceClass != "") + doc[F("device_class")] = deviceClass; + doc[F("expire_after")] = 1800; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F("WLED"); + device[F("model")] = F("FOSS"); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } public: void setup() { - Wire.begin(); - lightMeter.begin(); + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } + sensorFound = lightMeter.begin(); + initDone = true; } void loop() { - if (disabled || strip.isUpdating()) + if ((!enabled) || strip.isUpdating()) return; unsigned long now = millis(); @@ -88,20 +143,29 @@ class Usermod_BH1750 : public Usermod { lastLux = lux; lastSend = millis(); +#ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { - char subuf[45]; - strcpy(subuf, mqttDeviceTopic); - strcat_P(subuf, PSTR("/luminance")); - mqtt->publish(subuf, 0, true, String(lux).c_str()); + if (!mqttInitialized) + { + _mqttInitialize(); + mqttInitialized = true; + } + mqtt->publish(mqttLuminanceTopic.c_str(), 0, true, String(lux).c_str()); + DEBUG_PRINTLN(F("Brightness: ") + String(lux) + F("lx")); } else { - DEBUG_PRINTLN("Missing MQTT connection. Not publishing data"); + DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); } +#endif } } + inline float getIlluminance() { + return (float)lastLux; + } + void addToJsonInfo(JsonObject &root) { JsonObject user = root[F("u")]; @@ -109,43 +173,39 @@ class Usermod_BH1750 : public Usermod user = root.createNestedObject(F("u")); JsonArray lux_json = user.createNestedArray(F("Luminance")); - - if (!getLuminanceComplete) - { + if (!enabled) { + lux_json.add(F("disabled")); + } else if (!sensorFound) { + // if no sensor + lux_json.add(F("BH1750 ")); + lux_json.add(F("Not Found")); + } else if (!getLuminanceComplete) { // if we haven't read the sensor yet, let the user know - // that we are still waiting for the first measurement - lux_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000); - lux_json.add(F(" sec until read")); - return; + // that we are still waiting for the first measurement + lux_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000); + lux_json.add(F(" sec until read")); + return; + } else { + lux_json.add(lastLux); + lux_json.add(F(" lx")); } - - lux_json.add(lastLux); - lux_json.add(F(" lx")); - } - - uint16_t getId() - { - return USERMOD_ID_BH1750; } - /** - * addToConfig() (called from set.cpp) stores persistent properties to cfg.json - */ + // (called from set.cpp) stores persistent properties to cfg.json void addToConfig(JsonObject &root) { // we add JSON object. JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = !disabled; + top[FPSTR(_enabled)] = enabled; top[FPSTR(_maxReadInterval)] = maxReadingInterval; top[FPSTR(_minReadInterval)] = minReadingInterval; + top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; top[FPSTR(_offset)] = offset; - DEBUG_PRINTLN(F("Photoresistor config saved.")); + DEBUG_PRINTLN(F("BH1750 config saved.")); } - /** - * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - */ + // called before setup() to populate properties from values stored in cfg.json bool readFromConfig(JsonObject &root) { // we look for JSON object. @@ -153,20 +213,34 @@ class Usermod_BH1750 : public Usermod if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINT(F("BH1750")); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); + configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, 10000); //ms + configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, 500); //ms + configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); + configComplete &= getJsonValue(top[FPSTR(_offset)], offset, 1); - disabled = !(top[FPSTR(_enabled)] | !disabled); - maxReadingInterval = (top[FPSTR(_maxReadInterval)] | maxReadingInterval); // ms - minReadingInterval = (top[FPSTR(_minReadInterval)] | minReadingInterval); // ms - offset = top[FPSTR(_offset)] | offset; DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(" config (re)loaded.")); + if (!initDone) { + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return true; + return configComplete; + } + + uint16_t getId() + { + return USERMOD_ID_BH1750; + } + }; // strings to reduce flash memory usage (used more than twice) @@ -174,4 +248,5 @@ const char Usermod_BH1750::_name[] PROGMEM = "BH1750"; const char Usermod_BH1750::_enabled[] PROGMEM = "enabled"; const char Usermod_BH1750::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; const char Usermod_BH1750::_minReadInterval[] PROGMEM = "min-read-interval-ms"; +const char Usermod_BH1750::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscoveryLux"; const char Usermod_BH1750::_offset[] PROGMEM = "offset-lx"; diff --git a/usermods/BH1750_v2/usermods_list.cpp b/usermods/BH1750_v2/usermods_list.cpp deleted file mode 100644 index 6e914df596..0000000000 --- a/usermods/BH1750_v2/usermods_list.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "wled.h" -/* - * Register your v2 usermods here! - */ -#ifdef USERMOD_BH1750 -#include "../usermods/BH1750_v2/usermod_BH1750.h" -#endif - -void registerUsermods() -{ -#ifdef USERMOD_BH1750 - usermods.add(new Usermod_BH1750()); -#endif -} diff --git a/usermods/BME280_v2/README.md b/usermods/BME280_v2/README.md index 216ca63000..0a4afbf1f4 100644 --- a/usermods/BME280_v2/README.md +++ b/usermods/BME280_v2/README.md @@ -1,40 +1,90 @@ -Hello! I have written a v2 usermod for the BME280/BMP280 sensor based on the [existing v1 usermod](https://github.com/Aircoookie/WLED/blob/master/usermods/Wemos_D1_mini%2BWemos32_mini_shield/usermod_bme280.cpp). It is not just a refactor, there are many changes which I made to fit my use case, and I hope they will fit the use cases of others as well! Most notably, this usermod is *just* for the BME280 and does not control a display like in the v1 usermod designed for the WeMos shield. +# Usermod BME280 +This Usermod is designed to read a `BME280` or `BMP280` sensor and output the following: +- Temperature +- Humidity (`BME280` only) +- Pressure +- Heat Index (`BME280` only) +- Dew Point (`BME280` only) -- Requires libraries `BME280@~3.0.0` (by [finitespace](https://github.com/finitespace/BME280)) and `Wire`. Please add these under `lib_deps` in your `platform.ini` (or `platform_override.ini`). -- Data is published over MQTT so make sure you've enabled the MQTT sync interface. -- This usermod also writes to serial (GPIO1 on ESP8266). Please make sure nothing else listening on the serial TX pin of your board will get confused by log messages! +Configuration is performed via the Usermod menu. There are no parameters to set in code! The following settings can be configured in the Usermod Menu: +- Temperature Decimals (number of decimal places to output) +- Humidity Decimals +- Pressure Decimals +- Temperature Interval (how many seconds between temperature and humidity measurements) +- Pressure Interval +- Publish Always (turn off to only publish changes, on to publish whether or not value changed) +- Use Celsius (turn off to use Fahrenheit) +- Home Assistant Discovery (turn on to sent MQTT Discovery entries for Home Assistant) +- SCL/SDA GPIO Pins -To enable, compile with `USERMOD_BME280` defined (i.e. `platformio_override.ini`) +Dependencies +- Libraries + - `BME280@~3.0.0` (by [finitespace](https://github.com/finitespace/BME280)) + - `Wire` + - These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). +- Data is published over MQTT - make sure you've enabled the MQTT sync interface. +- This usermod also writes to serial (GPIO1 on ESP8266). Please make sure nothing else is listening to the serial TX pin or your board will get confused by log messages! + +In addition to outputting via MQTT, you can read the values from the Info Screen on the dashboard page of the device's web interface. + +Methods also exist to read the read/calculated values from other WLED modules through code. +- `getTemperatureC()` +- `getTemperatureF()` +- `getHumidity()` +- `getPressure()` +- `getDewPointC()` +- `getDewPointF()` +- `getHeatIndexC()` +- `getHeatIndexF()` + +# Compiling + +To enable, compile with `USERMOD_BME280` defined (e.g. in `platformio_override.ini`) ```ini +[env:usermod_bme280_d1_mini] +extends = env:d1_mini build_flags = ${common.build_flags_esp8266} -D USERMOD_BME280 +lib_deps = + ${esp8266.lib_deps} + BME280@~3.0.0 + Wire ``` -or define `USERMOD_BME280` in `my_config.h` -```c++ -#define USERMOD_BME280 -``` - -Changes include: -- Adjustable measure intervals - - Temperature and pressure have separate intervals due to pressure not frequently changing at any constant altitude -- Adjustment of number of decimal places in published sensor values - - Separate adjustment for temperature, humidity and pressure values - - Values are rounded to the specified number of decimal places -- Pressure measured in units of hPa instead of Pa -- Calculation of heat index (apparent temperature) and dew point - - These, along with humidity measurements, are disabled if the sensor is a BMP280 -- 16x oversampling of sensor during measurement -- Values are only published if they are different from the previous value -- Values are published on startup (continually until the MQTT broker acknowledges a successful publication) -Adjustments are made through preprocessor definitions at the start of the class definition. -MQTT topics are as follows: +# MQTT +MQTT topics are as follows (`` is set in MQTT section of Sync Setup menu): Measurement type | MQTT topic --- | --- Temperature | `/temperature` Humidity | `/humidity` Pressure | `/pressure` Heat index | `/heat_index` -Dew point | `/dew_point` \ No newline at end of file +Dew point | `/dew_point` + +If you are using Home Assistant, and `Home Assistant Discovery` is turned on, Home Assistant should automatically detect a new device, provided you have the MQTT integration installed. The device is separate from the main WLED device and will contain sensors for Pressure, Humidity, Temperature, Dew Point and Heat Index. + +# Revision History +Jul 2022 +- Added Home Assistant Discovery +- Added API interface to output data +- Removed compile-time variables +- Added usermod menu interface +- Added value outputs to info screen +- Updated `readme.md` +- Registered usermod +- Implemented PinManager for usermod +- Implemented reallocation of pins without reboot + +Apr 2021 +- Added `Publish Always` option + +Dec 2020 +- Ported to V2 Usermod format +- Customizable `measure intervals` +- Customizable number of `decimal places` in published sensor values +- Pressure measured in units of hPa instead of Pa +- Calculation of heat index (apparent temperature) and dew point +- `16x oversampling` of sensor during measurement +- Values only published if they are different from the previous value diff --git a/usermods/BME280_v2/usermod_bme280.h b/usermods/BME280_v2/usermod_bme280.h index 82eb08871e..73d3c3dfcc 100644 --- a/usermods/BME280_v2/usermod_bme280.h +++ b/usermods/BME280_v2/usermod_bme280.h @@ -1,51 +1,39 @@ +// force the compiler to show a warning to confirm that this file is included +#warning **** Included USERMOD_BME280 version 2.0 **** + +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + #pragma once #include "wled.h" #include -#include #include // BME280 sensor #include // BME280 extended measurements class UsermodBME280 : public Usermod { private: -// User-defined configuration -#define Celsius // Show temperature mesaurement in Celcius. Comment out for Fahrenheit -#define TemperatureDecimals 1 // Number of decimal places in published temperaure values -#define HumidityDecimals 2 // Number of decimal places in published humidity values -#define PressureDecimals 2 // Number of decimal places in published pressure values -#define TemperatureInterval 5 // Interval to measure temperature (and humidity, dew point if available) in seconds -#define PressureInterval 300 // Interval to measure pressure in seconds -#define PublishAlways 0 // Publish values even when they have not changed - -// Sanity checks -#if !defined(TemperatureDecimals) || TemperatureDecimals < 0 - #define TemperatureDecimals 0 -#endif -#if !defined(HumidityDecimals) || HumidityDecimals < 0 - #define HumidityDecimals 0 -#endif -#if !defined(PressureDecimals) || PressureDecimals < 0 - #define PressureDecimals 0 -#endif -#if !defined(TemperatureInterval) || TemperatureInterval < 0 - #define TemperatureInterval 1 -#endif -#if !defined(PressureInterval) || PressureInterval < 0 - #define PressureInterval TemperatureInterval -#endif -#if !defined(PublishAlways) - #define PublishAlways 0 -#endif + + // NOTE: Do not implement any compile-time variables, anything the user needs to configure + // should be configurable from the Usermod menu using the methods below + // key settings set via usermod menu + uint8_t TemperatureDecimals = 0; // Number of decimal places in published temperaure values + uint8_t HumidityDecimals = 0; // Number of decimal places in published humidity values + uint8_t PressureDecimals = 0; // Number of decimal places in published pressure values + uint16_t TemperatureInterval = 5; // Interval to measure temperature (and humidity, dew point if available) in seconds + uint16_t PressureInterval = 300; // Interval to measure pressure in seconds + bool PublishAlways = false; // Publish values even when they have not changed + bool UseCelsius = true; // Use Celsius for Reporting + bool HomeAssistantDiscovery = false; // Publish Home Assistant Device Information + bool enabled = true; -#ifdef ARDUINO_ARCH_ESP32 // ESP32 boards - uint8_t SCL_PIN = 22; - uint8_t SDA_PIN = 21; -#else // ESP8266 boards - uint8_t SCL_PIN = 5; - uint8_t SDA_PIN = 4; - //uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 -#endif + // set the default pins based on the architecture, these get overridden by Usermod menu settings + #ifdef ESP8266 + //uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 + #endif + bool initDone = false; // BME280 sensor settings BME280I2C::Settings settings{ @@ -75,6 +63,7 @@ class UsermodBME280 : public Usermod float sensorHeatIndex; float sensorDewPoint; float sensorPressure; + String tempScale; // Track previous sensor values float lastTemperature; float lastHumidity; @@ -82,43 +71,125 @@ class UsermodBME280 : public Usermod float lastDewPoint; float lastPressure; - // Store packet IDs of MQTT publications - uint16_t mqttTemperaturePub = 0; - uint16_t mqttPressurePub = 0; + // MQTT topic strings for publishing Home Assistant discovery topics + bool mqttInitialized = false; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + // Read the BME280/BMP280 Sensor (which one runs depends on whether Celsius or Fahrenheit being set in Usermod Menu) void UpdateBME280Data(int SensorType) { float _temperature, _humidity, _pressure; - #ifdef Celsius + + if (UseCelsius) { BME280::TempUnit tempUnit(BME280::TempUnit_Celsius); EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius); - #else + BME280::PresUnit presUnit(BME280::PresUnit_hPa); + + bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit); + + sensorTemperature = _temperature; + sensorHumidity = _humidity; + sensorPressure = _pressure; + tempScale = F("°C"); + if (sensorType == 1) + { + sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit); + sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit); + } + } else { BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit); EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Fahrenheit); - #endif - BME280::PresUnit presUnit(BME280::PresUnit_hPa); + BME280::PresUnit presUnit(BME280::PresUnit_hPa); - bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit); + bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit); - sensorTemperature = _temperature; - sensorHumidity = _humidity; - sensorPressure = _pressure; - if (sensorType == 1) - { - sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit); - sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit); + sensorTemperature = _temperature; + sensorHumidity = _humidity; + sensorPressure = _pressure; + tempScale = F("°F"); + if (sensorType == 1) + { + sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit); + sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit); + } } } + // Procedure to define all MQTT discovery Topics + void _mqttInitialize() + { + char mqttTemperatureTopic[128]; + char mqttHumidityTopic[128]; + char mqttPressureTopic[128]; + char mqttHeatIndexTopic[128]; + char mqttDewPointTopic[128]; + snprintf_P(mqttTemperatureTopic, 127, PSTR("%s/temperature"), mqttDeviceTopic); + snprintf_P(mqttPressureTopic, 127, PSTR("%s/pressure"), mqttDeviceTopic); + snprintf_P(mqttHumidityTopic, 127, PSTR("%s/humidity"), mqttDeviceTopic); + snprintf_P(mqttHeatIndexTopic, 127, PSTR("%s/heat_index"), mqttDeviceTopic); + snprintf_P(mqttDewPointTopic, 127, PSTR("%s/dew_point"), mqttDeviceTopic); + + if (HomeAssistantDiscovery) { + _createMqttSensor(F("Temperature"), mqttTemperatureTopic, "temperature", tempScale); + _createMqttSensor(F("Pressure"), mqttPressureTopic, "pressure", F("hPa")); + _createMqttSensor(F("Humidity"), mqttHumidityTopic, "humidity", F("%")); + _createMqttSensor(F("HeatIndex"), mqttHeatIndexTopic, "temperature", tempScale); + _createMqttSensor(F("DewPoint"), mqttDewPointTopic, "temperature", tempScale); + } + } + + // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. + void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) + { + String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = String(serverDescription) + " " + name; + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + if (deviceClass != "") + doc[F("device_class")] = deviceClass; + doc[F("expire_after")] = 1800; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F("WLED"); + device[F("model")] = F("FOSS"); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } + + void publishMqtt(const char *topic, const char* state) { + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED){ + char subuf[128]; + snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); + mqtt->publish(subuf, 0, false, state); + } + } + public: void setup() { - Wire.begin(SDA_PIN, SCL_PIN); - + if (i2c_scl<0 || i2c_sda<0) { enabled = false; sensorType = 0; return; } + if (!bme.begin()) { sensorType = 0; - Serial.println("Could not find BME280I2C sensor!"); + DEBUG_PRINTLN(F("Could not find BME280 I2C sensor!")); } else { @@ -126,69 +197,68 @@ class UsermodBME280 : public Usermod { case BME280::ChipModel_BME280: sensorType = 1; - Serial.println("Found BME280 sensor! Success."); + DEBUG_PRINTLN(F("Found BME280 sensor! Success.")); break; case BME280::ChipModel_BMP280: sensorType = 2; - Serial.println("Found BMP280 sensor! No Humidity available."); + DEBUG_PRINTLN(F("Found BMP280 sensor! No Humidity available.")); break; default: sensorType = 0; - Serial.println("Found UNKNOWN sensor! Error!"); + DEBUG_PRINTLN(F("Found UNKNOWN sensor! Error!")); } } + initDone=true; } void loop() { + if (!enabled || strip.isUpdating()) return; + // BME280 sensor MQTT publishing - // Check if sensor present and MQTT Connected, otherwise it will crash the MCU - if (sensorType != 0 && mqtt != nullptr) + // Check if sensor present and Connected, otherwise it will crash the MCU + if (sensorType != 0) { // Timer to fetch new temperature, humidity and pressure data at intervals timer = millis(); - if (timer - lastTemperatureMeasure >= TemperatureInterval * 1000 || mqttTemperaturePub == 0) + if (timer - lastTemperatureMeasure >= TemperatureInterval * 1000) { lastTemperatureMeasure = timer; UpdateBME280Data(sensorType); - float temperature = roundf(sensorTemperature * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); + float temperature = roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); float humidity, heatIndex, dewPoint; // If temperature has changed since last measure, create string populated with device topic // from the UI and values read from sensor, then publish to broker if (temperature != lastTemperature || PublishAlways) { - String topic = String(mqttDeviceTopic) + "/temperature"; - mqttTemperaturePub = mqtt->publish(topic.c_str(), 0, false, String(temperature, TemperatureDecimals).c_str()); + publishMqtt("temperature", String(temperature, TemperatureDecimals).c_str()); } lastTemperature = temperature; // Update last sensor temperature for next loop if (sensorType == 1) // Only if sensor is a BME280 { - humidity = roundf(sensorHumidity * pow(10, HumidityDecimals)) / pow(10, HumidityDecimals); - heatIndex = roundf(sensorHeatIndex * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); - dewPoint = roundf(sensorDewPoint * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); + humidity = roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals); + heatIndex = roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); + dewPoint = roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); if (humidity != lastHumidity || PublishAlways) { - String topic = String(mqttDeviceTopic) + "/humidity"; - mqtt->publish(topic.c_str(), 0, false, String(humidity, HumidityDecimals).c_str()); + publishMqtt("humidity", String(humidity, HumidityDecimals).c_str()); } if (heatIndex != lastHeatIndex || PublishAlways) { - String topic = String(mqttDeviceTopic) + "/heat_index"; - mqtt->publish(topic.c_str(), 0, false, String(heatIndex, TemperatureDecimals).c_str()); + publishMqtt("heat_index", String(heatIndex, TemperatureDecimals).c_str()); } if (dewPoint != lastDewPoint || PublishAlways) { - String topic = String(mqttDeviceTopic) + "/dew_point"; - mqtt->publish(topic.c_str(), 0, false, String(dewPoint, TemperatureDecimals).c_str()); + publishMqtt("dew_point", String(dewPoint, TemperatureDecimals).c_str()); } lastHumidity = humidity; @@ -197,20 +267,190 @@ class UsermodBME280 : public Usermod } } - if (timer - lastPressureMeasure >= PressureInterval * 1000 || mqttPressurePub == 0) + if (timer - lastPressureMeasure >= PressureInterval * 1000) { lastPressureMeasure = timer; - float pressure = roundf(sensorPressure * pow(10, PressureDecimals)) / pow(10, PressureDecimals); + float pressure = roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals); if (pressure != lastPressure || PublishAlways) { - String topic = String(mqttDeviceTopic) + "/pressure"; - mqttPressurePub = mqtt->publish(topic.c_str(), 0, true, String(pressure, PressureDecimals).c_str()); + publishMqtt("pressure", String(pressure, PressureDecimals).c_str()); } lastPressure = pressure; } } } -}; \ No newline at end of file + + void onMqttConnect(bool sessionPresent) + { + if (WLED_MQTT_CONNECTED && !mqttInitialized) + { + _mqttInitialize(); + mqttInitialized = true; + } + } + + /* + * API calls te enable data exchange between WLED modules + */ + inline float getTemperatureC() { + if (UseCelsius) { + return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); + } else { + return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32; + } + } + + inline float getTemperatureF() { + if (UseCelsius) { + return ((float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f; + } else { + return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); + } + } + + inline float getHumidity() { + return (float)roundf(sensorHumidity * powf(10, HumidityDecimals)); + } + + inline float getPressure() { + return (float)roundf(sensorPressure * powf(10, PressureDecimals)); + } + + inline float getDewPointC() { + if (UseCelsius) { + return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); + } else { + return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32; + } + } + + inline float getDewPointF() { + if (UseCelsius) { + return ((float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f; + } else { + return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); + } + } + + inline float getHeatIndexC() { + if (UseCelsius) { + return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); + } else { + return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32; + } + } + + inline float getHeatIndexF() { + if (UseCelsius) { + return ((float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f; + } else { + return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); + } + } + + // Publish Sensor Information to Info Page + void addToJsonInfo(JsonObject &root) + { + JsonObject user = root[F("u")]; + if (user.isNull()) user = root.createNestedObject(F("u")); + + if (sensorType==0) //No Sensor + { + // if we sensor not detected, let the user know + JsonArray temperature_json = user.createNestedArray(F("BME/BMP280 Sensor")); + temperature_json.add(F("Not Found")); + } + else if (sensorType==2) //BMP280 + { + + JsonArray temperature_json = user.createNestedArray(F("Temperature")); + JsonArray pressure_json = user.createNestedArray(F("Pressure")); + temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals))); + temperature_json.add(tempScale); + pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals))); + pressure_json.add(F("hPa")); + } + else if (sensorType==1) //BME280 + { + JsonArray temperature_json = user.createNestedArray(F("Temperature")); + JsonArray humidity_json = user.createNestedArray(F("Humidity")); + JsonArray pressure_json = user.createNestedArray(F("Pressure")); + JsonArray heatindex_json = user.createNestedArray(F("Heat Index")); + JsonArray dewpoint_json = user.createNestedArray(F("Dew Point")); + temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); + temperature_json.add(tempScale); + humidity_json.add(roundf(sensorHumidity * powf(10, HumidityDecimals))); + humidity_json.add(F("%")); + pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals))); + pressure_json.add(F("hPa")); + heatindex_json.add(roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); + heatindex_json.add(tempScale); + dewpoint_json.add(roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); + dewpoint_json.add(tempScale); + } + return; + } + + // Save Usermod Config Settings + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[F("TemperatureDecimals")] = TemperatureDecimals; + top[F("HumidityDecimals")] = HumidityDecimals; + top[F("PressureDecimals")] = PressureDecimals; + top[F("TemperatureInterval")] = TemperatureInterval; + top[F("PressureInterval")] = PressureInterval; + top[F("PublishAlways")] = PublishAlways; + top[F("UseCelsius")] = UseCelsius; + top[F("HomeAssistantDiscovery")] = HomeAssistantDiscovery; + DEBUG_PRINTLN(F("BME280 config saved.")); + } + + // Read Usermod Config Settings + bool readFromConfig(JsonObject& root) + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(F(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing + configComplete &= getJsonValue(top[F("TemperatureDecimals")], TemperatureDecimals, 1); + configComplete &= getJsonValue(top[F("HumidityDecimals")], HumidityDecimals, 0); + configComplete &= getJsonValue(top[F("PressureDecimals")], PressureDecimals, 0); + configComplete &= getJsonValue(top[F("TemperatureInterval")], TemperatureInterval, 30); + configComplete &= getJsonValue(top[F("PressureInterval")], PressureInterval, 30); + configComplete &= getJsonValue(top[F("PublishAlways")], PublishAlways, false); + configComplete &= getJsonValue(top[F("UseCelsius")], UseCelsius, true); + configComplete &= getJsonValue(top[F("HomeAssistantDiscovery")], HomeAssistantDiscovery, false); + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + } + + return configComplete; + } + + uint16_t getId() { + return USERMOD_ID_BME280; + } +}; + +const char UsermodBME280::_name[] PROGMEM = "BME280/BMP280"; +const char UsermodBME280::_enabled[] PROGMEM = "enabled"; diff --git a/usermods/battery_status_basic/assets/battery_connection_schematic_01.png b/usermods/Battery/assets/battery_connection_schematic_01.png similarity index 100% rename from usermods/battery_status_basic/assets/battery_connection_schematic_01.png rename to usermods/Battery/assets/battery_connection_schematic_01.png diff --git a/usermods/battery_status_basic/assets/battery_connection_schematic_02.png b/usermods/Battery/assets/battery_connection_schematic_02.png similarity index 100% rename from usermods/battery_status_basic/assets/battery_connection_schematic_02.png rename to usermods/Battery/assets/battery_connection_schematic_02.png diff --git a/usermods/Battery/assets/battery_info_screen.png b/usermods/Battery/assets/battery_info_screen.png new file mode 100644 index 0000000000..5aa60a031f Binary files /dev/null and b/usermods/Battery/assets/battery_info_screen.png differ diff --git a/usermods/Battery/assets/battery_usermod_logo.png b/usermods/Battery/assets/battery_usermod_logo.png new file mode 100644 index 0000000000..b1134eb3b4 Binary files /dev/null and b/usermods/Battery/assets/battery_usermod_logo.png differ diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h new file mode 100644 index 0000000000..958bfe5263 --- /dev/null +++ b/usermods/Battery/battery_defaults.h @@ -0,0 +1,81 @@ +// pin defaults +// for the esp32 it is best to use the ADC1: GPIO32 - GPIO39 +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html +#ifndef USERMOD_BATTERY_MEASUREMENT_PIN + #ifdef ARDUINO_ARCH_ESP32 + #define USERMOD_BATTERY_MEASUREMENT_PIN 35 + #else //ESP8266 boards + #define USERMOD_BATTERY_MEASUREMENT_PIN A0 + #endif +#endif + +// the frequency to check the battery, 30 sec +#ifndef USERMOD_BATTERY_MEASUREMENT_INTERVAL + #define USERMOD_BATTERY_MEASUREMENT_INTERVAL 30000 +#endif + +// default for 18650 battery +// https://batterybro.com/blogs/18650-wholesale-battery-reviews/18852515-when-to-recycle-18650-batteries-and-how-to-start-a-collection-center-in-your-vape-shop +// Discharge voltage: 2.5 volt + .1 for personal safety +#ifndef USERMOD_BATTERY_MIN_VOLTAGE + #ifdef USERMOD_BATTERY_USE_LIPO + // LiPo "1S" Batteries should not be dischared below 3V !! + #define USERMOD_BATTERY_MIN_VOLTAGE 3.2f + #else + #define USERMOD_BATTERY_MIN_VOLTAGE 2.6f + #endif +#endif + +//the default ratio for the voltage divider +#ifndef USERMOD_BATTERY_VOLTAGE_MULTIPLIER + #ifdef ARDUINO_ARCH_ESP32 + #define USERMOD_BATTERY_VOLTAGE_MULTIPLIER 2.0f + #else //ESP8266 boards + #define USERMOD_BATTERY_VOLTAGE_MULTIPLIER 4.2f + #endif +#endif + +#ifndef USERMOD_BATTERY_MAX_VOLTAGE + #define USERMOD_BATTERY_MAX_VOLTAGE 4.2f +#endif + +// a common capacity for single 18650 battery cells is between 2500 and 3600 mAh +#ifndef USERMOD_BATTERY_TOTAL_CAPACITY + #define USERMOD_BATTERY_TOTAL_CAPACITY 3100 +#endif + +// offset or calibration value to fine tune the calculated voltage +#ifndef USERMOD_BATTERY_CALIBRATION + #define USERMOD_BATTERY_CALIBRATION 0 +#endif + +// calculate remaining time / the time that is left before the battery runs out of power +// #ifndef USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED +// #define USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED false +// #endif + +// auto-off feature +#ifndef USERMOD_BATTERY_AUTO_OFF_ENABLED + #define USERMOD_BATTERY_AUTO_OFF_ENABLED true +#endif + +#ifndef USERMOD_BATTERY_AUTO_OFF_THRESHOLD + #define USERMOD_BATTERY_AUTO_OFF_THRESHOLD 10 +#endif + +// low power indication feature +#ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED + #define USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED true +#endif + +#ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET + #define USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET 0 +#endif + +#ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD + #define USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD 20 +#endif + +#ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION + #define USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION 5 +#endif \ No newline at end of file diff --git a/usermods/Battery/readme.md b/usermods/Battery/readme.md new file mode 100644 index 0000000000..999c0a541e --- /dev/null +++ b/usermods/Battery/readme.md @@ -0,0 +1,112 @@ +

+ +

+ +# Welcome to the battery usermod! 🔋 + +Enables battery level monitoring of your project. + +For this to work, the positive side of the (18650) battery must be connected to pin `A0` of the d1 mini/esp8266 with a 100k Ohm resistor (see [Useful Links](#useful-links)). + +If you have an ESP32 board, connect the positive side of the battery to ADC1 (GPIO32 - GPIO39) + +

+ +

+ +## ⚙️ Features + +- 💯 Displays current battery voltage +- 🚥 Displays battery level +- 🚫 Auto-off with configurable Threshold +- 🚨 Low power indicator with many configuration possibilities + +## 🎈 Installation + +define `USERMOD_BATTERY` in `wled00/my_config.h` + +### Example wiring + +

+ +

+ +### Define Your Options + +| Name | Unit | Description | +| ----------------------------------------------- | ----------- |-------------------------------------------------------------------------------------- | +| `USERMOD_BATTERY` | | define this (in `my_config.h`) to have this usermod included wled00\usermods_list.cpp | +| `USERMOD_BATTERY_USE_LIPO` | | define this (in `my_config.h`) if you use LiPo rechargeables (1S) | +| `USERMOD_BATTERY_MEASUREMENT_PIN` | | defaults to A0 on ESP8266 and GPIO35 on ESP32 | +| `USERMOD_BATTERY_MEASUREMENT_INTERVAL` | ms | battery check interval. defaults to 30 seconds | +| `USERMOD_BATTERY_MIN_VOLTAGE` | v | minimum battery voltage. default is 2.6 (18650 battery standard) | +| `USERMOD_BATTERY_MAX_VOLTAGE` | v | maximum battery voltage. default is 4.2 (18650 battery standard) | +| `USERMOD_BATTERY_TOTAL_CAPACITY` | mAh | the capacity of all cells in parallel summed up | +| `USERMOD_BATTERY_CALIBRATION` | | offset / calibration number, fine tune the measured voltage by the microcontroller | +| Auto-Off | --- | --- | +| `USERMOD_BATTERY_AUTO_OFF_ENABLED` | true/false | enables auto-off | +| `USERMOD_BATTERY_AUTO_OFF_THRESHOLD` | % (0-100) | when this threshold is reached master power turns off | +| Low-Power-Indicator | --- | --- | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED` | true/false | enables low power indication | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET` | preset id | when low power is detected then use this preset to indicate low power | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD` | % (0-100) | when this threshold is reached low power gets indicated | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION` | seconds | for this long the configured preset is played | + +All parameters can be configured at runtime via the Usermods settings page. + +## ⚠️ Important + +- Make sure you know your battery specifications! All batteries are **NOT** the same! +- Example: + +| Your battery specification table | | Options you can define | +| :-------------------------------- |:--------------- | :---------------------------- | +| Capacity | 3500mAh 12,5 Wh | | +| Minimum capacity | 3350mAh 11,9 Wh | | +| Rated voltage | 3.6V - 3.7V | | +| **Charging end voltage** | **4,2V ± 0,05** | `USERMOD_BATTERY_MAX_VOLTAGE` | +| **Discharge voltage** | **2,5V** | `USERMOD_BATTERY_MIN_VOLTAGE` | +| Max. discharge current (constant) | 10A (10000mA) | | +| max. charging current | 1.7A (1700mA) | | +| ... | ... | ... | +| .. | .. | .. | + +Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6V - 3.7V](https://www.akkuteile.de/lithium-ionen-akkus/18650/molicel/molicel-inr18650-m35a-3500mah-10a-lithium-ionen-akku-3-6v-3-7v_100833) + +## 🌐 Useful Links + +- https://lazyzero.de/elektronik/esp8266/wemos_d1_mini_a0/start +- https://arduinodiy.wordpress.com/2016/12/25/monitoring-lipo-battery-voltage-with-wemos-d1-minibattery-shield-and-thingspeak/ + +## 📝 Change Log + +2023-01-04 + +- basic support for LiPo rechargeable batteries ( `-D USERMOD_BATTERY_USE_LIPO`) +- improved support for esp32 (read calibrated voltage) +- corrected config saving (measurement pin, and battery min/max were lost) +- various bugfixes + +2022-12-25 + +- added "auto-off" feature +- added "low-power-indication" feature +- added "calibration/offset" field to configuration page +- added getter and setter, so that user usermods could interact with this one +- update readme (added new options, made it markdownlint compliant) + +2021-09-02 + +- added "Battery voltage" to info +- added circuit diagram to readme +- added MQTT support, sending battery voltage +- minor fixes + +2021-08-15 + +- changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries +- Updated readme, added specification table + +2021-08-10 + +- Created diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h new file mode 100644 index 0000000000..be3d8748bf --- /dev/null +++ b/usermods/Battery/usermod_v2_Battery.h @@ -0,0 +1,787 @@ +#pragma once + +#include "wled.h" +#include "battery_defaults.h" + +/* + * Usermod by Maximilian Mewes + * Mail: mewes.maximilian@gmx.de + * GitHub: itCarl + * Date: 25.12.2022 + * If you have any questions, please feel free to contact me. + */ +class UsermodBattery : public Usermod +{ + private: + // battery pin can be defined in my_config.h + int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; + // how often to read the battery voltage + unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; + unsigned long nextReadTime = 0; + unsigned long lastReadTime = 0; + // battery min. voltage + float minBatteryVoltage = USERMOD_BATTERY_MIN_VOLTAGE; + // battery max. voltage + float maxBatteryVoltage = USERMOD_BATTERY_MAX_VOLTAGE; + // all battery cells summed up + unsigned int totalBatteryCapacity = USERMOD_BATTERY_TOTAL_CAPACITY; + // raw analog reading + float rawValue = 0.0f; + // calculated voltage + float voltage = maxBatteryVoltage; + // between 0 and 1, to control strength of voltage smoothing filter + float alpha = 0.05f; + // multiplier for the voltage divider that is in place between ADC pin and battery, default will be 2 but might be adapted to readout voltages over ~5v ESP32 or ~6.6v ESP8266 + float voltageMultiplier = USERMOD_BATTERY_VOLTAGE_MULTIPLIER; + // mapped battery level based on voltage + int8_t batteryLevel = 100; + // offset or calibration value to fine tune the calculated voltage + float calibration = USERMOD_BATTERY_CALIBRATION; + + // time left estimation feature + // bool calculateTimeLeftEnabled = USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED; + // float estimatedTimeLeft = 0.0; + + // auto shutdown/shutoff/master off feature + bool autoOffEnabled = USERMOD_BATTERY_AUTO_OFF_ENABLED; + int8_t autoOffThreshold = USERMOD_BATTERY_AUTO_OFF_THRESHOLD; + + // low power indicator feature + bool lowPowerIndicatorEnabled = USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED; + int8_t lowPowerIndicatorPreset = USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET; + int8_t lowPowerIndicatorThreshold = USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD; + int8_t lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; + int8_t lowPowerIndicatorDuration = USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION; + bool lowPowerIndicationDone = false; + unsigned long lowPowerActivationTime = 0; // used temporary during active time + int8_t lastPreset = 0; + + bool initDone = false; + bool initializing = true; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _readInterval[]; + static const char _enabled[]; + static const char _threshold[]; + static const char _preset[]; + static const char _duration[]; + static const char _init[]; + + + // custom map function + // https://forum.arduino.cc/t/floating-point-using-map-function/348113/2 + double mapf(double x, double in_min, double in_max, double out_min, double out_max) + { + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; + } + + float dot2round(float x) + { + float nx = (int)(x * 100 + .5); + return (float)(nx / 100); + } + + /* + * Turn off all leds + */ + void turnOff() + { + bri = 0; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + } + + /* + * Indicate low power by activating a configured preset for a given time and then switching back to the preset that was selected previously + */ + void lowPowerIndicator() + { + if (!lowPowerIndicatorEnabled) return; + if (batteryPin < 0) return; // no measurement + if (lowPowerIndicationDone && lowPowerIndicatorReactivationThreshold <= batteryLevel) lowPowerIndicationDone = false; + if (lowPowerIndicatorThreshold <= batteryLevel) return; + if (lowPowerIndicationDone) return; + if (lowPowerActivationTime <= 1) { + lowPowerActivationTime = millis(); + lastPreset = currentPreset; + applyPreset(lowPowerIndicatorPreset); + } + + if (lowPowerActivationTime+(lowPowerIndicatorDuration*1000) <= millis()) { + lowPowerIndicationDone = true; + lowPowerActivationTime = 0; + applyPreset(lastPreset); + } + } + + float readVoltage() + { + #ifdef ARDUINO_ARCH_ESP32 + // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attentuation) and divide by 1000 to get from milivolts to volts and multiply by voltage multiplier and apply calibration value + return (analogReadMilliVolts(batteryPin) / 1000.0f) * voltageMultiplier + calibration; + #else + // use analog read on esp8266 ( 0V ~ 1V no attenuation options) and divide by ADC precision 1023 and multiply by voltage multiplier and apply calibration value + return (analogRead(batteryPin) / 1023.0f) * voltageMultiplier + calibration; + #endif + } + + public: + //Functions called by WLED + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() + { + #ifdef ARDUINO_ARCH_ESP32 + bool success = false; + DEBUG_PRINTLN(F("Allocating battery pin...")); + if (batteryPin >= 0 && digitalPinToAnalogChannel(batteryPin) >= 0) + if (pinManager.allocatePin(batteryPin, false, PinOwner::UM_Battery)) { + DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); + success = true; + voltage = readVoltage(); + } + + if (!success) { + DEBUG_PRINTLN(F("Battery pin allocation failed.")); + batteryPin = -1; // allocation failed + } else { + pinMode(batteryPin, INPUT); + } + #else //ESP8266 boards have only one analog input pin A0 + pinMode(batteryPin, INPUT); + voltage = readVoltage(); + #endif + + nextReadTime = millis() + readingInterval; + lastReadTime = millis(); + + initDone = true; + } + + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + //Serial.println("Connected to WiFi!"); + } + + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + * + */ + void loop() + { + if(strip.isUpdating()) return; + + lowPowerIndicator(); + + // check the battery level every USERMOD_BATTERY_MEASUREMENT_INTERVAL (ms) + if (millis() < nextReadTime) return; + + nextReadTime = millis() + readingInterval; + lastReadTime = millis(); + + if (batteryPin < 0) return; // nothing to read + + initializing = false; + + rawValue = readVoltage(); + // filter with exponential smoothing because ADC in esp32 is fluctuating too much for a good single readout + voltage = voltage + alpha * (rawValue - voltage); + + // check if voltage is within specified voltage range, allow 10% over/under voltage - removed cause this just makes it hard for people to troubleshoot as the voltage in the web gui will say invalid instead of displaying a voltage + //voltage = ((voltage < minBatteryVoltage * 0.85f) || (voltage > maxBatteryVoltage * 1.1f)) ? -1.0f : voltage; + + // translate battery voltage into percentage + /* + the standard "map" function doesn't work + https://www.arduino.cc/reference/en/language/functions/math/map/ notes and warnings at the bottom + */ + #ifdef USERMOD_BATTERY_USE_LIPO + batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); // basic mapping + // LiPo batteries have a differnt dischargin curve, see + // https://blog.ampow.com/lipo-voltage-chart/ + if (batteryLevel < 40.0f) + batteryLevel = mapf(batteryLevel, 0, 40, 0, 12); // last 45% -> drops very quickly + else { + if (batteryLevel < 90.0f) + batteryLevel = mapf(batteryLevel, 40, 90, 12, 95); // 90% ... 40% -> almost linear drop + else // level > 90% + batteryLevel = mapf(batteryLevel, 90, 105, 95, 100); // highest 15% -> drop slowly + } + #else + batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); + #endif + if (voltage > -1.0f) batteryLevel = constrain(batteryLevel, 0.0f, 110.0f); + + // if (calculateTimeLeftEnabled) { + // float currentBatteryCapacity = totalBatteryCapacity; + // estimatedTimeLeft = (currentBatteryCapacity/strip.currentMilliamps)*60; + // } + + // Auto off -- Master power off + if (autoOffEnabled && (autoOffThreshold >= batteryLevel)) + turnOff(); + +#ifndef WLED_DISABLE_MQTT + // SmartHome stuff + // still don't know much about MQTT and/or HA + if (WLED_MQTT_CONNECTED) { + char buf[64]; // buffer for snprintf() + snprintf_P(buf, 63, PSTR("%s/voltage"), mqttDeviceTopic); + mqtt->publish(buf, 0, false, String(voltage).c_str()); + } +#endif + + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + void addToJsonInfo(JsonObject& root) + { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + if (batteryPin < 0) { + JsonArray infoVoltage = user.createNestedArray(F("Battery voltage")); + infoVoltage.add(F("n/a")); + infoVoltage.add(F(" invalid GPIO")); + return; // no GPIO - nothing to report + } + + // info modal display names + JsonArray infoPercentage = user.createNestedArray(F("Battery level")); + JsonArray infoVoltage = user.createNestedArray(F("Battery voltage")); + // if (calculateTimeLeftEnabled) + // { + // JsonArray infoEstimatedTimeLeft = user.createNestedArray(F("Estimated time left")); + // if (initializing) { + // infoEstimatedTimeLeft.add(FPSTR(_init)); + // } else { + // infoEstimatedTimeLeft.add(estimatedTimeLeft); + // infoEstimatedTimeLeft.add(F(" min")); + // } + // } + JsonArray infoNextUpdate = user.createNestedArray(F("Next update")); + + infoNextUpdate.add((nextReadTime - millis()) / 1000); + infoNextUpdate.add(F(" sec")); + + if (initializing) { + infoPercentage.add(FPSTR(_init)); + infoVoltage.add(FPSTR(_init)); + return; + } + + if (batteryLevel < 0) { + infoPercentage.add(F("invalid")); + } else { + infoPercentage.add(batteryLevel); + } + infoPercentage.add(F(" %")); + + if (voltage < 0) { + infoVoltage.add(F("invalid")); + } else { + infoVoltage.add(dot2round(voltage)); + } + infoVoltage.add(F(" V")); + } + + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + /* + void addToJsonState(JsonObject& root) + { + + } + */ + + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + /* + void readFromJsonState(JsonObject& root) + { + } + */ + + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will make your settings editable through the Usermod Settings page automatically. + * + * Usermod Settings Overview: + * - Numeric values are treated as floats in the browser. + * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float + * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and + * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. + * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. + * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a + * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. + * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type + * used in the Usermod when reading the value from ArduinoJson. + * - Pin values can be treated differently from an integer value by using the key name "pin" + * - "pin" can contain a single or array of integer values + * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins + * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) + * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used + * + * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings + * + * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. + * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. + * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) + { + JsonObject battery = root.createNestedObject(FPSTR(_name)); // usermodname + #ifdef ARDUINO_ARCH_ESP32 + battery[F("pin")] = batteryPin; + #endif + + // battery[F("time-left")] = calculateTimeLeftEnabled; + battery[F("min-voltage")] = minBatteryVoltage; + battery[F("max-voltage")] = maxBatteryVoltage; + battery[F("capacity")] = totalBatteryCapacity; + battery[F("calibration")] = calibration; + battery[F("voltage-multiplier")] = voltageMultiplier; + battery[FPSTR(_readInterval)] = readingInterval; + + JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section + ao[FPSTR(_enabled)] = autoOffEnabled; + ao[FPSTR(_threshold)] = autoOffThreshold; + + JsonObject lp = battery.createNestedObject(F("indicator")); // low power section + lp[FPSTR(_enabled)] = lowPowerIndicatorEnabled; + lp[FPSTR(_preset)] = lowPowerIndicatorPreset; // dropdown trickery (String)lowPowerIndicatorPreset; + lp[FPSTR(_threshold)] = lowPowerIndicatorThreshold; + lp[FPSTR(_duration)] = lowPowerIndicatorDuration; + + // read voltage in case calibration or voltage multiplier changed to see immediate effect + voltage = readVoltage(); + + DEBUG_PRINTLN(F("Battery config saved.")); + } + + void appendConfigData() + { + oappend(SET_F("addInfo('Battery:min-voltage', 1, 'v');")); + oappend(SET_F("addInfo('Battery:max-voltage', 1, 'v');")); + oappend(SET_F("addInfo('Battery:capacity', 1, 'mAh');")); + oappend(SET_F("addInfo('Battery:interval', 1, 'ms');")); + oappend(SET_F("addInfo('Battery:auto-off:threshold', 1, '%');")); + oappend(SET_F("addInfo('Battery:indicator:threshold', 1, '%');")); + oappend(SET_F("addInfo('Battery:indicator:duration', 1, 's');")); + + // cannot quite get this mf to work. its exeeding some buffer limit i think + // what i wanted is a list of all presets to select one from + // oappend(SET_F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); + // the loop generates: oappend(SET_F("addOption(bd, 'preset name', preset id);")); + // for(int8_t i=1; i < 42; i++) { + // oappend(SET_F("addOption(bd, 'Preset#")); + // oappendi(i); + // oappend(SET_F("',")); + // oappendi(i); + // oappend(SET_F(");")); + // } + } + + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + * + * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present + * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them + * + * This function is guaranteed to be called on boot, but could also be called every time settings are updated + */ + bool readFromConfig(JsonObject& root) + { + #ifdef ARDUINO_ARCH_ESP32 + int8_t newBatteryPin = batteryPin; + #endif + + JsonObject battery = root[FPSTR(_name)]; + if (battery.isNull()) + { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + #ifdef ARDUINO_ARCH_ESP32 + newBatteryPin = battery[F("pin")] | newBatteryPin; + #endif + // calculateTimeLeftEnabled = battery[F("time-left")] | calculateTimeLeftEnabled; + setMinBatteryVoltage(battery[F("min-voltage")] | minBatteryVoltage); + setMaxBatteryVoltage(battery[F("max-voltage")] | maxBatteryVoltage); + setTotalBatteryCapacity(battery[F("capacity")] | totalBatteryCapacity); + setCalibration(battery[F("calibration")] | calibration); + setVoltageMultiplier(battery[F("voltage-multiplier")] | voltageMultiplier); + setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); + + JsonObject ao = battery[F("auto-off")]; + setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); + setAutoOffThreshold(ao[FPSTR(_threshold)] | autoOffThreshold); + + JsonObject lp = battery[F("indicator")]; + setLowPowerIndicatorEnabled(lp[FPSTR(_enabled)] | lowPowerIndicatorEnabled); + setLowPowerIndicatorPreset(lp[FPSTR(_preset)] | lowPowerIndicatorPreset); // dropdown trickery (int)lp["preset"] + setLowPowerIndicatorThreshold(lp[FPSTR(_threshold)] | lowPowerIndicatorThreshold); + lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; + setLowPowerIndicatorDuration(lp[FPSTR(_duration)] | lowPowerIndicatorDuration); + + DEBUG_PRINT(FPSTR(_name)); + + #ifdef ARDUINO_ARCH_ESP32 + if (!initDone) + { + // first run: reading from cfg.json + batteryPin = newBatteryPin; + DEBUG_PRINTLN(F(" config loaded.")); + } + else + { + DEBUG_PRINTLN(F(" config (re)loaded.")); + + // changing parameters from settings page + if (newBatteryPin != batteryPin) + { + // deallocate pin + pinManager.deallocatePin(batteryPin, PinOwner::UM_Battery); + batteryPin = newBatteryPin; + // initialise + setup(); + } + } + #endif + + return !battery[FPSTR(_readInterval)].isNull(); + } + + /* + * Generate a preset sample for low power indication + */ + void generateExamplePreset() + { + // StaticJsonDocument<300> j; + // JsonObject preset = j.createNestedObject(); + // preset["mainseg"] = 0; + // JsonArray seg = preset.createNestedArray("seg"); + // JsonObject seg0 = seg.createNestedObject(); + // seg0["id"] = 0; + // seg0["start"] = 0; + // seg0["stop"] = 60; + // seg0["grp"] = 0; + // seg0["spc"] = 0; + // seg0["on"] = true; + // seg0["bri"] = 255; + + // JsonArray col0 = seg0.createNestedArray("col"); + // JsonArray col00 = col0.createNestedArray(); + // col00.add(255); + // col00.add(0); + // col00.add(0); + + // seg0["fx"] = 1; + // seg0["sx"] = 128; + // seg0["ix"] = 128; + + // savePreset(199, "Low power Indicator", preset); + } + + + /* + * + * Getter and Setter. Just in case some other usermod wants to interact with this in the future + * + */ + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_BATTERY; + } + + + unsigned long getReadingInterval() + { + return readingInterval; + } + + /* + * minimum repetition is 3000ms (3s) + */ + void setReadingInterval(unsigned long newReadingInterval) + { + readingInterval = max((unsigned long)3000, newReadingInterval); + } + + + /* + * Get lowest configured battery voltage + */ + float getMinBatteryVoltage() + { + return minBatteryVoltage; + } + + /* + * Set lowest battery voltage + * can't be below 0 volt + */ + void setMinBatteryVoltage(float voltage) + { + minBatteryVoltage = max(0.0f, voltage); + } + + /* + * Get highest configured battery voltage + */ + float getMaxBatteryVoltage() + { + return maxBatteryVoltage; + } + + /* + * Set highest battery voltage + * can't be below minBatteryVoltage + */ + void setMaxBatteryVoltage(float voltage) + { + #ifdef USERMOD_BATTERY_USE_LIPO + maxBatteryVoltage = max(getMinBatteryVoltage()+0.7f, voltage); + #else + maxBatteryVoltage = max(getMinBatteryVoltage()+1.0f, voltage); + #endif + } + + + /* + * Get the capacity of all cells in parralel sumed up + * unit: mAh + */ + unsigned int getTotalBatteryCapacity() + { + return totalBatteryCapacity; + } + + void setTotalBatteryCapacity(unsigned int capacity) + { + totalBatteryCapacity = capacity; + } + + + + /* + * Get the calculated voltage + * formula: (adc pin value / adc precision * max voltage) + calibration + */ + float getVoltage() + { + return voltage; + } + + /* + * Get the mapped battery level (0 - 100) based on voltage + * important: voltage can drop when a load is applied, so its only an estimate + */ + int8_t getBatteryLevel() + { + return batteryLevel; + } + + /* + * Get the configured calibration value + * a offset value to fine-tune the calculated voltage. + */ + float getCalibration() + { + return calibration; + } + + /* + * Set the voltage calibration offset value + * a offset value to fine-tune the calculated voltage. + */ + void setCalibration(float offset) + { + calibration = offset; + } + + /* + * Set the voltage multiplier value + * A multiplier that may need adjusting for different voltage divider setups + */ + void setVoltageMultiplier(float multiplier) + { + voltageMultiplier = multiplier; + } + + /* + * Get the voltage multiplier value + * A multiplier that may need adjusting for different voltage divider setups + */ + float getVoltageMultiplier() + { + return voltageMultiplier; + } + + /* + * Get auto-off feature enabled status + * is auto-off enabled, true/false + */ + bool getAutoOffEnabled() + { + return autoOffEnabled; + } + + /* + * Set auto-off feature status + */ + void setAutoOffEnabled(bool enabled) + { + autoOffEnabled = enabled; + } + + /* + * Get auto-off threshold in percent (0-100) + */ + int8_t getAutoOffThreshold() + { + return autoOffThreshold; + } + + /* + * Set auto-off threshold in percent (0-100) + */ + void setAutoOffThreshold(int8_t threshold) + { + autoOffThreshold = min((int8_t)100, max((int8_t)0, threshold)); + // when low power indicator is enabled the auto-off threshold cannot be above indicator threshold + autoOffThreshold = lowPowerIndicatorEnabled /*&& autoOffEnabled*/ ? min(lowPowerIndicatorThreshold-1, (int)autoOffThreshold) : autoOffThreshold; + } + + + /* + * Get low-power-indicator feature enabled status + * is the low-power-indicator enabled, true/false + */ + bool getLowPowerIndicatorEnabled() + { + return lowPowerIndicatorEnabled; + } + + /* + * Set low-power-indicator feature status + */ + void setLowPowerIndicatorEnabled(bool enabled) + { + lowPowerIndicatorEnabled = enabled; + } + + /* + * Get low-power-indicator preset to activate when low power is detected + */ + int8_t getLowPowerIndicatorPreset() + { + return lowPowerIndicatorPreset; + } + + /* + * Set low-power-indicator preset to activate when low power is detected + */ + void setLowPowerIndicatorPreset(int8_t presetId) + { + // String tmp = ""; For what ever reason this doesn't work :( + // lowPowerIndicatorPreset = getPresetName(presetId, tmp) ? presetId : lowPowerIndicatorPreset; + lowPowerIndicatorPreset = presetId; + } + + /* + * Get low-power-indicator threshold in percent (0-100) + */ + int8_t getLowPowerIndicatorThreshold() + { + return lowPowerIndicatorThreshold; + } + + /* + * Set low-power-indicator threshold in percent (0-100) + */ + void setLowPowerIndicatorThreshold(int8_t threshold) + { + lowPowerIndicatorThreshold = threshold; + // when auto-off is enabled the indicator threshold cannot be below auto-off threshold + lowPowerIndicatorThreshold = autoOffEnabled /*&& lowPowerIndicatorEnabled*/ ? max(autoOffThreshold+1, (int)lowPowerIndicatorThreshold) : max(5, (int)lowPowerIndicatorThreshold); + } + + /* + * Get low-power-indicator duration in seconds + */ + int8_t getLowPowerIndicatorDuration() + { + return lowPowerIndicatorDuration; + } + + /* + * Set low-power-indicator duration in seconds + */ + void setLowPowerIndicatorDuration(int8_t duration) + { + lowPowerIndicatorDuration = duration; + } + + + /* + * Get low-power-indicator status when the indication is done thsi returns true + */ + bool getLowPowerIndicatorDone() + { + return lowPowerIndicationDone; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char UsermodBattery::_name[] PROGMEM = "Battery"; +const char UsermodBattery::_readInterval[] PROGMEM = "interval"; +const char UsermodBattery::_enabled[] PROGMEM = "enabled"; +const char UsermodBattery::_threshold[] PROGMEM = "threshold"; +const char UsermodBattery::_preset[] PROGMEM = "preset"; +const char UsermodBattery::_duration[] PROGMEM = "duration"; +const char UsermodBattery::_init[] PROGMEM = "init"; diff --git a/usermods/Cronixie/usermod_cronixie.h b/usermods/Cronixie/usermod_cronixie.h index 5e4255f438..534fd3a7c2 100644 --- a/usermods/Cronixie/usermod_cronixie.h +++ b/usermods/Cronixie/usermod_cronixie.h @@ -249,7 +249,7 @@ class UsermodCronixie : public Usermod { if (backlight && _digitOut[i] <11) { - uint32_t col = strip.gamma32(strip.getSegment(0).colors[1]); + uint32_t col = gamma32(strip.getSegment(0).colors[1]); for (uint16_t j=o; j< o+10; j++) { if (j != excl) strip.setPixelColor(j, col); } @@ -271,6 +271,7 @@ class UsermodCronixie : public Usermod { { if (root["nx"].is()) { strncpy(cronixieDisplay, root["nx"], 6); + setCronixie(); } } diff --git a/usermods/DHT/platformio_override.ini b/usermods/DHT/platformio_override.ini index 1771fd17a3..d192f0434e 100644 --- a/usermods/DHT/platformio_override.ini +++ b/usermods/DHT/platformio_override.ini @@ -6,12 +6,13 @@ ; USERMOD_DHT_CELSIUS - define this to report temperatures in degrees celsious, otherwise fahrenheit will be reported ; USERMOD_DHT_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds ; USERMOD_DHT_FIRST_MEASUREMENT_AT - the number of milliseconds after boot to take first measurement, defaults to 90 seconds +; USERMOD_DHT_MQTT - publish measurements to the MQTT broker ; USERMOD_DHT_STATS - For debug, report delay stats [env:d1_mini_usermod_dht_C] extends = env:d1_mini build_flags = ${env:d1_mini.build_flags} -D USERMOD_DHT -D USERMOD_DHT_CELSIUS -lib_deps = ${env.lib_deps} +lib_deps = ${env:d1_mini.lib_deps} https://github.com/alwynallan/DHT_nonblocking [env:custom32_LEDPIN_16_usermod_dht_C] diff --git a/usermods/DHT/readme.md b/usermods/DHT/readme.md index 5a123d4bde..6089ffbf88 100644 --- a/usermods/DHT/readme.md +++ b/usermods/DHT/readme.md @@ -1,9 +1,13 @@ # DHT Temperature/Humidity sensor usermod This usermod will read from an attached DHT22 or DHT11 humidity and temperature sensor. -The sensor readings are displayed in the Info section of the web UI. +The sensor readings are displayed in the Info section of the web UI (and optionally sent to an MQTT broker). -If sensor is not detected after a while (10 update intervals), this usermod will be disabled. +If sensor is not detected after 10 update intervals, the usermod will be disabled. + +If enabled, measured temperature and humidity will be published to the following MQTT topics +* `{devceTopic}/dht/temperature` +* `{devceTopic}/dht/humidity` ## Installation @@ -11,12 +15,13 @@ Copy the example `platformio_override.ini` to the root directory. This file sho ### Define Your Options -* `USERMOD_DHT` - define this to have this user mod included wled00\usermods_list.cpp +* `USERMOD_DHT` - define this to include this user mod wled00\usermods_list.cpp * `USERMOD_DHT_DHTTYPE` - DHT model: 11, 21, 22 for DHT11, DHT21, or DHT22, defaults to 22/DHT22 * `USERMOD_DHT_PIN` - pin to which DTH is connected, defaults to Q2 pin on QuinLed Dig-Uno's board -* `USERMOD_DHT_CELSIUS` - define this to report temperatures in degrees celsious, otherwise fahrenheit will be reported -* `USERMOD_DHT_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60 seconds -* `USERMOD_DHT_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 90 seconds +* `USERMOD_DHT_CELSIUS` - define this to report temperatures in degrees Celsius, otherwise Fahrenheit will be reported +* `USERMOD_DHT_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60000 ms +* `USERMOD_DHT_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 90000 ms +* `USERMOD_DHT_MQTT` - publish measurements to an MQTT broker * `USERMOD_DHT_STATS` - For debug, report delay stats ## Project link @@ -29,13 +34,15 @@ If you are using `platformio_override.ini`, you should be able to refresh the ta ## Change Log - +2022-10-15 +* Add ability to publish sensor readings to an MQTT broker +* fix compilation error for sample [env:d1_mini_usermod_dht_C] task 2020-02-04 * Change default QuinLed pin to Q2 -* Instead of trying to keep updates at constant cadence, space readings out by measurement interval; hope this helps to avoid occasional bursts of readings with errors +* Instead of trying to keep updates at constant cadence, space out readings by measurement interval. Hopefully, this helps eliminate occasional bursts of readings with errors * Add some more (optional) stats 2020-02-03 * Due to poor readouts on ESP32 with previous DHT library, rewrote to use https://github.com/alwynallan/DHT_nonblocking -* The new library serializes/delays up to 5ms for the sensor readout -2020-02-02 +* The new library serializes/delays up to 5ms for the sensor readout +2020-02-02 * Created diff --git a/usermods/DHT/usermod_dht.h b/usermods/DHT/usermod_dht.h index 9f46734f83..05a7267b5f 100644 --- a/usermods/DHT/usermod_dht.h +++ b/usermods/DHT/usermod_dht.h @@ -1,6 +1,10 @@ #pragma once #include "wled.h" +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + #include @@ -45,7 +49,7 @@ #endif // how many seconds after boot to take first measurement, 90 seconds -// 90 gives enough time to OTA update firmware if this crashses +// 90 gives enough time to OTA update firmware if this crashes #ifndef USERMOD_DHT_FIRST_MEASUREMENT_AT #define USERMOD_DHT_FIRST_MEASUREMENT_AT 90000 #endif @@ -62,6 +66,10 @@ class UsermodDHT : public Usermod { float humidity, temperature = 0; bool initializing = true; bool disabled = false; + #ifdef USERMOD_DHT_MQTT + char dhtMqttTopic[64]; + size_t dhtMqttTopicLen; + #endif #ifdef USERMOD_DHT_STATS unsigned long nextResetStatsTime = 0; uint16_t updates = 0; @@ -76,6 +84,10 @@ class UsermodDHT : public Usermod { void setup() { nextReadTime = millis() + USERMOD_DHT_FIRST_MEASUREMENT_AT; lastReadTime = millis(); + #ifdef USERMOD_DHT_MQTT + sprintf(dhtMqttTopic, "%s/dht", mqttDeviceTopic); + dhtMqttTopicLen = strlen(dhtMqttTopic); + #endif #ifdef USERMOD_DHT_STATS nextResetStatsTime = millis() + 60*60*1000; #endif @@ -110,10 +122,29 @@ class UsermodDHT : public Usermod { temperature = tempC * 9 / 5 + 32; #endif + #ifdef USERMOD_DHT_MQTT + // 10^n where n is number of decimal places to display in mqtt message. Please adjust buff size together with this constant + #define FLOAT_PREC 100 + if (WLED_MQTT_CONNECTED) { + char buff[10]; + + strcpy(dhtMqttTopic + dhtMqttTopicLen, "/temperature"); + sprintf(buff, "%d.%d", (int)temperature, ((int)(temperature * FLOAT_PREC)) % FLOAT_PREC); + mqtt->publish(dhtMqttTopic, 0, false, buff); + + sprintf(buff, "%d.%d", (int)humidity, ((int)(humidity * FLOAT_PREC)) % FLOAT_PREC); + strcpy(dhtMqttTopic + dhtMqttTopicLen, "/humidity"); + mqtt->publish(dhtMqttTopic, 0, false, buff); + + dhtMqttTopic[dhtMqttTopicLen] = '\0'; + } + #undef FLOAT_PREC + #endif + nextReadTime = millis() + USERMOD_DHT_MEASUREMENT_INTERVAL; lastReadTime = millis(); initializing = false; - + #ifdef USERMOD_DHT_STATS unsigned long icalc = millis() - currentIteration; if (icalc > maxIteration) { @@ -134,7 +165,7 @@ class UsermodDHT : public Usermod { dcalc = millis() - dcalc; if (dcalc > maxDelay) { maxDelay = dcalc; - } + } #endif if (((millis() - lastReadTime) > 10*USERMOD_DHT_MEASUREMENT_INTERVAL)) { @@ -207,7 +238,7 @@ class UsermodDHT : public Usermod { temp.add("°F"); #endif } - + uint16_t getId() { return USERMOD_ID_DHT; diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index 48fb7cd8e8..659d3e58be 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -22,8 +22,12 @@ //class name. Use something descriptive and leave the ": public Usermod" part :) class MyExampleUsermod : public Usermod { + private: - //Private class members. You can declare variables and functions only accessible to your usermod here + + // Private class members. You can declare variables and functions only accessible to your usermod here + bool enabled = false; + bool initDone = false; unsigned long lastTime = 0; // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) @@ -37,15 +41,56 @@ class MyExampleUsermod : public Usermod { long testLong; int8_t testPins[2]; + // string that are used multiple time (this will save some flash memory) + static const char _name[]; + static const char _enabled[]; + + + // any private methods should go here (non-inline method should be defined out of class) + void publishMqtt(const char* state, bool retain = false); // example for publishing MQTT message + + public: - //Functions called by WLED + + // non WLED related methods, may be used for data exchange between usermods (non-inline methods should be defined out of class) + + /** + * Enable/Disable the usermod + */ + inline void enable(bool enable) { enabled = enable; } + + /** + * Get usermod enabled/disabled state + */ + inline bool isEnabled() { return enabled; } + + // in such case add the following to another usermod: + // in private vars: + // #ifdef USERMOD_EXAMPLE + // MyExampleUsermod* UM; + // #endif + // in setup() + // #ifdef USERMOD_EXAMPLE + // UM = (MyExampleUsermod*) usermods.lookup(USERMOD_ID_EXAMPLE); + // #endif + // somewhere in loop() or other member method + // #ifdef USERMOD_EXAMPLE + // if (UM != nullptr) isExampleEnabled = UM->isEnabled(); + // if (!isExampleEnabled) UM->enable(true); + // #endif + + + // methods called by WLED (can be inlined as they are called only once but if you call them explicitly define them out of class) /* * setup() is called once at boot. WiFi is not yet connected at this point. + * readFromConfig() is called prior to setup() * You can use it to initialize variables, sensors or similar. */ void setup() { + // do your set-up here //Serial.println("Hello from my usermod!"); + initDone = true; } @@ -69,6 +114,11 @@ class MyExampleUsermod : public Usermod { * Instead, use a timer check as shown here. */ void loop() { + // if usermod is disabled or called during strip updating just exit + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!enabled || strip.isUpdating()) return; + + // do your magic here if (millis() - lastTime > 1000) { //Serial.println("I'm alive!"); lastTime = millis(); @@ -81,19 +131,25 @@ class MyExampleUsermod : public Usermod { * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. * Below it is shown how this could be used for e.g. a light sensor */ - /* void addToJsonInfo(JsonObject& root) { - int reading = 20; - //this code adds "u":{"Light":[20," lux"]} to the info object + // if "u" object does not exist yet wee need to create it JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); - JsonArray lightArr = user.createNestedArray("Light"); //name - lightArr.add(reading); //value - lightArr.add(" lux"); //unit + //this code adds "u":{"ExampleUsermod":[20," lux"]} to the info object + //int reading = 20; + //JsonArray lightArr = user.createNestedArray(FPSTR(_name))); //name + //lightArr.add(reading); //value + //lightArr.add(F(" lux")); //unit + + // if you are implementing a sensor usermod, you may publish sensor data + //JsonObject sensor = root[F("sensor")]; + //if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); + //temp = sensor.createNestedArray(F("light")); + //temp.add(reading); + //temp.add(F("lux")); } - */ /* @@ -102,7 +158,12 @@ class MyExampleUsermod : public Usermod { */ void addToJsonState(JsonObject& root) { - //root["user0"] = userVar0; + if (!initDone || !enabled) return; // prevent crash on boot applyPreset() + + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) usermod = root.createNestedObject(FPSTR(_name)); + + //usermod["user0"] = userVar0; } @@ -112,7 +173,14 @@ class MyExampleUsermod : public Usermod { */ void readFromJsonState(JsonObject& root) { - userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + if (!initDone) return; // prevent crash on boot applyPreset() + + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) { + // expect JSON usermod data in usermod name object: {"ExampleUsermod:{"user0":10}"} + userVar0 = usermod["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + } + // you can as well check WLED state JSON keys //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); } @@ -154,8 +222,10 @@ class MyExampleUsermod : public Usermod { */ void addToConfig(JsonObject& root) { - JsonObject top = root.createNestedObject("exampleUsermod"); - top["great"] = userVar0; //save these vars persistently whenever settings are saved + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + //save these vars persistently whenever settings are saved + top["great"] = userVar0; top["testBool"] = testBool; top["testInt"] = testInt; top["testLong"] = testLong; @@ -188,7 +258,7 @@ class MyExampleUsermod : public Usermod { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) - JsonObject top = root["exampleUsermod"]; + JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); @@ -201,13 +271,106 @@ class MyExampleUsermod : public Usermod { // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing configComplete &= getJsonValue(top["testInt"], testInt, 42); configComplete &= getJsonValue(top["testLong"], testLong, -42424242); + + // "pin" fields have special handling in settings page (or some_pin as well) configComplete &= getJsonValue(top["pin"][0], testPins[0], -1); configComplete &= getJsonValue(top["pin"][1], testPins[1], -1); return configComplete; } - + + /* + * appendConfigData() is called when user enters usermod settings page + * it may add additional metadata for certain entry fields (adding drop down is possible) + * be careful not to add too much as oappend() buffer is limited to 3k + */ + void appendConfigData() + { + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":great")); oappend(SET_F("',1,'(this is a great config value)');")); + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":testString")); oappend(SET_F("',1,'enter any string you want');")); + oappend(SET_F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F("','testInt');")); + oappend(SET_F("addOption(dd,'Nothing',0);")); + oappend(SET_F("addOption(dd,'Everything',42);")); + } + + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() + { + //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + } + + + /** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ + bool handleButton(uint8_t b) { + 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) { + return false; + } + + bool handled = false; + // do your button handling here + return handled; + } + + +#ifndef WLED_DISABLE_MQTT + /** + * handling of MQTT message + * topic only contains stripped topic (part after /wled/MAC) + */ + bool onMqttMessage(char* topic, char* payload) { + // check if we received a command + //if (strlen(topic) == 8 && strncmp_P(topic, PSTR("/command"), 8) == 0) { + // String action = payload; + // if (action == "on") { + // enabled = true; + // return true; + // } else if (action == "off") { + // enabled = false; + // return true; + // } else if (action == "toggle") { + // enabled = !enabled; + // return true; + // } + //} + return false; + } + + /** + * onMqttConnect() is called when MQTT connection is established + */ + void onMqttConnect(bool sessionPresent) { + // do any MQTT related initialisation here + //publishMqtt("I am alive!"); + } +#endif + + + /** + * onStateChanged() is used to detect WLED state change + * @mode parameter is CALL_MODE_... parameter used for notifications + */ + void onStateChange(uint8_t mode) { + // do something if WLED state changed (color, brightness, effect, preset, etc) + } + + /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). * This could be used in the future for the system to determine whether your usermod is installed. @@ -219,4 +382,25 @@ class MyExampleUsermod : public Usermod { //More methods can be added in the future, this example will then be extended. //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! -}; \ No newline at end of file +}; + + +// add more strings here to reduce flash memory usage +const char MyExampleUsermod::_name[] PROGMEM = "ExampleUsermod"; +const char MyExampleUsermod::_enabled[] PROGMEM = "enabled"; + + +// implementation of non-inline member methods + +void MyExampleUsermod::publishMqtt(const char* state, bool retain) +{ +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED) { + char subuf[64]; + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/example")); + mqtt->publish(subuf, 0, retain, state); + } +#endif +} diff --git a/usermods/EleksTube_IPS/TFTs.h b/usermods/EleksTube_IPS/TFTs.h index 4c6bd9cb88..030ec23add 100644 --- a/usermods/EleksTube_IPS/TFTs.h +++ b/usermods/EleksTube_IPS/TFTs.h @@ -75,7 +75,7 @@ class TFTs : public TFT_eSPI { uint8_t lineBuffer[w * 2]; - if (!realtimeMode || realtimeOverride) strip.service(); + if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) strip.service(); // 0,0 coordinates are top left for (row = 0; row < h; row++) { @@ -133,13 +133,13 @@ class TFTs : public TFT_eSPI { return false; } - read32(bmpFS); // filesize in bytes - read32(bmpFS); // reserved + (void) read32(bmpFS); // filesize in bytes + (void) read32(bmpFS); // reserved seekOffset = read32(bmpFS); // start of bitmap headerSize = read32(bmpFS); // header size w = read32(bmpFS); // width h = read32(bmpFS); // height - read16(bmpFS); // color planes (must be 1) + (void) read16(bmpFS); // color planes (must be 1) bitDepth = read16(bmpFS); if (read32(bmpFS) != 0 || (bitDepth != 24 && bitDepth != 1 && bitDepth != 4 && bitDepth != 8)) { @@ -151,9 +151,9 @@ class TFTs : public TFT_eSPI { uint32_t palette[256]; if (bitDepth <= 8) // 1,4,8 bit bitmap: read color palette { - read32(bmpFS); read32(bmpFS); read32(bmpFS); // size, w resolution, h resolution + (void) read32(bmpFS); (void) read32(bmpFS); (void) read32(bmpFS); // size, w resolution, h resolution paletteSize = read32(bmpFS); - if (paletteSize == 0) paletteSize = bitDepth * bitDepth; //if 0, size is 2^bitDepth + if (paletteSize == 0) paletteSize = 1 << bitDepth; //if 0, size is 2^bitDepth bmpFS.seek(14 + headerSize); // start of color palette for (uint16_t i = 0; i < paletteSize; i++) { palette[i] = read32(bmpFS); @@ -169,7 +169,7 @@ class TFTs : public TFT_eSPI { uint32_t lineSize = ((bitDepth * w +31) >> 5) * 4; uint8_t lineBuffer[lineSize]; - uint8_t serviceStrip = (!realtimeMode || realtimeOverride) ? 7 : 0; + uint8_t serviceStrip = (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) ? 7 : 0; // row is decremented as the BMP image is drawn bottom up for (row = h-1; row >= 0; row--) { if ((row & 0b00000111) == serviceStrip) strip.service(); //still refresh backlight to mitigate stutter every few rows @@ -198,7 +198,7 @@ class TFTs : public TFT_eSPI { } b = c; g = c >> 8; r = c >> 16; } - if (dimming != 255) { // only dimm when needed + if (dimming != 255) { // only dim when needed r *= dimming; g *= dimming; b *= dimming; r = r >> 8; g = g >> 8; b = b >> 8; } @@ -250,7 +250,7 @@ class TFTs : public TFT_eSPI { uint8_t lineBuffer[w * 2]; - if (!realtimeMode || realtimeOverride) strip.service(); + if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) strip.service(); // 0,0 coordinates are top left for (row = 0; row < h; row++) { @@ -355,7 +355,7 @@ class TFTs : public TFT_eSPI { // Color in grayscale bitmaps if Segment 1 exists // TODO If secondary and tertiary are black, color all in primary, // else color first three from Seg 1 color slots and last three from Seg 2 color slots - WS2812FX::Segment& seg1 = strip.getSegment(tubeSegment); + Segment& seg1 = strip.getSegment(tubeSegment); if (seg1.isActive()) { digitColor = strip.getPixelColor(seg1.start + digit); dimming = seg1.opacity; diff --git a/usermods/EleksTube_IPS/readme.md b/usermods/EleksTube_IPS/readme.md index 87827ac4af..a05c934663 100644 --- a/usermods/EleksTube_IPS/readme.md +++ b/usermods/EleksTube_IPS/readme.md @@ -15,7 +15,7 @@ Not supported: - On-device setup with buttons (WiFi setup only) Your images must be 1-135 pixels wide and 1-240 pixels high. -For BMP, 1, 4, 8, and 24 bits per pixel formats are supported. +BMP 1, 4, 8, and 24 bits per pixel formats are supported. ## Installation @@ -26,11 +26,11 @@ Use LED pin 12, relay pin 27 and button pin 34. ## Use of RGB565 images -Binary 16-bit per pixel RGB565 format `.bin` and `.clk` images are now supported. This has the benefit of only using 2/3rds of the file size a 24 BPP `.bmp` has. -The drawback is that this format cannot be handled by common image programs and that an extra conversion step is needed. +Binary 16-bit per pixel RGB565 format `.bin` and `.clk` images are now supported. This has the benefit of using only 2/3rds of the file space a 24 BPP `.bmp` occupies. +The drawback is this format cannot be handled by common image programs and an extra conversion step is needed. You can use https://lvgl.io/tools/imageconverter to convert your .bmp to a .bin file (settings `True color` and `Binary RGB565`). Thank you to @RedNax67 for adding .bin and .clk support. -For most clockface designs, using 4 or 8 BPP BMP formats will save even more file size: +For most clockface designs, using 4 or 8 BPP BMP format will reduce file size even more: | Bits per pixel | File size in kB (for 135x240 img) | % of 24 BPP BMP | Max unique colors | --- | --- | --- | --- | @@ -42,4 +42,4 @@ For most clockface designs, using 4 or 8 BPP BMP formats will save even more fil Comparison 1 vs. 4 vs. 8 vs. 24 BPP. With this clockface on the actual clock, 4 bit looks good, and 8 bit is almost indistinguishable from 24 bit. -![comparison](https://user-images.githubusercontent.com/21045690/156899667-5b55ed9f-6e03-4066-b2aa-1260e9570369.png) \ No newline at end of file +![comparison](https://user-images.githubusercontent.com/21045690/156899667-5b55ed9f-6e03-4066-b2aa-1260e9570369.png) diff --git a/usermods/EleksTube_IPS/usermod_elekstube_ips.h b/usermods/EleksTube_IPS/usermod_elekstube_ips.h index 06c6ecc894..0f7d92e7e9 100644 --- a/usermods/EleksTube_IPS/usermod_elekstube_ips.h +++ b/usermods/EleksTube_IPS/usermod_elekstube_ips.h @@ -63,7 +63,7 @@ class ElekstubeIPSUsermod : public Usermod { if (!toki.isTick()) return; updateLocalTime(); - WS2812FX::Segment& seg1 = strip.getSegment(tfts.tubeSegment); + Segment& seg1 = strip.getSegment(tfts.tubeSegment); if (seg1.isActive()) { bool update = false; if (seg1.opacity != lastBri) update = true; diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/readme.md b/usermods/Enclosure_with_OLED_temp_ESP07/readme.md index 94d1c1f2bb..d612e06eb1 100644 --- a/usermods/Enclosure_with_OLED_temp_ESP07/readme.md +++ b/usermods/Enclosure_with_OLED_temp_ESP07/readme.md @@ -10,7 +10,7 @@ For BME280 sensor use usermod_bme280.cpp. Copy to wled00 and rename to usermod.c ## Features - SSD1306 128x32 and 128x64 I2C OLED display - On screen IP address, SSID and controller status (e.g. ON or OFF, recent effect) -- Auto display shutoff for saving display lifetime +- Auto display shutoff for extending display lifetime - Dallas temperature sensor - Reporting temperature to MQTT broker @@ -39,15 +39,15 @@ default_envs = esp07 ... lib_deps_external = ... - #For use SSD1306 OLED display uncomment following + #To use the SSD1306 OLED display, uncomment following U8g2@~2.27.3 - #For Dallas sensor uncomment following 2 lines + #For Dallas sensor, uncomment the following 2 lines DallasTemperature@~3.8.0 OneWire@~2.3.5 ... ``` -For BME280 sensor uncomment `U8g2@~2.27.3`,`BME280@~3.0.0 under` `[common]` section in `platformio.ini`: +For BME280 sensor, uncomment `U8g2@~2.27.3`,`BME280@~3.0.0 under` `[common]` section in `platformio.ini`: ```ini # platformio.ini ... @@ -60,7 +60,7 @@ default_envs = esp07 ... lib_deps_external = ... - #For use SSD1306 OLED display uncomment following + #To use the SSD1306 OLED display, uncomment following U8g2@~2.27.3 #For BME280 sensor uncomment following BME280@~3.0.0 diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp b/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp index 36d322fa0d..823ad74724 100644 --- a/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp +++ b/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp @@ -1,3 +1,7 @@ +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + #include "wled.h" #include #include // from https://github.com/olikraus/u8g2/ @@ -11,23 +15,23 @@ OneWire oneWire(13); DallasTemperature sensor(&oneWire); long temptimer = millis(); long lastMeasure = 0; -#define Celsius // Show temperature mesaurement in Celcius otherwise is in Fahrenheit +#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit // If display does not work or looks corrupted check the // constructor reference: // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp // or check the gallery: // https://github.com/olikraus/u8g2/wiki/gallery -// --> First choise of cheap I2C OLED 128X32 0.91" +// --> First choice of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Second choise of cheap I2C OLED 128X64 0.96" or 1.3" +// --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // gets called once at boot. Do all initialization that doesn't depend on // network here void userSetup() { sensor.begin(); //Start Dallas temperature sensor u8x8.begin(); - //u8x8.setFlipMode(1); //Uncoment if using WLED Wemos shield + //u8x8.setFlipMode(1); //Un-comment if using WLED Wemos shield u8x8.setPowerSave(0); u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 u8x8.setFont(u8x8_font_chroma48medium8_r); @@ -67,7 +71,7 @@ void userLoop() { if (mqtt != nullptr) { sensor.requestTemperatures(); -//Gets prefered temperature scale based on selection in definitions section +//Gets preferred temperature scale based on selection in definitions section #ifdef Celsius float board_temperature = sensor.getTempCByIndex(0); #else @@ -134,11 +138,11 @@ void userLoop() { // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); - // Print `~` char to indicate that SSID is longer, than owr dicplay + // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); - // Second row with IP or Psssword + // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) @@ -148,58 +152,14 @@ void userLoop() { // Third row with mode name u8x8.setCursor(2, 2); - uint8_t qComma = 0; - bool insideQuotes = false; - uint8_t printedChars = 0; - char singleJsonSymbol; - - // Find the mode name in JSON - for (size_t i = 0; i < strlen_P(JSON_mode_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_mode_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownMode)) - break; - u8x8.print(singleJsonSymbol); - printedChars++; - } - if ((qComma > knownMode) || (printedChars > u8x8.getCols() - 2)) - break; - } + char lineBuffer[17]; + extractModeName(knownMode, JSON_mode_names, lineBuffer, 16); + u8x8.print(lineBuffer); + // Fourth row with palette name u8x8.setCursor(2, 3); - qComma = 0; - insideQuotes = false; - printedChars = 0; - // Looking for palette name in JSON. - for (size_t i = 0; i < strlen_P(JSON_palette_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_palette_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownPalette)) - break; - u8x8.print(singleJsonSymbol); - printedChars++; - } - if ((qComma > knownMode) || (printedChars > u8x8.getCols() - 2)) - break; - } + extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16); + u8x8.print(lineBuffer); u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); u8x8.drawGlyph(0, 0, 80); // wifi icon diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp b/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp index 5873128321..29a4332dbd 100644 --- a/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp +++ b/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp @@ -1,3 +1,7 @@ +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + #include "wled.h" #include #include // from https://github.com/olikraus/u8g2/ @@ -6,7 +10,7 @@ void UpdateBME280Data(); -#define Celsius // Show temperature mesaurement in Celcius otherwise is in Fahrenheit +#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit BME280I2C bme; // Default : forced mode, standby time = 1000 ms // Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off, @@ -16,14 +20,14 @@ uint8_t SDA_PIN = 21; #else //ESP8266 boards uint8_t SCL_PIN = 5; uint8_t SDA_PIN = 4; -// uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 +// uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif //The SCL and SDA pins are defined here. //ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21 #define U8X8_PIN_SCL SCL_PIN #define U8X8_PIN_SDA SDA_PIN -//#define U8X8_PIN_RESET RST_PIN // Uncoment for Heltec WiFi-Kit-8 +//#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8 // If display does not work or looks corrupted check the // constructor reference: @@ -32,9 +36,9 @@ uint8_t SDA_PIN = 4; // https://github.com/olikraus/u8g2/wiki/gallery // --> First choise of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Second choise of cheap I2C OLED 128X64 0.96" or 1.3" +// --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Third choise of Heltec WiFi-Kit-8 OLED 128X32 0.91" +// --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91" //U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8 // gets called once at boot. Do all initialization that doesn't depend on network here @@ -177,11 +181,11 @@ void userLoop() { // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); - // Print `~` char to indicate that SSID is longer, than owr dicplay + // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); - // Second row with IP or Psssword + // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) @@ -191,58 +195,14 @@ void userLoop() { // Third row with mode name u8x8.setCursor(2, 2); - uint8_t qComma = 0; - bool insideQuotes = false; - uint8_t printedChars = 0; - char singleJsonSymbol; + char lineBuffer[17]; + extractModeName(knownMode, JSON_mode_names, lineBuffer, 16); + u8x8.print(lineBuffer); - // Find the mode name in JSON - for (size_t i = 0; i < strlen_P(JSON_mode_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_mode_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownMode)) - break; - u8x8.print(singleJsonSymbol); - printedChars++; - } - if ((qComma > knownMode) || (printedChars > u8x8.getCols() - 2)) - break; - } // Fourth row with palette name u8x8.setCursor(2, 3); - qComma = 0; - insideQuotes = false; - printedChars = 0; - // Looking for palette name in JSON. - for (size_t i = 0; i < strlen_P(JSON_palette_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_palette_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownPalette)) - break; - u8x8.print(singleJsonSymbol); - printedChars++; - } - if ((qComma > knownMode) || (printedChars > u8x8.getCols() - 2)) - break; - } + extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16); + u8x8.print(lineBuffer); u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); u8x8.drawGlyph(0, 0, 80); // wifi icon diff --git a/usermods/Fix_unreachable_netservices_v2/readme.md b/usermods/Fix_unreachable_netservices_v2/readme.md index f7b721dd88..006eaf9f94 100644 --- a/usermods/Fix_unreachable_netservices_v2/readme.md +++ b/usermods/Fix_unreachable_netservices_v2/readme.md @@ -2,15 +2,16 @@ **Attention: This usermod compiles only for ESP8266** -This usermod-v2 modification performs a ping request to the local IP address every 60 seconds. By this procedure the net services of WLED remains accessible in some problematic WLAN environments. +This usermod-v2 modification performs a ping request to a local IP address every 60 seconds. This ensures WLED net services remain accessible in some problematic WiFi environments. The modification works with static or DHCP IP address configuration. _Story:_ -Unfortunately, with all ESP projects where a web server or other network services are running, I have the problem that after some time the web server is no longer accessible. Now I found out that the connection is at least reestablished when a ping request is executed by the device. +Unfortunately, with many ESP projects where a web server or other network services are running, after some time, the connecton to the web server is lost. +The connection can be reestablished with a ping request from the device. -With this modification, in the worst case, the network functions are not available for 60 seconds until the next ping request. +With this modification, in the worst case, the network functions are not available until the next ping request. (60 seconds) ## Webinterface @@ -23,7 +24,7 @@ The usermod supports the following state changes: | JSON key | Value range | Description | |-------------|------------------|---------------------------------| -| PingDelayMs | 5000 to 18000000 | Deactivdate/activate the sensor | +| PingDelayMs | 5000 to 18000000 | Deactivate/activate the sensor | Changes also persist after a reboot. diff --git a/usermods/Internal_Temperature_v2/readme.md b/usermods/Internal_Temperature_v2/readme.md new file mode 100644 index 0000000000..58a9e19392 --- /dev/null +++ b/usermods/Internal_Temperature_v2/readme.md @@ -0,0 +1,17 @@ +# Internal Temperature Usermod +This usermod adds the temperature readout to the Info tab and also publishes that over the topic `mcutemp` topic. + +## Important +A shown temp of 53,33°C might indicate that the internal temp is not supported. + +ESP8266 does not have a internal temp sensor + +ESP32S2 seems to crash on reading the sensor -> disabled + +## Installation +Add a build flag `-D USERMOD_INTERNAL_TEMPERATURE` to your `platformio.ini` (or `platformio_override.ini`). + +## Authors +Soeren Willrodt [@lost-hope](https://github.com/lost-hope) + +Dimitry Zhemkov [@dima-zhemkov](https://github.com/dima-zhemkov) \ No newline at end of file diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h new file mode 100644 index 0000000000..3989e76681 --- /dev/null +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -0,0 +1,117 @@ +#pragma once + +#include "wled.h" + +class InternalTemperatureUsermod : public Usermod +{ + +private: + unsigned long loopInterval = 10000; + unsigned long lastTime = 0; + bool isEnabled = false; + float temperature = 0; + + static const char _name[]; + static const char _enabled[]; + static const char _loopInterval[]; + + // any private methods should go here (non-inline method should be defined out of class) + void publishMqtt(const char *state, bool retain = false); // example for publishing MQTT message + +public: + void setup() + { + } + + void loop() + { + // if usermod is disabled or called during strip updating just exit + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!isEnabled || strip.isUpdating() || millis() - lastTime <= loopInterval) + return; + + lastTime = millis(); + +#ifdef ESP8266 // ESP8266 + // does not seem possible + temperature = -1; +#elif defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32S2 + temperature = -1; +#else // ESP32 ESP32S3 and ESP32C3 + temperature = roundf(temperatureRead() * 10) / 10; +#endif + +#ifndef WLED_DISABLE_MQTT + if (WLED_MQTT_CONNECTED) + { + char array[10]; + snprintf(array, sizeof(array), "%f", temperature); + publishMqtt(array); + } +#endif + } + + void addToJsonInfo(JsonObject &root) + { + if (!isEnabled) + return; + + // if "u" object does not exist yet wee need to create it + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + + JsonArray userTempArr = user.createNestedArray(FPSTR(_name)); + userTempArr.add(temperature); + userTempArr.add(F(" °C")); + + // if "sensor" object does not exist yet wee need to create it + JsonObject sensor = root[F("sensor")]; + if (sensor.isNull()) + sensor = root.createNestedObject(F("sensor")); + + JsonArray sensorTempArr = sensor.createNestedArray(FPSTR(_name)); + sensorTempArr.add(temperature); + sensorTempArr.add(F("°C")); + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = isEnabled; + top[FPSTR(_loopInterval)] = loopInterval; + } + + bool readFromConfig(JsonObject &root) + { + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top[FPSTR(_enabled)], isEnabled); + configComplete &= getJsonValue(top[FPSTR(_loopInterval)], loopInterval); + + return configComplete; + } + + uint16_t getId() + { + return USERMOD_ID_INTERNAL_TEMPERATURE; + } +}; + +const char InternalTemperatureUsermod::_name[] PROGMEM = "Internal Temperature"; +const char InternalTemperatureUsermod::_enabled[] PROGMEM = "Enabled"; +const char InternalTemperatureUsermod::_loopInterval[] PROGMEM = "Loop Interval"; + +void InternalTemperatureUsermod::publishMqtt(const char *state, bool retain) +{ +#ifndef WLED_DISABLE_MQTT + // Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED) + { + char subuf[64]; + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/mcutemp")); + mqtt->publish(subuf, 0, retain, state); + } +#endif +} \ No newline at end of file diff --git a/usermods/JSON_IR_remote/readme.md b/usermods/JSON_IR_remote/readme.md index 2cba06ed00..43532a6f7e 100644 --- a/usermods/JSON_IR_remote/readme.md +++ b/usermods/JSON_IR_remote/readme.md @@ -2,8 +2,8 @@ ## Purpose -The JSON IR remote allows users to customize IR remote behavior without writing custom code and compiling. -It also enables using any remote that is compatible with your IR receiver. Using the JSON IR remote, you can +The JSON IR remote enables users to customize IR remote behavior without writing custom code and compiling. +It also allows using any remote compatible with your IR receiver. Using the JSON IR remote, you can map buttons from any remote to any HTTP request API or JSON API command. ## Usage diff --git a/usermods/LDR_Dusk_Dawn_v2/README.md b/usermods/LDR_Dusk_Dawn_v2/README.md new file mode 100644 index 0000000000..5e33518a9d --- /dev/null +++ b/usermods/LDR_Dusk_Dawn_v2/README.md @@ -0,0 +1,26 @@ +# LDR_Dusk_Dawn_v2 +This usermod will obtain readings from a Light Dependent Resistor (LDR) and will turn on/off specific presets based on those readings. This is useful for exterior lighting situations where you want the lights to only be on when it is dark out. + +# Installation +Add "-D USERMOD_LDR_DUSK_DAWN" to your platformio.ini [common] build_flags and build. + +Example: +``` +[common] +build_flags = + -D USERMOD_LDR_DUSK_DAWN # Enable LDR Dusk Dawn Usermod +``` + +# Usermod Settings +Setting | Description | Default +--- | --- | --- +Enabled | Enable/Disable the LDR functionality. | Disabled +LDR Pin | The analog capable pin your LDR is connected to. | 34 +Threshold Minutes | The number of minutes of consistent readings above/below the on/off threshold before the LED state will change. | 5 +Threshold | The analog read value threshold from the LDR. Readings lower than this number will count towards changing the LED state to off. You can see the current LDR reading by going into the info section when LDR functionality is enabled. | 1000 +On Preset | The WLED preset to be used for the LED on state. | 1 +Off Preset | The WLED preset to be used for the LED off state. | 2 + +## Author +[@jeffwdh](https://github.com/jeffwdh) +jeffwdh@tarball.ca diff --git a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h new file mode 100644 index 0000000000..393fc22327 --- /dev/null +++ b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h @@ -0,0 +1,153 @@ +#pragma once +#include "wled.h" + +#ifndef ARDUINO_ARCH_ESP32 + // 8266 does not support analogRead on user selectable pins + #error only ESP32 is supported by usermod LDR_DUSK_DAWN +#endif + +class LDR_Dusk_Dawn_v2 : public Usermod { + private: + // Defaults + bool ldrEnabled = false; + int ldrPin = 34; //A2 on Adafruit Huzzah32 + int ldrThresholdMinutes = 5; // How many minutes of readings above/below threshold until it switches LED state + int ldrThreshold = 1000; // Readings higher than this number will turn off LED. + int ldrOnPreset = 1; // Default "On" Preset + int ldrOffPreset = 2; // Default "Off" Preset + + // Variables + bool initDone = false; + bool ldrEnabledPreviously = false; // Was LDR enabled for the previous check? First check is always no. + int ldrOffCount; // Number of readings above the threshold + int ldrOnCount; // Number of readings below the threshold + int ldrReading = 0; // Last LDR reading + int ldrLEDState; // Current LED on/off state + unsigned long lastMillis = 0; + static const char _name[]; + + public: + void setup() { + // register ldrPin + if ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)) { + if(!pinManager.allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod + else pinMode(ldrPin, INPUT); // alloc success -> configure pin for input + } else ldrEnabled = false; // invalid pin -> disable usermod + initDone = true; + } + + void loop() { + // Only update every 10 seconds + if (millis() - lastMillis > 10000) { + if ( (ldrEnabled == true) + && (ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0) ) { // make sure that pin is valid for analogread() + // Default state is off + if (ldrEnabledPreviously == false) { + applyPreset(ldrOffPreset); + ldrEnabledPreviously = true; + ldrLEDState = 0; + } + + // Get LDR reading and increment counter by number of seconds since last read + ldrReading = analogRead(ldrPin); + if (ldrReading <= ldrThreshold) { + ldrOnCount = ldrOnCount + 10; + ldrOffCount = 0; + } else { + ldrOffCount = ldrOffCount + 10; + ldrOnCount = 0; + } + + if (ldrOnCount >= (ldrThresholdMinutes * 60)) { + ldrOnCount = 0; + // If LEDs were previously off, turn on + if (ldrLEDState == 0) { + applyPreset(ldrOnPreset); + ldrLEDState = 1; + } + } + + if (ldrOffCount >= (ldrThresholdMinutes * 60)) { + ldrOffCount = 0; + // If LEDs were previously on, turn off + if (ldrLEDState == 1) { + applyPreset(ldrOffPreset); + ldrLEDState = 0; + } + } + } else { + // LDR is disabled, reset variables to default + ldrReading = 0; + ldrOnCount = 0; + ldrOffCount = 0; + ldrLEDState = 0; + ldrEnabledPreviously = false; + } + lastMillis = millis(); + } + } + + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top["Enabled"] = ldrEnabled; + top["LDR Pin"] = ldrPin; + top["Threshold Minutes"] = ldrThresholdMinutes; + top["Threshold"] = ldrThreshold; + top["On Preset"] = ldrOnPreset; + top["Off Preset"] = ldrOffPreset; + } + + bool readFromConfig(JsonObject& root) { + int8_t oldLdrPin = ldrPin; + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top["Enabled"], ldrEnabled); + configComplete &= getJsonValue(top["LDR Pin"], ldrPin); + configComplete &= getJsonValue(top["Threshold Minutes"], ldrThresholdMinutes); + configComplete &= getJsonValue(top["Threshold"], ldrThreshold); + configComplete &= getJsonValue(top["On Preset"], ldrOnPreset); + configComplete &= getJsonValue(top["Off Preset"], ldrOffPreset); + + if (initDone && (ldrPin != oldLdrPin)) { + // pin changed - un-register previous pin, register new pin + if (oldLdrPin >= 0) pinManager.deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN); + setup(); // setup new pin + } + return configComplete; + } + + void addToJsonInfo(JsonObject& root) { + // If "u" object does not exist yet we need to create it + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray LDR_Enabled = user.createNestedArray("LDR dusk/dawn enabled"); + LDR_Enabled.add(ldrEnabled); + if (!ldrEnabled) return; // do not add more if usermod is disabled + + JsonArray LDR_Reading = user.createNestedArray("LDR reading"); + LDR_Reading.add(ldrReading); + + JsonArray LDR_State = user.createNestedArray("LDR turned LEDs on"); + LDR_State.add(bool(ldrLEDState)); + + // Optional debug information: + //JsonArray LDR_On_Count = user.createNestedArray("LDR on count"); + //LDR_On_Count.add(ldrOnCount); + + //JsonArray LDR_Off_Count = user.createNestedArray("LDR off count"); + //LDR_Off_Count.add(ldrOffCount); + + //bool pinValid = ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)); + //if (pinManager.getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false; + //JsonArray LDR_valid = user.createNestedArray(F("LDR pin")); + //LDR_valid.add(ldrPin); + //LDR_valid.add(pinValid ? F(" OK"): F(" invalid")); + } + + uint16_t getId() { + return USERMOD_ID_LDR_DUSK_DAWN; + } +}; + +const char LDR_Dusk_Dawn_v2::_name[] PROGMEM = "LDR_Dusk_Dawn_v2"; diff --git a/usermods/MY9291/MY92xx.h b/usermods/MY9291/MY92xx.h new file mode 100644 index 0000000000..658852b446 --- /dev/null +++ b/usermods/MY9291/MY92xx.h @@ -0,0 +1,321 @@ +/* + +MY92XX LED Driver for Arduino +Based on the C driver by MaiKe Labs + +Copyright (c) 2016 - 2026 MaiKe Labs +Copyright (C) 2017 - 2018 Xose Pérez for the Arduino compatible library + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#ifndef _my92xx_h +#define _my92xx_h + +#include + +#ifdef DEBUG_MY92XX +#if ARDUINO_ARCH_ESP8266 +#define DEBUG_MSG_MY92XX(...) DEBUG_MY92XX.printf( __VA_ARGS__ ) +#elif ARDUINO_ARCH_AVR +#define DEBUG_MSG_MY92XX(...) { char buffer[80]; snprintf(buffer, sizeof(buffer), __VA_ARGS__ ); DEBUG_MY92XX.print(buffer); } +#endif +#else +#define DEBUG_MSG_MY92XX(...) +#endif + +typedef enum my92xx_model_t { + MY92XX_MODEL_MY9291 = 0X00, + MY92XX_MODEL_MY9231 = 0X01, +} my92xx_model_t; + +typedef enum my92xx_cmd_one_shot_t { + MY92XX_CMD_ONE_SHOT_DISABLE = 0X00, + MY92XX_CMD_ONE_SHOT_ENFORCE = 0X01, +} my92xx_cmd_one_shot_t; + +typedef enum my92xx_cmd_reaction_t { + MY92XX_CMD_REACTION_FAST = 0X00, + MY92XX_CMD_REACTION_SLOW = 0X01, +} my92xx_cmd_reaction_t; + +typedef enum my92xx_cmd_bit_width_t { + MY92XX_CMD_BIT_WIDTH_16 = 0X00, + MY92XX_CMD_BIT_WIDTH_14 = 0X01, + MY92XX_CMD_BIT_WIDTH_12 = 0X02, + MY92XX_CMD_BIT_WIDTH_8 = 0X03, +} my92xx_cmd_bit_width_t; + +typedef enum my92xx_cmd_frequency_t { + MY92XX_CMD_FREQUENCY_DIVIDE_1 = 0X00, + MY92XX_CMD_FREQUENCY_DIVIDE_4 = 0X01, + MY92XX_CMD_FREQUENCY_DIVIDE_16 = 0X02, + MY92XX_CMD_FREQUENCY_DIVIDE_64 = 0X03, +} my92xx_cmd_frequency_t; + +typedef enum my92xx_cmd_scatter_t { + MY92XX_CMD_SCATTER_APDM = 0X00, + MY92XX_CMD_SCATTER_PWM = 0X01, +} my92xx_cmd_scatter_t; + +typedef struct { + my92xx_cmd_scatter_t scatter : 1; + my92xx_cmd_frequency_t frequency : 2; + my92xx_cmd_bit_width_t bit_width : 2; + my92xx_cmd_reaction_t reaction : 1; + my92xx_cmd_one_shot_t one_shot : 1; + unsigned char resv : 1; +} __attribute__((aligned(1), packed)) my92xx_cmd_t; + +#define MY92XX_COMMAND_DEFAULT { \ + .scatter = MY92XX_CMD_SCATTER_APDM, \ + .frequency = MY92XX_CMD_FREQUENCY_DIVIDE_1, \ + .bit_width = MY92XX_CMD_BIT_WIDTH_8, \ + .reaction = MY92XX_CMD_REACTION_FAST, \ + .one_shot = MY92XX_CMD_ONE_SHOT_DISABLE, \ + .resv = 0 \ +} + +class my92xx { + +public: + + my92xx(my92xx_model_t model, unsigned char chips, unsigned char di, unsigned char dcki, my92xx_cmd_t command); + unsigned char getChannels(); + void setChannel(unsigned char channel, unsigned int value); + unsigned int getChannel(unsigned char channel); + void setState(bool state); + bool getState(); + void update(); + +private: + + void _di_pulse(unsigned int times); + void _dcki_pulse(unsigned int times); + void _set_cmd(my92xx_cmd_t command); + void _send(); + void _write(unsigned int data, unsigned char bit_length); + + my92xx_cmd_t _command; + my92xx_model_t _model = MY92XX_MODEL_MY9291; + unsigned char _chips = 1; + unsigned char _channels; + uint16_t* _value; + bool _state = false; + unsigned char _pin_di; + unsigned char _pin_dcki; + + +}; + + +#if ARDUINO_ARCH_ESP8266 + +extern "C" { + void os_delay_us(unsigned int); +} + +#elif ARDUINO_ARCH_AVR + +#define os_delay_us delayMicroseconds + +#endif + +void my92xx::_di_pulse(unsigned int times) { + for (unsigned int i = 0; i < times; i++) { + digitalWrite(_pin_di, HIGH); + digitalWrite(_pin_di, LOW); + } +} + +void my92xx::_dcki_pulse(unsigned int times) { + for (unsigned int i = 0; i < times; i++) { + digitalWrite(_pin_dcki, HIGH); + digitalWrite(_pin_dcki, LOW); + } +} + +void my92xx::_write(unsigned int data, unsigned char bit_length) { + + unsigned int mask = (0x01 << (bit_length - 1)); + + for (unsigned int i = 0; i < bit_length / 2; i++) { + digitalWrite(_pin_dcki, LOW); + digitalWrite(_pin_di, (data & mask) ? HIGH : LOW); + digitalWrite(_pin_dcki, HIGH); + data = data << 1; + digitalWrite(_pin_di, (data & mask) ? HIGH : LOW); + digitalWrite(_pin_dcki, LOW); + digitalWrite(_pin_di, LOW); + data = data << 1; + } + +} + +void my92xx::_set_cmd(my92xx_cmd_t command) { + + // ets_intr_lock(); + + // TStop > 12us. + os_delay_us(12); + + // Send 12 DI pulse, after 6 pulse's falling edge store duty data, and 12 + // pulse's rising edge convert to command mode. + _di_pulse(12); + + // Delay >12us, begin send CMD data + os_delay_us(12); + + // Send CMD data + unsigned char command_data = *(unsigned char*)(&command); + for (unsigned char i = 0; i < _chips; i++) { + _write(command_data, 8); + } + + // TStart > 12us. Delay 12 us. + os_delay_us(12); + + // Send 16 DI pulse,at 14 pulse's falling edge store CMD data, and + // at 16 pulse's falling edge convert to duty mode. + _di_pulse(16); + + // TStop > 12us. + os_delay_us(12); + + // ets_intr_unlock(); + +} + +void my92xx::_send() { + +#ifdef DEBUG_MY92XX + DEBUG_MSG_MY92XX("[MY92XX] Refresh: %s (", _state ? "ON" : "OFF"); + for (unsigned char channel = 0; channel < _channels; channel++) { + DEBUG_MSG_MY92XX(" %d", _value[channel]); + } + DEBUG_MSG_MY92XX(" )\n"); +#endif + + unsigned char bit_length = 8; + switch (_command.bit_width) { + case MY92XX_CMD_BIT_WIDTH_16: + bit_length = 16; + break; + case MY92XX_CMD_BIT_WIDTH_14: + bit_length = 14; + break; + case MY92XX_CMD_BIT_WIDTH_12: + bit_length = 12; + break; + case MY92XX_CMD_BIT_WIDTH_8: + bit_length = 8; + break; + default: + bit_length = 8; + break; + } + + // ets_intr_lock(); + + // TStop > 12us. + os_delay_us(12); + + // Send color data + for (unsigned char channel = 0; channel < _channels; channel++) { + _write(_state ? _value[channel] : 0, bit_length); + } + + // TStart > 12us. Ready for send DI pulse. + os_delay_us(12); + + // Send 8 DI pulse. After 8 pulse falling edge, store old data. + _di_pulse(8); + + // TStop > 12us. + os_delay_us(12); + + // ets_intr_unlock(); + +} + +// ----------------------------------------------------------------------------- + +unsigned char my92xx::getChannels() { + return _channels; +} + +void my92xx::setChannel(unsigned char channel, unsigned int value) { + if (channel < _channels) { + _value[channel] = value; + } +} + +unsigned int my92xx::getChannel(unsigned char channel) { + if (channel < _channels) { + return _value[channel]; + } + return 0; +} + +bool my92xx::getState() { + return _state; +} + +void my92xx::setState(bool state) { + _state = state; +} + +void my92xx::update() { + _send(); +} + +// ----------------------------------------------------------------------------- + +my92xx::my92xx(my92xx_model_t model, unsigned char chips, unsigned char di, unsigned char dcki, my92xx_cmd_t command) : _command(command) { + + _model = model; + _chips = chips; + _pin_di = di; + _pin_dcki = dcki; + + // Init channels + if (_model == MY92XX_MODEL_MY9291) { + _channels = 4 * _chips; + } + else if (_model == MY92XX_MODEL_MY9231) { + _channels = 3 * _chips; + } + _value = new uint16_t[_channels]; + for (unsigned char i = 0; i < _channels; i++) { + _value[i] = 0; + } + + // Init GPIO + pinMode(_pin_di, OUTPUT); + pinMode(_pin_dcki, OUTPUT); + digitalWrite(_pin_di, LOW); + digitalWrite(_pin_dcki, LOW); + + // Clear all duty register + _dcki_pulse(32 * _chips); + + // Send init command + _set_cmd(command); + + DEBUG_MSG_MY92XX("[MY92XX] Initialized\n"); + +} + +#endif \ No newline at end of file diff --git a/usermods/MY9291/usermode_MY9291.h b/usermods/MY9291/usermode_MY9291.h new file mode 100644 index 0000000000..66bbc34cbc --- /dev/null +++ b/usermods/MY9291/usermode_MY9291.h @@ -0,0 +1,45 @@ +#pragma once + +#include "wled.h" +#include "MY92xx.h" + +#define MY92XX_MODEL MY92XX_MODEL_MY9291 +#define MY92XX_CHIPS 1 +#define MY92XX_DI_PIN 13 +#define MY92XX_DCKI_PIN 15 + +#define MY92XX_RED 0 +#define MY92XX_GREEN 1 +#define MY92XX_BLUE 2 +#define MY92XX_WHITE 3 + +class MY9291Usermod : public Usermod { + private: + my92xx _my92xx = my92xx(MY92XX_MODEL, MY92XX_CHIPS, MY92XX_DI_PIN, MY92XX_DCKI_PIN, MY92XX_COMMAND_DEFAULT); + + public: + + void setup() { + _my92xx.setState(true); + } + + void connected() { + } + + void loop() { + uint32_t c = strip.getPixelColor(0); + int w = ((c >> 24) & 0xff) * bri / 255.0; + int r = ((c >> 16) & 0xff) * bri / 255.0; + int g = ((c >> 8) & 0xff) * bri / 255.0; + int b = (c & 0xff) * bri / 255.0; + _my92xx.setChannel(MY92XX_RED, r); + _my92xx.setChannel(MY92XX_GREEN, g); + _my92xx.setChannel(MY92XX_BLUE, b); + _my92xx.setChannel(MY92XX_WHITE, w); + _my92xx.update(); + } + + uint16_t getId() { + return USERMOD_ID_MY9291; + } +}; \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index 15a8db0804..85a5a74c08 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -7,71 +7,35 @@ _Story:_ I use the PIR Sensor to automatically turn on the WLED analog clock in my home office room when I am there. The LED strip is switched [using a relay](https://github.com/Aircoookie/WLED/wiki/Control-a-relay-with-WLED) to keep the power consumption low when it is switched off. -## Webinterface +## Web interface The info page in the web interface shows the remaining time of the off timer. Usermod can also be temporarily disbled/enabled from the info page by clicking PIR button. ## Sensor connection -My setup uses an HC-SR501 or HC-SR602 sensor, a HC-SR505 should also work. +My setup uses an HC-SR501 or HC-SR602 sensor, an HC-SR505 should also work. -The usermod uses GPIO13 (D1 mini pin D7) by default for the sensor signal but can be changed in the Usermod settings page. +The usermod uses GPIO13 (D1 mini pin D7) by default for the sensor signal, but can be changed in the Usermod settings page. [This example page](http://www.esp8266learning.com/wemos-mini-pir-sensor-example.php) describes how to connect the sensor. -Use the potentiometers on the sensor to set the time-delay to the minimum and the sensitivity to about half, or slightly above. +Use the potentiometers on the sensor to set the time delay to the minimum and the sensitivity to about half, or slightly above. You can also use usermod's off timer instead of sensor's. In such case rotate the potentiometer to its shortest time possible (or use SR602 which lacks such potentiometer). ## Usermod installation -1. Copy the file `usermod_PIR_sensor_switch.h` to the `wled00` directory. -2. Register the usermod by adding `#include "usermod_PIR_sensor_switch.h"` in the top and `registerUsermod(new PIRsensorSwitch());` in the bottom of `usermods_list.cpp`. +**NOTE:** Usermod has been included in master branch of WLED so it can be compiled in directly just by defining `-D USERMOD_PIRSWITCH` and optionally `-D PIR_SENSOR_PIN=16` to override default pin. You can also change the default off time by adding `-D PIR_SENSOR_OFF_SEC=30`. -Example **usermods_list.cpp**: - -```cpp -#include "wled.h" -/* - * Register your v2 usermods here! - * (for v1 usermods using just usermod.cpp, you can ignore this file) - */ - -/* - * Add/uncomment your usermod filename here (and once more below) - * || || || - * \/ \/ \/ - */ -//#include "usermod_v2_example.h" -//#include "usermod_temperature.h" -//#include "usermod_v2_empty.h" -#include "usermod_PIR_sensor_switch.h" - -void registerUsermods() -{ - /* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //usermods.add(new MyExampleUsermod()); - //usermods.add(new UsermodTemperature()); - //usermods.add(new UsermodRenameMe()); - usermods.add(new PIRsensorSwitch()); - -} -``` - -**NOTE:** Usermod has been included in master branch of WLED so it can be compiled in directly just by defining `-D USERMOD_PIRSWITCH` and optionaly `-D PIR_SENSOR_PIN=16` to override default pin. - -## API to enable/disable the PIR sensor from outside. For example from another usermod. +## API to enable/disable the PIR sensor from outside. For example from another usermod: To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. When the PIR sensor state changes an MQTT message is broadcasted with topic `wled/deviceMAC/motion` and message `on` or `off`. -Usermod can also be configured to just send MQTT message and not change WLED state using settings page as well as responding to motion only during nighttime (assuming NTP and lattitude/longitude are set to determine sunrise/sunset times). +Usermod can also be configured to send just the MQTT message but not change WLED state using settings page as well as responding to motion only at night +(assuming NTP and latitude/longitude are set to determine sunrise/sunset times). ### There are two options to get access to the usermod instance: -1. Include `usermod_PIR_sensor_switch.h` **before** you include the other usermod in `usermods_list.cpp' +1. Include `usermod_PIR_sensor_switch.h` **before** you include other usermods in `usermods_list.cpp' or @@ -100,7 +64,7 @@ class MyUsermod : public Usermod { ### Configuration options -Usermod can be configured in Usermods settings page. +Usermod can be configured via the Usermods settings page. * `PIRenabled` - enable/disable usermod * `pin` - dynamically change GPIO pin where PIR sensor is attached to ESP @@ -108,8 +72,8 @@ Usermod can be configured in Usermods settings page. * `on-preset` - preset triggered when PIR activates (if this is 0 it will just turn WLED on) * `off-preset` - preset triggered when PIR deactivates (if this is 0 it will just turn WLED off) * `nighttime-only` - enable triggering only between sunset and sunrise (you will need to set up _NTP_, _Lat_ & _Lon_ in Time & Macro settings) -* `mqtt-only` - only send MQTT messages, do not interact with WLED -* `off-only` - only trigger presets or turn WLED on/off in WLED is not already on (displaying effect) +* `mqtt-only` - send only MQTT messages, do not interact with WLED +* `off-only` - only trigger presets or turn WLED on/off if WLED is not already on (displaying effect) * `notifications` - enable or disable sending notifications to other WLED instances using Sync button @@ -121,4 +85,9 @@ Have fun - @gegu & @blazoncek 2021-11 * Added information about dynamic configuration options -* Added option to temporary enable/disble usermod from WLED UI (Info dialog) \ No newline at end of file +* Added option to temporary enable/disable usermod from WLED UI (Info dialog) + +2022-11 +* Added compile time option for off timer. +* Added Home Assistant autodiscovery MQTT broadcast. +* Updated info on compiling. diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 8085d79a9a..48d48f217d 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -1,452 +1,553 @@ -#pragma once - -#include "wled.h" - -#ifndef PIR_SENSOR_PIN - // compatible with QuinLED-Dig-Uno - #ifdef ARDUINO_ARCH_ESP32 - #define PIR_SENSOR_PIN 23 // Q4 - #else //ESP8266 boards - #define PIR_SENSOR_PIN 13 // Q4 (D7 on D1 mini) - #endif -#endif - -/* - * This usermod handles PIR sensor states. - * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. - * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. - * - * - * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality - * - * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. - * Multiple v2 usermods can be added to one compilation easily. - * - * Creating a usermod: - * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template. - * Please remember to rename the class and file to a descriptive name. - * You may also use multiple .h and .cpp files. - * - * Using a usermod: - * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) - * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp - */ - -class PIRsensorSwitch : public Usermod -{ -public: - /** - * constructor - */ - PIRsensorSwitch() {} - /** - * desctructor - */ - ~PIRsensorSwitch() {} - - /** - * Enable/Disable the PIR sensor - */ - void EnablePIRsensor(bool en) { enabled = en; } - /** - * Get PIR sensor enabled/disabled state - */ - bool PIRsensorEnabled() { return enabled; } - -private: - - byte prevPreset = 0; - byte prevPlaylist = 0; - bool savedState = false; - - uint32_t offTimerStart = 0; // off timer start time - byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // notification mode for stateUpdated(): CALL_MODE_NO_NOTIFY or CALL_MODE_DIRECT_CHANGE - byte sensorPinState = LOW; // current PIR sensor pin state - bool initDone = false; // status of initialization - bool PIRtriggered = false; - unsigned long lastLoop = 0; - - // configurable parameters - bool enabled = true; // PIR sensor enabled - int8_t PIRsensorPin = PIR_SENSOR_PIN; // PIR sensor pin - uint32_t m_switchOffDelay = 600000; // delay before switch off after the sensor state goes LOW (10min) - uint8_t m_onPreset = 0; // on preset - uint8_t m_offPreset = 0; // off preset - bool m_nightTimeOnly = false; // flag to indicate that PIR sensor should activate WLED during nighttime only - bool m_mqttOnly = false; // flag to send MQTT message only (assuming it is enabled) - // flag to enable triggering only if WLED is initially off (LEDs are not on, preventing running effect being overwritten by PIR) - bool m_offOnly = false; - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _switchOffDelay[]; - static const char _enabled[]; - static const char _onPreset[]; - static const char _offPreset[]; - static const char _nightTime[]; - static const char _mqttOnly[]; - static const char _offOnly[]; - static const char _notify[]; - - /** - * check if it is daytime - * if sunrise/sunset is not defined (no NTP or lat/lon) default to nighttime - */ - bool isDayTime() { - updateLocalTime(); - uint8_t hr = hour(localTime); - uint8_t mi = minute(localTime); - - if (sunrise && sunset) { - if (hour(sunrise)
hr) { - return true; - } else { - if (hour(sunrise)==hr && minute(sunrise)mi) { - return true; - } - } - } - return false; - } - - /** - * switch strip on/off - */ - void switchStrip(bool switchOn) - { - if (m_offOnly && bri && (switchOn || (!PIRtriggered && !switchOn))) return; - PIRtriggered = switchOn; - if (switchOn) { - if (m_onPreset) { - if (currentPlaylist>0) prevPlaylist = currentPlaylist; - else if (currentPreset>0) prevPreset = currentPreset; - else { - saveTemporaryPreset(); - savedState = true; - prevPlaylist = 0; - prevPreset = 0; - } - applyPreset(m_onPreset, NotifyUpdateMode); - return; - } - // preset not assigned - if (bri == 0) { - bri = briLast; - stateUpdated(NotifyUpdateMode); - } - } else { - if (m_offPreset) { - applyPreset(m_offPreset, NotifyUpdateMode); - return; - } else if (prevPlaylist) { - applyPreset(prevPlaylist, NotifyUpdateMode); - prevPlaylist = 0; - return; - } else if (prevPreset) { - applyPreset(prevPreset, NotifyUpdateMode); - prevPreset = 0; - return; - } else if (savedState) { - applyTemporaryPreset(); - savedState = false; - return; - } - // preset not assigned - if (bri != 0) { - briLast = bri; - bri = 0; - stateUpdated(NotifyUpdateMode); - } - } - } - - void publishMqtt(const char* state) - { - //Check if MQTT Connected, otherwise it will crash the 8266 - if (WLED_MQTT_CONNECTED){ - char subuf[64]; - strcpy(subuf, mqttDeviceTopic); - strcat_P(subuf, PSTR("/motion")); - mqtt->publish(subuf, 0, false, state); - } - } - - /** - * Read and update PIR sensor state. - * Initilize/reset switch off timer - */ - bool updatePIRsensorState() - { - bool pinState = digitalRead(PIRsensorPin); - if (pinState != sensorPinState) { - sensorPinState = pinState; // change previous state - - if (sensorPinState == HIGH) { - offTimerStart = 0; - if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true); - publishMqtt("on"); - } else /*if (bri != 0)*/ { - // start switch off timer - offTimerStart = millis(); - } - return true; - } - return false; - } - - /** - * switch off the strip if the delay has elapsed - */ - bool handleOffTimer() - { - if (offTimerStart > 0 && millis() - offTimerStart > m_switchOffDelay) - { - if (enabled == true) - { - if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(false); - publishMqtt("off"); - } - offTimerStart = 0; - return true; - } - return false; - } - -public: - //Functions called by WLED - - /** - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - */ - void setup() - { - if (enabled) { - // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (PIRsensorPin >= 0 && pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { - // PIR Sensor mode INPUT_PULLUP - pinMode(PIRsensorPin, INPUT_PULLUP); - sensorPinState = digitalRead(PIRsensorPin); - } else { - if (PIRsensorPin >= 0) { - DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed.")); - } - PIRsensorPin = -1; // allocation failed - enabled = false; - } - } - initDone = true; - } - - /** - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() - { - } - - /** - * loop() is called continuously. Here you can check for events, read sensors, etc. - */ - void loop() - { - // only check sensors 4x/s - if (!enabled || millis() - lastLoop < 250 || strip.isUpdating()) return; - lastLoop = millis(); - - if (!updatePIRsensorState()) { - handleOffTimer(); - } - } - - /** - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * - * Add PIR sensor state and switch off timer duration to jsoninfo - */ - void addToJsonInfo(JsonObject &root) - { - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - String uiDomString = F(""); - JsonArray infoArr = user.createNestedArray(uiDomString); // timer value - - if (enabled) { - if (offTimerStart > 0) - { - uiDomString = ""; - unsigned int offSeconds = (m_switchOffDelay - (millis() - offTimerStart)) / 1000; - if (offSeconds >= 3600) - { - uiDomString += (offSeconds / 3600); - uiDomString += F("h "); - offSeconds %= 3600; - } - if (offSeconds >= 60) - { - uiDomString += (offSeconds / 60); - offSeconds %= 60; - } - else if (uiDomString.length() > 0) - { - uiDomString += 0; - } - if (uiDomString.length() > 0) - { - uiDomString += F("min "); - } - uiDomString += (offSeconds); - infoArr.add(uiDomString + F("s")); - } else { - infoArr.add(sensorPinState ? F("sensor on") : F("inactive")); - } - } else { - infoArr.add(F("disabled")); - } - } - - /** - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ -/* - void addToJsonState(JsonObject &root) - { - } -*/ - - /** - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - - void readFromJsonState(JsonObject &root) - { - if (!initDone) return; // prevent crash on boot applyPreset() - JsonObject usermod = root[FPSTR(_name)]; - if (!usermod.isNull()) { - if (usermod[FPSTR(_enabled)].is()) { - enabled = usermod[FPSTR(_enabled)].as(); - } - } - } - - - /** - * provide the changeable values - */ - void addToConfig(JsonObject &root) - { - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000; - top["pin"] = PIRsensorPin; - top[FPSTR(_onPreset)] = m_onPreset; - top[FPSTR(_offPreset)] = m_offPreset; - top[FPSTR(_nightTime)] = m_nightTimeOnly; - top[FPSTR(_mqttOnly)] = m_mqttOnly; - top[FPSTR(_offOnly)] = m_offOnly; - top[FPSTR(_notify)] = (NotifyUpdateMode != CALL_MODE_NO_NOTIFY); - DEBUG_PRINTLN(F("PIR config saved.")); - } - - /** - * restore the changeable values - * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - * - * The function should return true if configuration was successfully loaded or false if there was no configuration. - */ - bool readFromConfig(JsonObject &root) - { - bool oldEnabled = enabled; - int8_t oldPin = PIRsensorPin; - - DEBUG_PRINT(FPSTR(_name)); - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - - PIRsensorPin = top["pin"] | PIRsensorPin; - - enabled = top[FPSTR(_enabled)] | enabled; - - m_switchOffDelay = (top[FPSTR(_switchOffDelay)] | m_switchOffDelay/1000) * 1000; - - m_onPreset = top[FPSTR(_onPreset)] | m_onPreset; - m_onPreset = max(0,min(250,(int)m_onPreset)); - m_offPreset = top[FPSTR(_offPreset)] | m_offPreset; - m_offPreset = max(0,min(250,(int)m_offPreset)); - - m_nightTimeOnly = top[FPSTR(_nightTime)] | m_nightTimeOnly; - m_mqttOnly = top[FPSTR(_mqttOnly)] | m_mqttOnly; - m_offOnly = top[FPSTR(_offOnly)] | m_offOnly; - - NotifyUpdateMode = top[FPSTR(_notify)] ? CALL_MODE_DIRECT_CHANGE : CALL_MODE_NO_NOTIFY; - - if (!initDone) { - // reading config prior to setup() - DEBUG_PRINTLN(F(" config loaded.")); - } else { - if (oldPin != PIRsensorPin || oldEnabled != enabled) { - // check if pin is OK - if (oldPin != PIRsensorPin && oldPin >= 0) { - // if we are changing pin in settings page - // deallocate old pin - pinManager.deallocatePin(oldPin, PinOwner::UM_PIR); - if (pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { - pinMode(PIRsensorPin, INPUT_PULLUP); - } else { - // allocation failed - PIRsensorPin = -1; - enabled = false; - } - } - if (enabled) { - sensorPinState = digitalRead(PIRsensorPin); - } - } - DEBUG_PRINTLN(F(" config (re)loaded.")); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_notify)].isNull(); - } - - /** - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_PIRSWITCH; - } -}; - -// strings to reduce flash memory usage (used more than twice) -const char PIRsensorSwitch::_name[] PROGMEM = "PIRsensorSwitch"; -const char PIRsensorSwitch::_enabled[] PROGMEM = "PIRenabled"; -const char PIRsensorSwitch::_switchOffDelay[] PROGMEM = "PIRoffSec"; -const char PIRsensorSwitch::_onPreset[] PROGMEM = "on-preset"; -const char PIRsensorSwitch::_offPreset[] PROGMEM = "off-preset"; -const char PIRsensorSwitch::_nightTime[] PROGMEM = "nighttime-only"; -const char PIRsensorSwitch::_mqttOnly[] PROGMEM = "mqtt-only"; -const char PIRsensorSwitch::_offOnly[] PROGMEM = "off-only"; -const char PIRsensorSwitch::_notify[] PROGMEM = "notifications"; +#pragma once + +#include "wled.h" + +#ifndef PIR_SENSOR_PIN + // compatible with QuinLED-Dig-Uno + #ifdef ARDUINO_ARCH_ESP32 + #define PIR_SENSOR_PIN 23 // Q4 + #else //ESP8266 boards + #define PIR_SENSOR_PIN 13 // Q4 (D7 on D1 mini) + #endif +#endif + +#ifndef PIR_SENSOR_OFF_SEC + #define PIR_SENSOR_OFF_SEC 600 +#endif + + +/* + * This usermod handles PIR sensor states. + * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. + * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. + * Maintained by: @blazoncek + * + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * + * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. + * Multiple v2 usermods can be added to one compilation easily. + */ + +class PIRsensorSwitch : public Usermod +{ +public: + // constructor + PIRsensorSwitch() {} + // destructor + ~PIRsensorSwitch() {} + + //Enable/Disable the PIR sensor + inline void EnablePIRsensor(bool en) { enabled = en; } + + // Get PIR sensor enabled/disabled state + inline bool PIRsensorEnabled() { return enabled; } + +private: + + byte prevPreset = 0; + byte prevPlaylist = 0; + + volatile unsigned long offTimerStart = 0; // off timer start time + volatile bool PIRtriggered = false; // did PIR trigger? + byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // notification mode for stateUpdated(): CALL_MODE_NO_NOTIFY or CALL_MODE_DIRECT_CHANGE + byte sensorPinState = LOW; // current PIR sensor pin state + bool initDone = false; // status of initialization + unsigned long lastLoop = 0; + + // configurable parameters + bool enabled = true; // PIR sensor enabled + int8_t PIRsensorPin = PIR_SENSOR_PIN; // PIR sensor pin + uint32_t m_switchOffDelay = PIR_SENSOR_OFF_SEC*1000; // delay before switch off after the sensor state goes LOW (10min) + uint8_t m_onPreset = 0; // on preset + uint8_t m_offPreset = 0; // off preset + bool m_nightTimeOnly = false; // flag to indicate that PIR sensor should activate WLED during nighttime only + bool m_mqttOnly = false; // flag to send MQTT message only (assuming it is enabled) + // flag to enable triggering only if WLED is initially off (LEDs are not on, preventing running effect being overwritten by PIR) + bool m_offOnly = false; + bool m_offMode = offMode; + bool m_override = false; + + // Home Assistant + bool HomeAssistantDiscovery = false; // is HA discovery turned on + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _switchOffDelay[]; + static const char _enabled[]; + static const char _onPreset[]; + static const char _offPreset[]; + static const char _nightTime[]; + static const char _mqttOnly[]; + static const char _offOnly[]; + static const char _haDiscovery[]; + static const char _notify[]; + static const char _override[]; + + /** + * check if it is daytime + * if sunrise/sunset is not defined (no NTP or lat/lon) default to nighttime + */ + static bool isDayTime(); + + /** + * switch strip on/off + */ + void switchStrip(bool switchOn); + void publishMqtt(const char* state); + + // Create an MQTT Binary Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. + void publishHomeAssistantAutodiscovery(); + + /** + * Read and update PIR sensor state. + * Initialize/reset switch off timer + */ + bool updatePIRsensorState(); + + /** + * switch off the strip if the delay has elapsed + */ + bool handleOffTimer(); + +public: + //Functions called by WLED + + /** + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup(); + + /** + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + //void connected(); + + /** + * onMqttConnect() is called when MQTT connection is established + */ + void onMqttConnect(bool sessionPresent); + + /** + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop(); + + /** + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * + * Add PIR sensor state and switch off timer duration to jsoninfo + */ + void addToJsonInfo(JsonObject &root); + + /** + * onStateChanged() is used to detect WLED state change + */ + void onStateChange(uint8_t mode); + + /** + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + //void addToJsonState(JsonObject &root); + + /** + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject &root); + + /** + * provide the changeable values + */ + void addToConfig(JsonObject &root); + + /** + * provide UI information and allow extending UI options + */ + void appendConfigData(); + + /** + * restore the changeable values + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject &root); + + /** + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { return USERMOD_ID_PIRSWITCH; } +}; + +// strings to reduce flash memory usage (used more than twice) +const char PIRsensorSwitch::_name[] PROGMEM = "PIRsensorSwitch"; +const char PIRsensorSwitch::_enabled[] PROGMEM = "PIRenabled"; +const char PIRsensorSwitch::_switchOffDelay[] PROGMEM = "PIRoffSec"; +const char PIRsensorSwitch::_onPreset[] PROGMEM = "on-preset"; +const char PIRsensorSwitch::_offPreset[] PROGMEM = "off-preset"; +const char PIRsensorSwitch::_nightTime[] PROGMEM = "nighttime-only"; +const char PIRsensorSwitch::_mqttOnly[] PROGMEM = "mqtt-only"; +const char PIRsensorSwitch::_offOnly[] PROGMEM = "off-only"; +const char PIRsensorSwitch::_haDiscovery[] PROGMEM = "HA-discovery"; +const char PIRsensorSwitch::_notify[] PROGMEM = "notifications"; +const char PIRsensorSwitch::_override[] PROGMEM = "override"; + +bool PIRsensorSwitch::isDayTime() { + updateLocalTime(); + uint8_t hr = hour(localTime); + uint8_t mi = minute(localTime); + + if (sunrise && sunset) { + if (hour(sunrise)
hr) { + return true; + } else { + if (hour(sunrise)==hr && minute(sunrise)mi) { + return true; + } + } + } + return false; +} + +void PIRsensorSwitch::switchStrip(bool switchOn) +{ + if (m_offOnly && bri && (switchOn || (!PIRtriggered && !switchOn))) return; //if lights on and off only, do nothing + if (PIRtriggered && switchOn) return; //if already on and triggered before, do nothing + PIRtriggered = switchOn; + DEBUG_PRINT(F("PIR: strip=")); DEBUG_PRINTLN(switchOn?"on":"off"); + if (switchOn) { + if (m_onPreset) { + if (currentPlaylist>0 && !offMode) { + prevPlaylist = currentPlaylist; + unloadPlaylist(); + } else if (currentPreset>0 && !offMode) { + prevPreset = currentPreset; + } else { + saveTemporaryPreset(); + prevPlaylist = 0; + prevPreset = 255; + } + applyPreset(m_onPreset, NotifyUpdateMode); + return; + } + // preset not assigned + if (bri == 0) { + bri = briLast; + stateUpdated(NotifyUpdateMode); + } + } else { + if (m_offPreset) { + applyPreset(m_offPreset, NotifyUpdateMode); + return; + } else if (prevPlaylist) { + if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPlaylist, NotifyUpdateMode); + prevPlaylist = 0; + return; + } else if (prevPreset) { + if (prevPreset<255) { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPreset, NotifyUpdateMode); } + else { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyTemporaryPreset(); } + prevPreset = 0; + return; + } + // preset not assigned + if (bri != 0) { + briLast = bri; + bri = 0; + stateUpdated(NotifyUpdateMode); + } + } +} + +void PIRsensorSwitch::publishMqtt(const char* state) +{ +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED) { + char buf[64]; + sprintf_P(buf, PSTR("%s/motion"), mqttDeviceTopic); //max length: 33 + 7 = 40 + mqtt->publish(buf, 0, false, state); + } +#endif +} + +void PIRsensorSwitch::publishHomeAssistantAutodiscovery() +{ +#ifndef WLED_DISABLE_MQTT + if (WLED_MQTT_CONNECTED) { + StaticJsonDocument<600> doc; + char uid[24], json_str[1024], buf[128]; + + sprintf_P(buf, PSTR("%s Motion"), serverDescription); //max length: 33 + 7 = 40 + doc[F("name")] = buf; + sprintf_P(buf, PSTR("%s/motion"), mqttDeviceTopic); //max length: 33 + 7 = 40 + doc[F("stat_t")] = buf; + doc[F("pl_on")] = "on"; + doc[F("pl_off")] = "off"; + sprintf_P(uid, PSTR("%s_motion"), escapedMac.c_str()); + doc[F("uniq_id")] = uid; + doc[F("dev_cla")] = F("motion"); + doc[F("exp_aft")] = 1800; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; + device[F("mf")] = "WLED"; + device[F("mdl")] = F("FOSS"); + device[F("sw")] = versionString; + + sprintf_P(buf, PSTR("homeassistant/binary_sensor/%s/config"), uid); + DEBUG_PRINTLN(buf); + size_t payload_size = serializeJson(doc, json_str); + DEBUG_PRINTLN(json_str); + + mqtt->publish(buf, 0, true, json_str, payload_size); // do we really need to retain? + } +#endif +} + +bool PIRsensorSwitch::updatePIRsensorState() +{ + bool pinState = digitalRead(PIRsensorPin); + if (pinState != sensorPinState) { + sensorPinState = pinState; // change previous state + + if (sensorPinState == HIGH) { + offTimerStart = 0; + if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true); + else if (NotifyUpdateMode != CALL_MODE_NO_NOTIFY) updateInterfaces(CALL_MODE_WS_SEND); + publishMqtt("on"); + } else { + // start switch off timer + offTimerStart = millis(); + if (NotifyUpdateMode != CALL_MODE_NO_NOTIFY) updateInterfaces(CALL_MODE_WS_SEND); + } + return true; + } + return false; +} + +bool PIRsensorSwitch::handleOffTimer() +{ + if (offTimerStart > 0 && millis() - offTimerStart > m_switchOffDelay) { + offTimerStart = 0; + if (enabled == true) { + if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()) || PIRtriggered)) switchStrip(false); + else if (NotifyUpdateMode != CALL_MODE_NO_NOTIFY) updateInterfaces(CALL_MODE_WS_SEND); + publishMqtt("off"); + } + return true; + } + return false; +} + +//Functions called by WLED + +void PIRsensorSwitch::setup() +{ + if (enabled) { + // pin retrieved from cfg.json (readFromConfig()) prior to running setup() + if (PIRsensorPin >= 0 && pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { + // PIR Sensor mode INPUT_PULLUP + pinMode(PIRsensorPin, INPUT_PULLUP); + sensorPinState = digitalRead(PIRsensorPin); + } else { + if (PIRsensorPin >= 0) { + DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed.")); + } + PIRsensorPin = -1; // allocation failed + enabled = false; + } + } + initDone = true; +} + +void PIRsensorSwitch::onMqttConnect(bool sessionPresent) +{ + if (HomeAssistantDiscovery) { + publishHomeAssistantAutodiscovery(); + } +} + +void PIRsensorSwitch::loop() +{ + // only check sensors 4x/s + if (!enabled || millis() - lastLoop < 250 || strip.isUpdating()) return; + lastLoop = millis(); + + if (!updatePIRsensorState()) { + handleOffTimer(); + } +} + +void PIRsensorSwitch::addToJsonInfo(JsonObject &root) +{ + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + + String uiDomString; + if (enabled) { + if (offTimerStart > 0) + { + uiDomString = ""; + unsigned int offSeconds = (m_switchOffDelay - (millis() - offTimerStart)) / 1000; + if (offSeconds >= 3600) + { + uiDomString += (offSeconds / 3600); + uiDomString += F("h "); + offSeconds %= 3600; + } + if (offSeconds >= 60) + { + uiDomString += (offSeconds / 60); + offSeconds %= 60; + } + else if (uiDomString.length() > 0) + { + uiDomString += 0; + } + if (uiDomString.length() > 0) + { + uiDomString += F("min "); + } + uiDomString += (offSeconds); + infoArr.add(uiDomString + F("s")); + } else { + infoArr.add(sensorPinState ? F("sensor on") : F("inactive")); + } + } else { + infoArr.add(F("disabled")); + } + + uiDomString = F(" "); + infoArr.add(uiDomString); + + JsonObject sensor = root[F("sensor")]; + if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); + sensor[F("motion")] = sensorPinState || offTimerStart>0 ? true : false; +} + +void PIRsensorSwitch::onStateChange(uint8_t mode) { + if (!initDone) return; + DEBUG_PRINT(F("PIR: offTimerStart=")); DEBUG_PRINTLN(offTimerStart); + if (m_override && PIRtriggered && offTimerStart) { // debounce + // checking PIRtriggered and offTimerStart will prevent cancellation upon On trigger + DEBUG_PRINTLN(F("PIR: Canceled.")); + offTimerStart = 0; + PIRtriggered = false; + } +} + +void PIRsensorSwitch::readFromJsonState(JsonObject &root) +{ + if (!initDone) return; // prevent crash on boot applyPreset() + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) { + if (usermod[FPSTR(_enabled)].is()) { + enabled = usermod[FPSTR(_enabled)].as(); + } + } +} + +void PIRsensorSwitch::addToConfig(JsonObject &root) +{ + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000; + top["pin"] = PIRsensorPin; + top[FPSTR(_onPreset)] = m_onPreset; + top[FPSTR(_offPreset)] = m_offPreset; + top[FPSTR(_nightTime)] = m_nightTimeOnly; + top[FPSTR(_mqttOnly)] = m_mqttOnly; + top[FPSTR(_offOnly)] = m_offOnly; + top[FPSTR(_override)] = m_override; + top[FPSTR(_haDiscovery)] = HomeAssistantDiscovery; + top[FPSTR(_notify)] = (NotifyUpdateMode != CALL_MODE_NO_NOTIFY); + DEBUG_PRINTLN(F("PIR config saved.")); +} + +void PIRsensorSwitch::appendConfigData() +{ + oappend(SET_F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('PIRsensorSwitch:notifications',1,'Periodic WS updates');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field +} + +bool PIRsensorSwitch::readFromConfig(JsonObject &root) +{ + bool oldEnabled = enabled; + int8_t oldPin = PIRsensorPin; + + DEBUG_PRINT(FPSTR(_name)); + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + PIRsensorPin = top["pin"] | PIRsensorPin; + + enabled = top[FPSTR(_enabled)] | enabled; + + m_switchOffDelay = (top[FPSTR(_switchOffDelay)] | m_switchOffDelay/1000) * 1000; + + m_onPreset = top[FPSTR(_onPreset)] | m_onPreset; + m_onPreset = max(0,min(250,(int)m_onPreset)); + m_offPreset = top[FPSTR(_offPreset)] | m_offPreset; + m_offPreset = max(0,min(250,(int)m_offPreset)); + + m_nightTimeOnly = top[FPSTR(_nightTime)] | m_nightTimeOnly; + m_mqttOnly = top[FPSTR(_mqttOnly)] | m_mqttOnly; + m_offOnly = top[FPSTR(_offOnly)] | m_offOnly; + m_override = top[FPSTR(_override)] | m_override; + HomeAssistantDiscovery = top[FPSTR(_haDiscovery)] | HomeAssistantDiscovery; + + NotifyUpdateMode = top[FPSTR(_notify)] ? CALL_MODE_DIRECT_CHANGE : CALL_MODE_NO_NOTIFY; + + if (!initDone) { + // reading config prior to setup() + DEBUG_PRINTLN(F(" config loaded.")); + } else { + if (oldPin != PIRsensorPin || oldEnabled != enabled) { + // check if pin is OK + if (oldPin != PIRsensorPin && oldPin >= 0) { + // if we are changing pin in settings page + // deallocate old pin + pinManager.deallocatePin(oldPin, PinOwner::UM_PIR); + if (pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { + pinMode(PIRsensorPin, INPUT_PULLUP); + } else { + // allocation failed + PIRsensorPin = -1; + enabled = false; + } + } + if (enabled) { + sensorPinState = digitalRead(PIRsensorPin); + } + } + DEBUG_PRINTLN(F(" config (re)loaded.")); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_override)].isNull(); +} diff --git a/usermods/PWM_fan/readme.md b/usermods/PWM_fan/readme.md index a40098c159..6a44acf3b3 100644 --- a/usermods/PWM_fan/readme.md +++ b/usermods/PWM_fan/readme.md @@ -2,12 +2,12 @@ v2 Usermod to to control PWM fan with RPM feedback and temperature control -This usermod requires Dallas Temperature usermod to obtain temperature information. If this is not available the fan will always run at 100% speed. -If the fan does not have _tacho_ (RPM) output you can set the _tacho-pin_ to -1 to not use that feature. +This usermod requires the Dallas Temperature usermod to obtain temperature information. If it's not available, the fan will run at 100% speed. +If the fan does not have _tachometer_ (RPM) output you can set the _tachometer-pin_ to -1 to disable that feature. -You can also set the thershold temperature at which fan runs at lowest speed. If the actual temperature measured will be 3°C greater than threshold temperature the fan will run at 100%. +You can also set the threshold temperature at which fan runs at lowest speed. If the measured temperature is 3°C greater than the threshold temperature, the fan will run at 100%. -If the _tacho_ is supported the current speed (in RPM) will be repored in WLED Info page. +If the _tachometer_ is supported, the current speed (in RPM) will be displayed on the WLED Info page. ## Installation @@ -19,10 +19,10 @@ You will also need `-D USERMOD_DALLASTEMPERATURE`. All of the parameters are configured during run-time using Usermods settings page. This includes: -* PWM output pin -* tacho input pin +* PWM output pin (can be configured at compile time `-D PWM_PIN=xx`) +* tachometer input pin (can be configured at compile time `-D TACHO_PIN=xx`) * sampling frequency in seconds -* threshold temperature in degees C +* threshold temperature in degrees Celsius _NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency to match PWM fan sampling frequency. @@ -30,7 +30,16 @@ _NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency No special requirements. +## Control PWM fan speed using JSON API + +e.g. you can use `{"PWM-fan":{"speed":30,"lock":true}}` to lock fan speed to 30 percent of maximum. (replace 30 with an arbitrary value between 0 and 100) +If you include `speed` property you can set fan speed as a percentage (%) of maximum speed. +If you include `lock` property you can lock (_true_) or unlock (_false_) the fan speed. +If the fan speed is unlocked, it will revert to temperature controlled speed on the next update cycle. Once fan speed is locked it will remain so until it is unlocked by the next API call. + ## Change Log 2021-10 * First public release +2022-05 +* Added JSON API call to allow changing of speed diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/usermod_PWM_fan.h index 82aa917bbd..05b6d9b3bf 100644 --- a/usermods/PWM_fan/usermod_PWM_fan.h +++ b/usermods/PWM_fan/usermod_PWM_fan.h @@ -1,7 +1,7 @@ #pragma once -#ifndef USERMOD_DALLASTEMPERATURE -#error The "PWM fan" usermod requires "Dallas Temeprature" usermod to function properly. +#if !defined(USERMOD_DALLASTEMPERATURE) && !defined(USERMOD_SHT) +#error The "PWM fan" usermod requires "Dallas Temeprature" or "SHT" usermod to function properly. #endif #include "wled.h" @@ -10,6 +10,13 @@ // https://github.com/KlausMu/esp32-fan-controller/tree/main/src // adapted for WLED usermod by @blazoncek +#ifndef TACHO_PIN + #define TACHO_PIN -1 +#endif + +#ifndef PWM_PIN + #define PWM_PIN -1 +#endif // tacho counter static volatile unsigned long counter_rpm = 0; @@ -31,18 +38,28 @@ class PWMFanUsermod : public Usermod { #ifdef ARDUINO_ARCH_ESP32 uint8_t pwmChannel = 255; #endif + bool lockFan = false; #ifdef USERMOD_DALLASTEMPERATURE UsermodTemperature* tempUM; + #elif defined(USERMOD_SHT) + ShtUsermod* tempUM; #endif // configurable parameters - int8_t tachoPin = -1; - int8_t pwmPin = -1; + int8_t tachoPin = TACHO_PIN; + int8_t pwmPin = PWM_PIN; uint8_t tachoUpdateSec = 30; - float targetTemperature = 25.0; - uint8_t minPWMValuePct = 50; + float targetTemperature = 35.0; + uint8_t minPWMValuePct = 0; + uint8_t maxPWMValuePct = 100; uint8_t numberOfInterrupsInOneSingleRotation = 2; // Number of interrupts ESP32 sees on tacho signal on a single fan rotation. All the fans I've seen trigger two interrups. + uint8_t pwmValuePct = 0; + + // constant values + static const uint8_t _pwmMaxValue = 255; + static const uint8_t _pwmMaxStepCount = 7; + float _pwmTempStepSize = 0.5f; // strings to reduce flash memory usage (used more than twice) static const char _name[]; @@ -52,7 +69,10 @@ class PWMFanUsermod : public Usermod { static const char _temperature[]; static const char _tachoUpdateSec[]; static const char _minPWMValuePct[]; + static const char _maxPWMValuePct[]; static const char _IRQperRotation[]; + static const char _speed[]; + static const char _lock[]; void initTacho(void) { if (tachoPin < 0 || !pinManager.allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ @@ -73,6 +93,8 @@ class PWMFanUsermod : public Usermod { } void updateTacho(void) { + // store milliseconds when tacho was measured the last time + msLastTachoMeasurement = millis(); if (tachoPin < 0) return; // start of tacho measurement @@ -83,8 +105,6 @@ class PWMFanUsermod : public Usermod { last_rpm /= tachoUpdateSec; // reset counter counter_rpm = 0; - // store milliseconds when tacho was measured the last time - msLastTachoMeasurement = millis(); // attach interrupt again attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); } @@ -92,6 +112,7 @@ class PWMFanUsermod : public Usermod { // https://randomnerdtutorials.com/esp32-pwm-arduino-ide/ void initPWMfan(void) { if (pwmPin < 0 || !pinManager.allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { + enabled = false; pwmPin = -1; return; } @@ -123,7 +144,7 @@ class PWMFanUsermod : public Usermod { } void updateFanSpeed(uint8_t pwmValue){ - if (pwmPin < 0) return; + if (!enabled || pwmPin < 0) return; #ifdef ESP8266 analogWrite(pwmPin, pwmValue); @@ -133,7 +154,7 @@ class PWMFanUsermod : public Usermod { } float getActualTemperature(void) { - #ifdef USERMOD_DALLASTEMPERATURE + #if defined(USERMOD_DALLASTEMPERATURE) || defined(USERMOD_SHT) if (tempUM != nullptr) return tempUM->getTemperatureC(); #endif @@ -142,31 +163,25 @@ class PWMFanUsermod : public Usermod { void setFanPWMbasedOnTemperature(void) { float temp = getActualTemperature(); - float difftemp = temp - targetTemperature; - // Default to run fan at full speed. - int newPWMvalue = 255; - int pwmStep = ((100 - minPWMValuePct) * newPWMvalue) / (7*100); - int pwmMinimumValue = (minPWMValuePct * newPWMvalue) / 100; + // dividing minPercent and maxPercent into equal pwmvalue sizes + int pwmStepSize = ((maxPWMValuePct - minPWMValuePct) * _pwmMaxValue) / (_pwmMaxStepCount*100); + int pwmStep = calculatePwmStep(temp - targetTemperature); + // minimum based on full speed - not entered MaxPercent + int pwmMinimumValue = (minPWMValuePct * _pwmMaxValue) / 100; + updateFanSpeed(pwmMinimumValue + pwmStep*pwmStepSize); + } - if ((temp == NAN) || (temp <= 0.0)) { + uint8_t calculatePwmStep(float diffTemp){ + if ((diffTemp == NAN) || (diffTemp <= -100.0)) { DEBUG_PRINTLN(F("WARNING: no temperature value available. Cannot do temperature control. Will set PWM fan to 255.")); - } else if (difftemp <= 0.0) { - // Temperature is below target temperature. Run fan at minimum speed. - newPWMvalue = pwmMinimumValue; - } else if (difftemp <= 0.5) { - newPWMvalue = pwmMinimumValue + pwmStep; - } else if (difftemp <= 1.0) { - newPWMvalue = pwmMinimumValue + 2*pwmStep; - } else if (difftemp <= 1.5) { - newPWMvalue = pwmMinimumValue + 3*pwmStep; - } else if (difftemp <= 2.0) { - newPWMvalue = pwmMinimumValue + 4*pwmStep; - } else if (difftemp <= 2.5) { - newPWMvalue = pwmMinimumValue + 5*pwmStep; - } else if (difftemp <= 3.0) { - newPWMvalue = pwmMinimumValue + 6*pwmStep; + return _pwmMaxStepCount; + } + if(diffTemp <=0){ + return 0; } - updateFanSpeed(newPWMvalue); + int calculatedStep = (diffTemp / _pwmTempStepSize)+1; + // anything greater than max stepcount gets max + return (uint8_t)min((int)_pwmMaxStepCount,calculatedStep); } public: @@ -177,6 +192,8 @@ class PWMFanUsermod : public Usermod { #ifdef USERMOD_DALLASTEMPERATURE // This Usermod requires Temperature usermod tempUM = (UsermodTemperature*) usermods.lookup(USERMOD_ID_TEMPERATURE); + #elif defined(USERMOD_SHT) + tempUM = (ShtUsermod*) usermods.lookup(USERMOD_ID_SHT); #endif initTacho(); initPWMfan(); @@ -198,7 +215,7 @@ class PWMFanUsermod : public Usermod { if ((now - msLastTachoMeasurement) < (tachoUpdateSec * 1000)) return; updateTacho(); - setFanPWMbasedOnTemperature(); + if (!lockFan) setFanPWMbasedOnTemperature(); } /* @@ -207,12 +224,41 @@ class PWMFanUsermod : public Usermod { * Below it is shown how this could be used for e.g. a light sensor */ void addToJsonInfo(JsonObject& root) { - if (tachoPin < 0) return; JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); - JsonArray data = user.createNestedArray(FPSTR(_name)); - data.add(last_rpm); - data.add(F("rpm")); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + String uiDomString = F(""); + infoArr.add(uiDomString); + + if (enabled) { + JsonArray infoArr = user.createNestedArray(F("Manual")); + String uiDomString = F("
"); // + infoArr.add(uiDomString); + + JsonArray data = user.createNestedArray(F("Speed")); + if (tachoPin >= 0) { + data.add(last_rpm); + data.add(F("rpm")); + } else { + if (lockFan) data.add(F("locked")); + else data.add(F("auto")); + } + } } /* @@ -226,9 +272,24 @@ class PWMFanUsermod : public Usermod { * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - //void readFromJsonState(JsonObject& root) { - // if (!initDone) return; // prevent crash on boot applyPreset() - //} + void readFromJsonState(JsonObject& root) { + if (!initDone) return; // prevent crash on boot applyPreset() + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) { + if (usermod[FPSTR(_enabled)].is()) { + enabled = usermod[FPSTR(_enabled)].as(); + if (!enabled) updateFanSpeed(0); + } + if (enabled && !usermod[FPSTR(_speed)].isNull() && usermod[FPSTR(_speed)].is()) { + pwmValuePct = usermod[FPSTR(_speed)].as(); + updateFanSpeed((constrain(pwmValuePct,0,100) * 255) / 100); + if (pwmValuePct) lockFan = true; + } + if (enabled && !usermod[FPSTR(_lock)].isNull() && usermod[FPSTR(_lock)].is()) { + lockFan = usermod[FPSTR(_lock)].as(); + } + } + } /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. @@ -252,6 +313,7 @@ class PWMFanUsermod : public Usermod { top[FPSTR(_tachoUpdateSec)] = tachoUpdateSec; top[FPSTR(_temperature)] = targetTemperature; top[FPSTR(_minPWMValuePct)] = minPWMValuePct; + top[FPSTR(_maxPWMValuePct)] = maxPWMValuePct; top[FPSTR(_IRQperRotation)] = numberOfInterrupsInOneSingleRotation; DEBUG_PRINTLN(F("Autosave config saved.")); } @@ -285,6 +347,8 @@ class PWMFanUsermod : public Usermod { targetTemperature = top[FPSTR(_temperature)] | targetTemperature; minPWMValuePct = top[FPSTR(_minPWMValuePct)] | minPWMValuePct; minPWMValuePct = (uint8_t) min(100,max(0,(int)minPWMValuePct)); // bounds checking + maxPWMValuePct = top[FPSTR(_maxPWMValuePct)] | maxPWMValuePct; + maxPWMValuePct = (uint8_t) min(100,max((int)minPWMValuePct,(int)maxPWMValuePct)); // bounds checking numberOfInterrupsInOneSingleRotation = top[FPSTR(_IRQperRotation)] | numberOfInterrupsInOneSingleRotation; numberOfInterrupsInOneSingleRotation = (uint8_t) max(1,(int)numberOfInterrupsInOneSingleRotation); // bounds checking @@ -329,4 +393,7 @@ const char PWMFanUsermod::_pwmPin[] PROGMEM = "PWM-pin"; const char PWMFanUsermod::_temperature[] PROGMEM = "target-temp-C"; const char PWMFanUsermod::_tachoUpdateSec[] PROGMEM = "tacho-update-s"; const char PWMFanUsermod::_minPWMValuePct[] PROGMEM = "min-PWM-percent"; +const char PWMFanUsermod::_maxPWMValuePct[] PROGMEM = "max-PWM-percent"; const char PWMFanUsermod::_IRQperRotation[] PROGMEM = "IRQs-per-rotation"; +const char PWMFanUsermod::_speed[] PROGMEM = "speed"; +const char PWMFanUsermod::_lock[] PROGMEM = "lock"; diff --git a/usermods/QuinLED_Dig_Uno_Temp_MQTT/readme.md b/usermods/QuinLED_Dig_Uno_Temp_MQTT/readme.md deleted file mode 100644 index 60fc31f73f..0000000000 --- a/usermods/QuinLED_Dig_Uno_Temp_MQTT/readme.md +++ /dev/null @@ -1,34 +0,0 @@ -# QuinLED Dig Uno board - -These files allow WLED 0.9.1 to report the temp sensor on the Quinled board to MQTT. I use it to report the board temp to Home Assistant via MQTT, so it will send notifications if something happens and the board start to heat up. -This code uses Aircookie's WLED software. It has a premade file for user modifications. I use it to publish the temperature from the dallas temperature sensor on the Quinled board. The entries for the top of the WLED00 file, initializes the required libraries, and variables for the sensor. The .ino file waits for 60 seconds, and checks to see if the MQTT server is connected (thanks Aircoookie). It then poles the sensor, and published it using the MQTT service already running, using the main topic programmed in the WLED UI. - -Installation of file: Copy and replace file in wled00 directory - -## Project link - -* [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link - -### Platformio requirements - -Uncomment `DallasTemperature@~3.8.0`,`OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: - -```ini -# platformio.ini -... -[platformio] -... -; default_envs = esp07 -default_envs = d1_mini -... -[common] -... -lib_deps_external = - ... - #For use SSD1306 OLED display uncomment following - U8g2@~2.27.3 - #For Dallas sensor uncomment following 2 lines - DallasTemperature@~3.8.0 - OneWire@~2.3.5 -... -``` diff --git a/usermods/QuinLED_Dig_Uno_Temp_MQTT/usermod.cpp b/usermods/QuinLED_Dig_Uno_Temp_MQTT/usermod.cpp deleted file mode 100644 index 5b4e2e5c7e..0000000000 --- a/usermods/QuinLED_Dig_Uno_Temp_MQTT/usermod.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include -#include "wled.h" -//Intiating code for QuinLED Dig-Uno temp sensor -//Uncomment Celsius if that is your prefered temperature scale -#include //Dallastemperature sensor -#ifdef ARDUINO_ARCH_ESP32 //ESP32 boards -OneWire oneWire(18); -#else //ESP8266 boards -OneWire oneWire(14); -#endif -DallasTemperature sensor(&oneWire); -long temptimer = millis(); -long lastMeasure = 0; -#define Celsius // Show temperature mesaurement in Celcius otherwise is in Fahrenheit -void userSetup() -{ -// Start the DS18B20 sensor - sensor.begin(); -} - -//gets called every time WiFi is (re-)connected. Initialize own network interfaces here -void userConnected() -{ - -} - -void userLoop() -{ - temptimer = millis(); - -// Timer to publishe new temperature every 60 seconds - if (temptimer - lastMeasure > 60000) { - lastMeasure = temptimer; - -//Check if MQTT Connected, otherwise it will crash the 8266 - if (mqtt != nullptr){ - sensor.requestTemperatures(); - -//Gets prefered temperature scale based on selection in definitions section - #ifdef Celsius - float board_temperature = sensor.getTempCByIndex(0); - #else - float board_temperature = sensors.getTempFByIndex(0); - #endif - -//Create character string populated with user defined device topic from the UI, and the read temperature. Then publish to MQTT server. - char subuf[38]; - strcpy(subuf, mqttDeviceTopic); - strcat(subuf, "/temperature"); - mqtt->publish(subuf, 0, true, String(board_temperature).c_str()); - return;} - return;} -return; -} diff --git a/usermods/RTC/readme.md b/usermods/RTC/readme.md index 3aaa609118..0add4efcf3 100644 --- a/usermods/RTC/readme.md +++ b/usermods/RTC/readme.md @@ -1,6 +1,6 @@ # DS1307/DS3231 Real time clock -Gets the time from I2C RTC module on boot. This allows clocks to operate e.g. if temporarily no WiFi is available. +Gets the time from I2C RTC module on boot. This allows clock operation if WiFi is not available. The stored time is updated each time NTP is synced. ## Installation diff --git a/usermods/RTC/usermod_rtc.h b/usermods/RTC/usermod_rtc.h index 8c174e6fa8..42965e3af3 100644 --- a/usermods/RTC/usermod_rtc.h +++ b/usermods/RTC/usermod_rtc.h @@ -3,14 +3,6 @@ #include "src/dependencies/time/DS1307RTC.h" #include "wled.h" -#ifdef ARDUINO_ARCH_ESP32 - #define HW_PIN_SCL 22 - #define HW_PIN_SDA 21 -#else - #define HW_PIN_SCL 5 - #define HW_PIN_SDA 4 -#endif - //Connect DS1307 to standard I2C pins (ESP32: GPIO 21 (SDA)/GPIO 22 (SCL)) class RTCUsermod : public Usermod { @@ -20,8 +12,8 @@ class RTCUsermod : public Usermod { public: void setup() { - PinManagerPinType pins[2] = { { HW_PIN_SCL, true }, { HW_PIN_SDA, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { disabled = true; return; } + if (i2c_scl<0 || i2c_sda<0) { disabled = true; return; } + RTC.begin(); time_t rtcTime = RTC.get(); if (rtcTime) { toki.setTime(rtcTime,TOKI_NO_MS_ACCURACY,TOKI_TS_RTC); @@ -32,8 +24,8 @@ class RTCUsermod : public Usermod { } void loop() { - if (strip.isUpdating()) return; - if (!disabled && toki.isTick()) { + if (disabled || strip.isUpdating()) return; + if (toki.isTick()) { time_t t = toki.second(); if (t != RTC.get()) RTC.set(t); //set RTC to NTP/UI-provided value } @@ -44,13 +36,13 @@ class RTCUsermod : public Usermod { * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ - void addToConfig(JsonObject& root) - { - JsonObject top = root.createNestedObject("RTC"); - JsonArray pins = top.createNestedArray("pin"); - pins.add(HW_PIN_SCL); - pins.add(HW_PIN_SDA); - } +// void addToConfig(JsonObject& root) +// { +// JsonObject top = root.createNestedObject("RTC"); +// JsonArray pins = top.createNestedArray("pin"); +// pins.add(i2c_scl); +// pins.add(i2c_sda); +// } uint16_t getId() { diff --git a/usermods/RelayBlinds/readme.md b/usermods/RelayBlinds/readme.md index 0c3d2a0ba5..8c533dd4cb 100644 --- a/usermods/RelayBlinds/readme.md +++ b/usermods/RelayBlinds/readme.md @@ -1,8 +1,8 @@ # RelayBlinds usermod -This simple usermod toggles two relay pins momentarily (default for 500ms) when `userVar0` is set. -This can be used to e.g. "push" the buttons of a window blinds motor controller. +This simple usermod toggles two relay pins momentarily (defaults to 500ms) when `userVar0` is set. +e.g. can be used to "push" the buttons of a window blinds motor controller. v1 usermod. Please replace usermod.cpp in the `wled00` directory with the one in this file. You may upload `index.htm` to `[WLED-IP]/edit` to replace the default lighting UI with a simple Up/Down button one. -Also, a simple `presets.json` file is available, this makes the relay actions controllable via two presets to facilitate control e.g. via the default UI or Alexa. \ No newline at end of file +A simple `presets.json` file is available. This makes the relay actions controllable via two presets to facilitate control e.g. the default UI or Alexa. diff --git a/usermods/SN_Photoresistor/readme.md b/usermods/SN_Photoresistor/readme.md index 4f3a36fbe7..feacf41ae3 100644 --- a/usermods/SN_Photoresistor/readme.md +++ b/usermods/SN_Photoresistor/readme.md @@ -1,7 +1,7 @@ # SN_Photoresistor usermod -This usermod will read from an attached photoresistor sensor like the KY-018 sensor. -The luminance is displayed both in the Info section of the web UI as well as published to the `/luminance` MQTT topic if enabled. +This usermod will read from an attached photoresistor sensor like the KY-018. +The luminance is displayed in both the Info section of the web UI as well as published to the `/luminance` MQTT topic, if enabled. ## Installation @@ -9,15 +9,15 @@ Copy the example `platformio_override.ini` to the root directory. This file sho ### Define Your Options -* `USERMOD_SN_PHOTORESISTOR` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60 seconds -* `USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 20 seconds -* `USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE` - the voltage supplied to the sensor, defaults to 5v -* `USERMOD_SN_PHOTORESISTOR_ADC_PRECISION` - the ADC precision is the number of distinguishable ADC inputs, defaults to 1024.0 (10 bits) -* `USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE` - the resistor size, defaults to 10000.0 (10K hms) -* `USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE` - the offset value to report on, defaults to 25 +* `USERMOD_SN_PHOTORESISTOR` - Enables this user mod. wled00\usermods_list.cpp +* `USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL` - Number of milliseconds between measurements. Defaults to 60000 ms +* `USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT` - Number of milliseconds after boot to take first measurement. Defaults to 20000 ms +* `USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE` - Voltage supplied to the sensor. Defaults to 5v +* `USERMOD_SN_PHOTORESISTOR_ADC_PRECISION` - ADC precision. Defaults to 10 bits +* `USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE` - Resistor size, defaults to 10000.0 (10K Ohms) +* `USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE` - Offset value to report on. Defaults to 25 -All parameters can be configured at runtime using Usermods settings page. +All parameters can be configured at runtime via the Usermods settings page. ## Project link diff --git a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h index 9c3be7cc28..1e92d7d716 100644 --- a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h +++ b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h @@ -30,7 +30,7 @@ #define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0f #endif -// only report if differance grater than offset value +// only report if difference grater than offset value #ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE #define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5 #endif @@ -109,6 +109,7 @@ class Usermod_SN_Photoresistor : public Usermod { lastLDRValue = currentLDRValue; +#ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { char subuf[45]; @@ -121,6 +122,7 @@ class Usermod_SN_Photoresistor : public Usermod DEBUG_PRINTLN("Missing MQTT connection. Not publishing data"); } } +#endif } uint16_t getLastLDRValue() diff --git a/usermods/ST7789_display/README.md b/usermods/ST7789_display/README.md index b98d5be002..ebaae49228 100644 --- a/usermods/ST7789_display/README.md +++ b/usermods/ST7789_display/README.md @@ -1,12 +1,15 @@ -# ST7789 TFT IPS Color display 240x240pxwith ESP32 boards +# Using the ST7789 TFT IPS 240x240 pixel color display with ESP32 boards -This usermod allow to use 240x240 display to display following: +This usermod enables display of the following: +* Current date and time; * Network SSID; * IP address; +* WiFi signal strength; * Brightness; -* Chosen effect; -* Chosen palette; +* Selected effect; +* Selected palette; +* Effect speed and intensity; * Estimated current in mA; ## Hardware @@ -38,35 +41,37 @@ lib_deps = ... ``` -Also, while in the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows: +In the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows: -Add lines to section: +Add the following lines to section: ```ini default_envs = esp32dev build_flags = ${common.build_flags_esp32} -D USERMOD_ST7789_DISPLAY - + -DUSER_SETUP_LOADED=1 + -DST7789_DRIVER=1 + -DTFT_WIDTH=240 + -DTFT_HEIGHT=240 + -DCGRAM_OFFSET=1 + -DTFT_MOSI=21 + -DTFT_SCLK=22 + -DTFT_DC=27 + -DTFT_RST=26 + -DTFT_BL=14 + -DLOAD_GLCD=1 + ;optional for WROVER + ;-DCONFIG_SPIRAM_SUPPORT=1 ``` -Save the `platformio.ini` file. Once this is saved, the required library files should be automatically downloaded for modifications in a later step. +Save the `platformio.ini` file. Once saved, the required library files should be automatically downloaded for modifications in a later step. ### TFT_eSPI Library Adjustments -We need to modify a file in the `TFT_eSPI` library. If you followed the directions to modify and save the `platformio.ini` file above, the `User_Setup_Select.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI` folder. - -Modify the `User_Setup_Select.h` file as follows: - -* Comment out the following line (which is the 'default' setup file): - -```ini -//#include // Default setup is root library folder -``` +If you are not using PlatformIO, you need to modify a file in the `TFT_eSPI` library. If you followed the directions to modify and save the `platformio.ini` file above, the `Setup24_ST7789.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI/User_Setups/` folder. -* Add following line: +Edit `Setup_ST7789.h` file and uncomment and change GPIO pin numbers in lines containing `TFT_MOSI`, `TFT_SCLK`, `TFT_RST`, `TFT_DC`. -```ini -#include // Setup file for ESP32 ST7789V SPI bus TFT -``` +Modify the `User_Setup_Select.h` by uncommenting the line containing `#include ` and commenting out the line containing `#include `. -* Copy file `"Setup_ST7789_Display.h"` from usermod folder to `/.pio/libdeps/esp32dev/TFT_eSPI/User_Setups` +If your display uses the backlight enable pin, add this definition: #define TFT_BL with backlight enable GPIO number. diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.h index 19ad5790ae..144cccbfaa 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.h @@ -7,33 +7,49 @@ #include #include -#define USERMOD_ST7789_DISPLAY 97 - -#ifndef TFT_DISPOFF -#define TFT_DISPOFF 0x28 +#ifndef USER_SETUP_LOADED + #ifndef ST7789_DRIVER + #error Please define ST7789_DRIVER + #endif + #ifndef TFT_WIDTH + #error Please define TFT_WIDTH + #endif + #ifndef TFT_HEIGHT + #error Please define TFT_HEIGHT + #endif + #ifndef TFT_DC + #error Please define TFT_DC + #endif + #ifndef TFT_RST + #error Please define TFT_RST + #endif + #ifndef LOAD_GLCD + #error Please define LOAD_GLCD + #endif #endif - -#ifndef TFT_SLPIN -#define TFT_SLPIN 0x10 +#ifndef TFT_BL + #define TFT_BL -1 #endif -#define TFT_MOSI 21 -#define TFT_SCLK 22 -#define TFT_DC 18 -#define TFT_RST 5 -#define TFT_BL 26 // Display backlight control pin +#define USERMOD_ID_ST7789_DISPLAY 97 + +TFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT); // Invoke custom library -TFT_eSPI tft = TFT_eSPI(240, 240); // Invoke custom library +// Extra char (+1) for null +#define LINE_BUFFER_SIZE 20 // How often we are redrawing screen #define USER_LOOP_REFRESH_RATE_MS 1000 +extern int getSignalQuality(int rssi); + //class name. Use something descriptive and leave the ": public Usermod" part :) class St7789DisplayUsermod : public Usermod { private: //Private class members. You can declare variables and functions only accessible to your usermod here unsigned long lastTime = 0; + bool enabled = true; bool displayTurnedOff = false; long lastRedraw = 0; @@ -45,9 +61,70 @@ class St7789DisplayUsermod : public Usermod { uint8_t knownBrightness = 0; uint8_t knownMode = 0; uint8_t knownPalette = 0; - uint8_t tftcharwidth = 19; // Number of chars that fit on screen with text size set to 2 + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMinute = 99; + uint8_t knownHour = 99; + + const uint8_t tftcharwidth = 19; // Number of chars that fit on screen with text size set to 2 long lastUpdate = 0; + void center(String &line, uint8_t width) { + int len = line.length(); + if (len0; i--) line = ' ' + line; + for (byte i=line.length(); i 12) { + showHour -= 12; + isAM = false; + } else { + isAM = true; + } + } + + sprintf_P(lineBuffer, PSTR("%2d:%02d"), (useAMPM ? showHour : hourCurrent), minuteCurrent); + tft.setTextColor(TFT_WHITE); + tft.setTextSize(4); + tft.setCursor(60, 24); + tft.print(lineBuffer); + + tft.setTextSize(2); + tft.setCursor(186, 24); + //sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); + if (useAMPM) tft.print(isAM ? "AM" : "PM"); + //else tft.print(lineBuffer); + } + public: //Functions called by WLED @@ -57,6 +134,15 @@ class St7789DisplayUsermod : public Usermod { */ void setup() { + PinManagerPinType spiPins[] = { { spi_mosi, true }, { spi_miso, false}, { spi_sclk, true } }; + if (!pinManager.allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; } + PinManagerPinType displayPins[] = { { TFT_CS, true}, { TFT_DC, true}, { TFT_RST, true }, { TFT_BL, true } }; + if (!pinManager.allocateMultiplePins(displayPins, sizeof(displayPins)/sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { + pinManager.deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); + enabled = false; + return; + } + tft.init(); tft.setRotation(0); //Rotation here is set up for the text to be readable with the port on the left. Use 1 to flip. tft.fillScreen(TFT_BLACK); @@ -65,10 +151,10 @@ class St7789DisplayUsermod : public Usermod { tft.setTextDatum(MC_DATUM); tft.setTextSize(2); tft.print("Loading..."); - if (TFT_BL > 0) - { // TFT_BL has been set in the TFT_eSPI library - pinMode(TFT_BL, OUTPUT); // Set backlight pin to output mode - digitalWrite(TFT_BL, HIGH); // Turn backlight on. + if (TFT_BL >= 0) + { + pinMode(TFT_BL, OUTPUT); // Set backlight pin to output mode + digitalWrite(TFT_BL, HIGH); // Turn backlight on. } } @@ -91,192 +177,153 @@ class St7789DisplayUsermod : public Usermod { * Instead, use a timer check as shown here. */ void loop() { -// Check if we time interval for redrawing passes. - if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) + char buff[LINE_BUFFER_SIZE]; + + // Check if we time interval for redrawing passes. + if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { return; } - lastUpdate = millis(); + lastUpdate = millis(); -// Turn off display after 5 minutes with no change. - if(!displayTurnedOff && millis() - lastRedraw > 5*60*1000) + // Turn off display after 5 minutes with no change. + if (!displayTurnedOff && millis() - lastRedraw > 5*60*1000) { - digitalWrite(TFT_BL, LOW); // Turn backlight off. + if (TFT_BL >= 0) digitalWrite(TFT_BL, LOW); // Turn backlight off. displayTurnedOff = true; } -// Check if values which are shown on display changed from the last time. - if (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) - { - needRedraw = true; - } - else if (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP())) - { - needRedraw = true; - } - else if (knownBrightness != bri) - { - needRedraw = true; - } - else if (knownMode != strip.getMainSegment().mode) - { - needRedraw = true; - } - else if (knownPalette != strip.getMainSegment().palette) - { - needRedraw = true; - } + // Check if values which are shown on display changed from the last time. + if ((((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) || + (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : Network.localIP())) || + (knownBrightness != bri) || + (knownEffectSpeed != strip.getMainSegment().speed) || + (knownEffectIntensity != strip.getMainSegment().intensity) || + (knownMode != strip.getMainSegment().mode) || + (knownPalette != strip.getMainSegment().palette)) + { + needRedraw = true; + } - if (!needRedraw) - { - return; - } - needRedraw = false; - - if (displayTurnedOff) - { - digitalWrite(TFT_BL, TFT_BACKLIGHT_ON); // Turn backlight on. - displayTurnedOff = false; - } - lastRedraw = millis(); + if (!needRedraw) + { + return; + } + needRedraw = false; + + if (displayTurnedOff) + { + digitalWrite(TFT_BL, HIGH); // Turn backlight on. + displayTurnedOff = false; + } + lastRedraw = millis(); + + // Update last known values. + #if defined(ESP8266) + knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); + #else + knownSsid = WiFi.SSID(); + #endif + knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); + knownBrightness = bri; + knownMode = strip.getMainSegment().mode; + knownPalette = strip.getMainSegment().palette; + knownEffectSpeed = strip.getMainSegment().speed; + knownEffectIntensity = strip.getMainSegment().intensity; -// Update last known values. - #if defined(ESP8266) - knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); - #else - knownSsid = WiFi.SSID(); - #endif - knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); - knownBrightness = bri; - knownMode = strip.getMainSegment().mode; - knownPalette = strip.getMainSegment().palette; - - tft.fillScreen(TFT_BLACK); - tft.setTextSize(2); -// First row with Wifi name - tft.setTextColor(TFT_SILVER); - tft.setCursor(3, 40); - tft.print(knownSsid.substring(0, tftcharwidth > 1 ? tftcharwidth - 1 : 0)); -// Print `~` char to indicate that SSID is longer, than our dicplay - if (knownSsid.length() > tftcharwidth) - tft.print("~"); - -// Second row with AP IP and Password or IP - tft.setTextColor(TFT_GREEN); - tft.setTextSize(2); - tft.setCursor(3, 64); -// Print AP IP and password in AP mode or knownIP if AP not active. - - if (apActive) - { - tft.setTextColor(TFT_YELLOW); - tft.print("AP IP: "); - tft.print(knownIp); - tft.setCursor(3,86); - tft.setTextColor(TFT_YELLOW); - tft.print("AP Pass:"); - tft.print(apPass); - } - else - { - tft.setTextColor(TFT_GREEN); - tft.print("IP: "); - tft.print(knownIp); - tft.setCursor(3,86); - //tft.print("Signal Strength: "); - //tft.print(i.wifi.signal); - tft.setTextColor(TFT_WHITE); - tft.print("Bri: "); - tft.print(((float(bri)/255)*100),0); - tft.print("%"); - } + tft.fillScreen(TFT_BLACK); -// Third row with mode name - tft.setCursor(3, 108); - uint8_t qComma = 0; - bool insideQuotes = false; - uint8_t printedChars = 0; - char singleJsonSymbol; -// Find the mode name in JSON - for (size_t i = 0; i < strlen_P(JSON_mode_names); i++) - { - singleJsonSymbol = pgm_read_byte_near(JSON_mode_names + i); - switch (singleJsonSymbol) + showTime(); + + tft.setTextSize(2); + + // Wifi name + tft.setTextColor(TFT_GREEN); + tft.setCursor(0, 60); + String line = knownSsid.substring(0, tftcharwidth-1); + // Print `~` char to indicate that SSID is longer, than our display + if (knownSsid.length() > tftcharwidth) line = line.substring(0, tftcharwidth-1) + '~'; + center(line, tftcharwidth); + tft.print(line.c_str()); + + // Print AP IP and password in AP mode or knownIP if AP not active. + if (apActive) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownMode)) - break; - tft.setTextColor(TFT_MAGENTA); - tft.print(singleJsonSymbol); - printedChars++; + tft.setCursor(0, 84); + tft.print("AP IP: "); + tft.print(knownIp); + tft.setCursor(0,108); + tft.print("AP Pass:"); + tft.print(apPass); } - if ((qComma > knownMode) || (printedChars > tftcharwidth - 1)) - break; - } -// Fourth row with palette name - tft.setTextColor(TFT_YELLOW); - tft.setCursor(3, 130); - qComma = 0; - insideQuotes = false; - printedChars = 0; -// Looking for palette name in JSON. - for (size_t i = 0; i < strlen_P(JSON_palette_names); i++) - { - singleJsonSymbol = pgm_read_byte_near(JSON_palette_names + i); - switch (singleJsonSymbol) + else { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownPalette)) - break; - tft.print(singleJsonSymbol); - printedChars++; + tft.setCursor(0, 84); + line = knownIp.toString(); + center(line, tftcharwidth); + tft.print(line.c_str()); + // percent brightness + tft.setCursor(0, 120); + tft.setTextColor(TFT_WHITE); + tft.print("Bri: "); + tft.print((((int)bri*100)/255)); + tft.print("%"); + // signal quality + tft.setCursor(124,120); + tft.print("Sig: "); + if (getSignalQuality(WiFi.RSSI()) < 10) { + tft.setTextColor(TFT_RED); + } else if (getSignalQuality(WiFi.RSSI()) < 25) { + tft.setTextColor(TFT_ORANGE); + } else { + tft.setTextColor(TFT_GREEN); + } + tft.print(getSignalQuality(WiFi.RSSI())); + tft.setTextColor(TFT_WHITE); + tft.print("%"); } -// The following is modified from the code from the u8g2/u8g8 based code (knownPalette was knownMode) - if ((qComma > knownPalette) || (printedChars > tftcharwidth - 1)) - break; - } -// Fifth row with estimated mA usage - tft.setTextColor(TFT_SILVER); - tft.setCursor(3, 152); -// Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate). - tft.print("Current: "); - tft.print(strip.currentMilliamps); - tft.print("mA"); + + // mode name + tft.setTextColor(TFT_CYAN); + tft.setCursor(0, 144); + char lineBuffer[tftcharwidth+1]; + extractModeName(knownMode, JSON_mode_names, lineBuffer, tftcharwidth); + tft.print(lineBuffer); + + // palette name + tft.setTextColor(TFT_YELLOW); + tft.setCursor(0, 168); + extractModeName(knownPalette, JSON_palette_names, lineBuffer, tftcharwidth); + tft.print(lineBuffer); + + tft.setCursor(0, 192); + tft.setTextColor(TFT_SILVER); + sprintf_P(buff, PSTR("FX Spd:%3d Int:%3d"), effectSpeed, effectIntensity); + tft.print(buff); + + // Fifth row with estimated mA usage + tft.setTextColor(TFT_SILVER); + tft.setCursor(0, 216); + // Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate). + tft.print("Current: "); + tft.setTextColor(TFT_ORANGE); + tft.print(strip.currentMilliamps); + tft.print("mA"); } + /* * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. * Below it is shown how this could be used for e.g. a light sensor */ - /* void addToJsonInfo(JsonObject& root) { - int reading = 20; - //this code adds "u":{"Light":[20," lux"]} to the info object JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); - JsonArray lightArr = user.createNestedArray("Light"); //name - lightArr.add(reading); //value - lightArr.add(" lux"); //unit + JsonArray lightArr = user.createNestedArray("ST7789"); //name + lightArr.add(enabled?F("installed"):F("disabled")); //unit } - */ /* @@ -295,7 +342,7 @@ class St7789DisplayUsermod : public Usermod { */ void readFromJsonState(JsonObject& root) { - userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); } @@ -316,11 +363,23 @@ class St7789DisplayUsermod : public Usermod { */ void addToConfig(JsonObject& root) { - JsonObject top = root.createNestedObject("exampleUsermod"); - top["great"] = userVar0; //save this var persistently whenever settings are saved + JsonObject top = root.createNestedObject("ST7789"); + JsonArray pins = top.createNestedArray("pin"); + pins.add(TFT_CS); + pins.add(TFT_DC); + pins.add(TFT_RST); + pins.add(TFT_BL); + //top["great"] = userVar0; //save this var persistently whenever settings are saved } + void appendConfigData() { + oappend(SET_F("addInfo('ST7789:pin[]',0,'','SPI CS');")); + oappend(SET_F("addInfo('ST7789:pin[]',1,'','SPI DC');")); + oappend(SET_F("addInfo('ST7789:pin[]',2,'','SPI RST');")); + oappend(SET_F("addInfo('ST7789:pin[]',2,'','SPI BL');")); + } + /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) @@ -329,10 +388,11 @@ class St7789DisplayUsermod : public Usermod { * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) */ - void readFromConfig(JsonObject& root) + bool readFromConfig(JsonObject& root) { - JsonObject top = root["top"]; - userVar0 = top["great"] | 42; //The value right of the pipe "|" is the default value in case your setting was not present in cfg.json (e.g. first boot) + //JsonObject top = root["top"]; + //userVar0 = top["great"] | 42; //The value right of the pipe "|" is the default value in case your setting was not present in cfg.json (e.g. first boot) + return true; } @@ -342,7 +402,7 @@ class St7789DisplayUsermod : public Usermod { */ uint16_t getId() { - return USERMOD_ST7789_DISPLAY; + return USERMOD_ID_ST7789_DISPLAY; } //More methods can be added in the future, this example will then be extended. diff --git a/usermods/ST7789_display/Setup_ST7789_Display.h b/usermods/ST7789_display/Setup_ST7789_Display.h deleted file mode 100644 index 26d5c17ff4..0000000000 --- a/usermods/ST7789_display/Setup_ST7789_Display.h +++ /dev/null @@ -1,39 +0,0 @@ -// Setup for the ESP32 board with 1.5" 240x240 display - -// See SetupX_Template.h for all options available - -#define ST7789_DRIVER -#define TFT_SDA_READ // Display has a bidirectionsl SDA pin - -#define TFT_WIDTH 240 -#define TFT_HEIGHT 240 - -#define CGRAM_OFFSET // Library will add offsets required - -//#define TFT_MISO -1 - -#define TFT_MOSI 21 -#define TFT_SCLK 22 -//#define TFT_CS 5 -#define TFT_DC 18 -#define TFT_RST 5 - -#define TFT_BL 26 // Display backlight control pin - -#define TFT_BACKLIGHT_ON HIGH // HIGH or LOW are options - -#define LOAD_GLCD -#define LOAD_FONT2 -#define LOAD_FONT4 -#define LOAD_FONT6 -#define LOAD_FONT7 -#define LOAD_FONT8 -#define LOAD_GFXFF - -//#define SMOOTH_FONT - -//#define SPI_FREQUENCY 27000000 - #define SPI_FREQUENCY 40000000 // Maximum for ILI9341 - - -#define SPI_READ_FREQUENCY 6000000 // 6 MHz is the maximum SPI read speed for the ST7789V \ No newline at end of file diff --git a/usermods/Si7021_MQTT_HA/readme.md b/usermods/Si7021_MQTT_HA/readme.md new file mode 100644 index 0000000000..99a240f7dd --- /dev/null +++ b/usermods/Si7021_MQTT_HA/readme.md @@ -0,0 +1,69 @@ +# Si7021 to MQTT (with Home Assistant Auto Discovery) usermod + +This usermod implements support for [Si7021 I²C temperature and humidity sensors](https://www.silabs.com/documents/public/data-sheets/Si7021-A20.pdf). + +As of this writing, the sensor data will *not* be shown on the WLED UI, but it _is_ published via MQTT to WLED's "built-in" MQTT device topic. + +``` +temperature: $mqttDeviceTopic/si7021_temperature +humidity: $mqttDeviceTopic/si7021_humidity +``` + +The following sensors can also be published: + +``` +heat_index: $mqttDeviceTopic/si7021_heat_index +dew_point: $mqttDeviceTopic/si7021_dew_point +absolute_humidity: $mqttDeviceTopic/si7021_absolute_humidity +``` + +Sensor data will be updated/sent every 60 seconds. + +This usermod also supports Home Assistant Auto Discovery. + +## Settings via Usermod Setup + +- `enabled`: Enables this usermod +- `Send Dew Point, Abs. Humidity and Heat Index`: Enables additional sensors +- `Home Assistant MQTT Auto-Discovery`: Enables Home Assistant Auto Discovery + +# Installation + +## Hardware + +Attach the Si7021 sensor to the I²C interface. + +Default PINs ESP32: + +``` +SCL_PIN = 22; +SDA_PIN = 21; +``` + +Default PINs ESP8266: + +``` +SCL_PIN = 5; +SDA_PIN = 4; +``` + +## Software + +Add to `build_flags` in platformio.ini: + +``` + -D USERMOD_SI7021_MQTT_HA +``` + +Add to `lib_deps` in platformio.ini: + +``` + adafruit/Adafruit Si7021 Library @ 1.4.0 + BME280@~3.0.0 +``` + +# Credits + +- Aircoookie for making WLED +- Other usermod creators for example code (`sensors_to_mqtt` and `multi_relay` especially) +- You, for reading this diff --git a/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h b/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h new file mode 100644 index 0000000000..bdf7848446 --- /dev/null +++ b/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h @@ -0,0 +1,231 @@ +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +#pragma once + +// this is remixed from usermod_v2_SensorsToMqtt.h (sensors_to_mqtt usermod) +// and usermod_multi_relay.h (multi_relay usermod) + +#include "wled.h" +#include +#include // EnvironmentCalculations::HeatIndex(), ::DewPoint(), ::AbsoluteHumidity() + +Adafruit_Si7021 si7021; + +class Si7021_MQTT_HA : public Usermod +{ + private: + bool sensorInitialized = false; + bool mqttInitialized = false; + float sensorTemperature = 0; + float sensorHumidity = 0; + float sensorHeatIndex = 0; + float sensorDewPoint = 0; + float sensorAbsoluteHumidity= 0; + String mqttTemperatureTopic = ""; + String mqttHumidityTopic = ""; + String mqttHeatIndexTopic = ""; + String mqttDewPointTopic = ""; + String mqttAbsoluteHumidityTopic = ""; + unsigned long nextMeasure = 0; + bool enabled = false; + bool haAutoDiscovery = true; + bool sendAdditionalSensors = true; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _sendAdditionalSensors[]; + static const char _haAutoDiscovery[]; + + void _initializeSensor() + { + sensorInitialized = si7021.begin(); + Serial.printf("Si7021_MQTT_HA: sensorInitialized = %d\n", sensorInitialized); + } + + void _initializeMqtt() + { + mqttTemperatureTopic = String(mqttDeviceTopic) + "/si7021_temperature"; + mqttHumidityTopic = String(mqttDeviceTopic) + "/si7021_humidity"; + mqttHeatIndexTopic = String(mqttDeviceTopic) + "/si7021_heat_index"; + mqttDewPointTopic = String(mqttDeviceTopic) + "/si7021_dew_point"; + mqttAbsoluteHumidityTopic = String(mqttDeviceTopic) + "/si7021_absolute_humidity"; + + // Update and publish sensor data + _updateSensorData(); + _publishSensorData(); + + if (haAutoDiscovery) { + _publishHAMqttSensor("temperature", "Temperature", mqttTemperatureTopic, "temperature", "°C"); + _publishHAMqttSensor("humidity", "Humidity", mqttHumidityTopic, "humidity", "%"); + if (sendAdditionalSensors) { + _publishHAMqttSensor("heat_index", "Heat Index", mqttHeatIndexTopic, "temperature", "°C"); + _publishHAMqttSensor("dew_point", "Dew Point", mqttDewPointTopic, "", "°C"); + _publishHAMqttSensor("absolute_humidity", "Absolute Humidity", mqttAbsoluteHumidityTopic, "", "g/m³"); + } + } + + mqttInitialized = true; + } + + void _publishHAMqttSensor( + const String &name, + const String &friendly_name, + const String &state_topic, + const String &deviceClass, + const String &unitOfMeasurement) + { + if (WLED_MQTT_CONNECTED) { + String topic = String("homeassistant/sensor/") + mqttClientID + "/" + name + "/config"; + + StaticJsonDocument<300> doc; + + doc["name"] = String(serverDescription) + " " + friendly_name; + doc["state_topic"] = state_topic; + doc["unique_id"] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc["unit_of_measurement"] = unitOfMeasurement; + if (deviceClass != "") + doc["device_class"] = deviceClass; + doc["expire_after"] = 1800; + + JsonObject device = doc.createNestedObject("device"); // attach the sensor to the same device + device["name"] = String(serverDescription); + device["model"] = "WLED"; + device["manufacturer"] = "Aircoookie"; + device["identifiers"] = String("wled-") + String(serverDescription); + device["sw_version"] = VERSION; + + String payload; + serializeJson(doc, payload); + + mqtt->publish(topic.c_str(), 0, true, payload.c_str()); + } + } + + void _updateSensorData() + { + sensorTemperature = si7021.readTemperature(); + sensorHumidity = si7021.readHumidity(); + + // Serial.print("Si7021_MQTT_HA: Temperature: "); + // Serial.print(sensorTemperature, 2); + // Serial.print("\tHumidity: "); + // Serial.print(sensorHumidity, 2); + + if (sendAdditionalSensors) { + EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius); + sensorHeatIndex = EnvironmentCalculations::HeatIndex(sensorTemperature, sensorHumidity, envTempUnit); + sensorDewPoint = EnvironmentCalculations::DewPoint(sensorTemperature, sensorHumidity, envTempUnit); + sensorAbsoluteHumidity = EnvironmentCalculations::AbsoluteHumidity(sensorTemperature, sensorHumidity, envTempUnit); + + // Serial.print("\tHeat Index: "); + // Serial.print(sensorHeatIndex, 2); + // Serial.print("\tDew Point: "); + // Serial.print(sensorDewPoint, 2); + // Serial.print("\tAbsolute Humidity: "); + // Serial.println(sensorAbsoluteHumidity, 2); + } + // else + // Serial.println(""); + } + + void _publishSensorData() + { + if (WLED_MQTT_CONNECTED) { + mqtt->publish(mqttTemperatureTopic.c_str(), 0, false, String(sensorTemperature).c_str()); + mqtt->publish(mqttHumidityTopic.c_str(), 0, false, String(sensorHumidity).c_str()); + if (sendAdditionalSensors) { + mqtt->publish(mqttHeatIndexTopic.c_str(), 0, false, String(sensorHeatIndex).c_str()); + mqtt->publish(mqttDewPointTopic.c_str(), 0, false, String(sensorDewPoint).c_str()); + mqtt->publish(mqttAbsoluteHumidityTopic.c_str(), 0, false, String(sensorAbsoluteHumidity).c_str()); + } + } + } + + public: + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_sendAdditionalSensors)] = sendAdditionalSensors; + top[FPSTR(_haAutoDiscovery)] = haAutoDiscovery; + } + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + configComplete &= getJsonValue(top[FPSTR(_sendAdditionalSensors)], sendAdditionalSensors); + configComplete &= getJsonValue(top[FPSTR(_haAutoDiscovery)], haAutoDiscovery); + + return configComplete; + } + + void onMqttConnect(bool sessionPresent) { + if (mqttDeviceTopic[0] != 0) + _initializeMqtt(); + } + + void setup() + { + if (enabled) { + Serial.println("Si7021_MQTT_HA: Starting!"); + Serial.println("Si7021_MQTT_HA: Initializing sensors.. "); + _initializeSensor(); + } + } + + // gets called every time WiFi is (re-)connected. + void connected() + { + nextMeasure = millis() + 5000; // Schedule next measure in 5 seconds + } + + void loop() + { + yield(); + if (!enabled || strip.isUpdating()) return; // !sensorFound || + + unsigned long tempTimer = millis(); + + if (tempTimer > nextMeasure) { + nextMeasure = tempTimer + 60000; // Schedule next measure in 60 seconds + + if (!sensorInitialized) { + Serial.println("Si7021_MQTT_HA: Error! Sensors not initialized in loop()!"); + _initializeSensor(); + return; // lets try again next loop + } + + if (WLED_MQTT_CONNECTED) { + if (!mqttInitialized) + _initializeMqtt(); + + // Update and publish sensor data + _updateSensorData(); + _publishSensorData(); + } + else { + Serial.println("Si7021_MQTT_HA: Missing MQTT connection. Not publishing data"); + mqttInitialized = false; + } + } + } + + uint16_t getId() + { + return USERMOD_ID_SI7021_MQTT_HA; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char Si7021_MQTT_HA::_name[] PROGMEM = "Si7021 MQTT (Home Assistant)"; +const char Si7021_MQTT_HA::_enabled[] PROGMEM = "enabled"; +const char Si7021_MQTT_HA::_sendAdditionalSensors[] PROGMEM = "Send Dew Point, Abs. Humidity and Heat Index"; +const char Si7021_MQTT_HA::_haAutoDiscovery[] PROGMEM = "Home Assistant MQTT Auto-Discovery"; diff --git a/usermods/TTGO-T-Display/README.md b/usermods/TTGO-T-Display/README.md index 872beeb8cf..439f9832dd 100644 --- a/usermods/TTGO-T-Display/README.md +++ b/usermods/TTGO-T-Display/README.md @@ -1,19 +1,19 @@ # TTGO T-Display ESP32 with 240x135 TFT via SPI with TFT_eSPI -This usermod allows use of the TTGO T-Display ESP32 module with integrated 240x135 display +This usermod enables use of the TTGO 240x135 T-Display ESP32 module for controlling WLED and showing the following information: * Current SSID -* IP address if obtained - * If connected to a network, current brightness % is shown - * in AP mode AP IP and password are shown +* IP address, if obtained + * If connected to a network, current brightness percentage is shown + * In AP mode, AP, IP and password are shown * Current effect * Current palette -* Estimated current in mA is shown (NOTE: for this to be a reasonable value, the correct LED type must be specified in the LED Prefs section) +* Estimated current in mA (NOTE: for this to be a reasonable value, the correct LED type must be specified in the LED Prefs section) -Button pin is mapped to the onboard button next to the side actuated reset button of the TTGO T-Display board. +Button pin is mapped to the onboard button adjacent to the reset button of the TTGO T-Display board. -I have designed a 3D printed case around this board and an ["ElectroCookie"](https://amzn.to/2WCNeeA) project board, a [level shifter](https://amzn.to/3hbKu18), a [buck regulator](https://amzn.to/3mLMy0W), and a DC [power jack](https://amzn.to/3phj9NZ). I use 12V WS2815 LED strips for my projects, and power them with 12V power supplies, so the regulator drops the voltage to the 5V level I need to power the ESP module and the level shifter. If there is any interest in this case, which elevates the board and display on some custom extended headers to make place the screen at the top of the enclosure (with accessible buttons), let me know, and I could post the STL files. It is a bit tricky to get the height correct, so I also designed a one-time use 3D printed solder fixture to set the board in the right location and at the correct height for the housing. (It is one-time use because it has to be cut off after soldering to be able to remove it). I didn't think the effort to make it in multiple pieces was worthwhile. +I have designed a 3D printed case around this board and an ["ElectroCookie"](https://amzn.to/2WCNeeA) project board, a [level shifter](https://amzn.to/3hbKu18), a [buck regulator](https://amzn.to/3mLMy0W), and a DC [power jack](https://amzn.to/3phj9NZ). I use 12V WS2815 LED strips for my projects, and power them with 12V power supplies. The regulator supplies 5V for the ESP module and the level shifter. If there is any interest in this case which elevates the board and display on custom extended standoffs to place the screen at the top of the enclosure (with accessible buttons), let me know, and I will post the STL files. It is a bit tricky to get the height correct, so I also designed a one-time use 3D printed solder fixture to set the board in the right location and at the correct height for the housing. (It is one-time use because it has to be cut off after soldering to be able to remove it). I didn't think the effort to make it in multiple pieces was worthwhile. -Usermod based on a rework of the ssd1306_i2c_oled_u8g2 usermod from the WLED repo. +Based on a rework of the ssd1306_i2c_oled_u8g2 usermod from the WLED repo. ## Hardware ![Hardware](assets/ttgo_hardware1.png) @@ -30,8 +30,8 @@ Usermod based on a rework of the ssd1306_i2c_oled_u8g2 usermod from the WLED rep Functionality checked with: * TTGO T-Display * PlatformIO -* Group of 4 individual Neopixels from Adafruit, and a several full strings of 12v WS2815 LEDs. -* The hardware design shown above should be limited to shorter strings. For larger strings, I use a different setup with a dedicated 12v power supply and power them directly off the supply (in addition to dropping the 12v supply down to 5v with a buck regulator for the ESP module and level shifter). +* Group of 4 individual Neopixels from Adafruit and several full strings of 12v WS2815 LEDs. +* The hardware design shown above should be limited to shorter strings. For larger strings, I use a different setup with a dedicated 12v power supply and power them directly from said supply (in addition to dropping the 12v to 5v with a buck regulator for the ESP module and level shifter). ## Setup Needed: * As with all usermods, copy the usermod.cpp file from the TTGO-T-Display usermod folder to the wled00 folder (replacing the default usermod.cpp file). @@ -51,24 +51,24 @@ lib_deps = ... ``` -Also, while in the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows: +In the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows: Comment out the line described below: ```ini -# Travis CI binaries (comment this out when building for single board) -; default_envs = travis_esp8266, esp01, esp01_1m_ota, travis_esp32 +# Release binaries +; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32s2_saola, esp32c3 ``` -and UNCOMMENT the following line in the 'Single binaries' section: +and uncomment the following line in the 'Single binaries' section: ```ini default_envs = esp32dev ``` -Save the `platformio.ini` file. Once this is saved, the required library files should be automatically downloaded for modifications in a later step. +Save the `platformio.ini` file. Once saved, the required library files should be automatically downloaded for modifications in a later step. ### Platformio_overrides.ini (added) Copy the `platformio_overrides.ini` file which is contained in the `usermods/TTGO-T-Display/` folder into the root of your project folder. This file contains an override that remaps the button pin of WLED to use the on-board button to the right of the USB-C connector (when viewed with the port oriented downward - see hardware photo). ### TFT_eSPI Library Adjustments (board selection) -We need to modify a file in the `TFT_eSPI` library to select the correct board. If you followed the directions to modify and save the `platformio.ini` file above, the `User_Setup_Select.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI_ID1559` folder. +You need to modify a file in the `TFT_eSPI` library to select the correct board. If you followed the directions to modify and save the `platformio.ini` file above, the `User_Setup_Select.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI_ID1559` folder. Modify the `User_Setup_Select.h` file as follows: * Comment out the following line (which is the 'default' setup file): @@ -80,12 +80,12 @@ Modify the `User_Setup_Select.h` file as follows: #include // Setup file for ESP32 and TTGO T-Display ST7789V SPI bus TFT ``` -Run the build and it should complete correctly. If you see a failure like this: +Build the file. If you see a failure like this: ```ini xtensa-esp32-elf-g++: error: wled00\wled00.ino.cpp: No such file or directory xtensa-esp32-elf-g++: fatal error: no input files ``` -Just try building again - I find that sometimes this happens on the first build attempt and subsequent attempts will build correctly. +try building again. Sometimes this happens on the first build attempt and subsequent attempts build correctly. ## Arduino IDE -- UNTESTED \ No newline at end of file +- UNTESTED diff --git a/usermods/TTGO-T-Display/usermod.cpp b/usermods/TTGO-T-Display/usermod.cpp index 9e08a001a1..cbba07771d 100644 --- a/usermods/TTGO-T-Display/usermod.cpp +++ b/usermods/TTGO-T-Display/usermod.cpp @@ -3,7 +3,7 @@ * This file allows you to add own functionality to WLED more easily * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) - * bytes 2400+ are currently ununsed, but might be used for future wled features + * bytes 2400+ are currently unused, but might be used for future wled features */ /* @@ -144,7 +144,7 @@ void userLoop() { // First row with Wifi name tft.setCursor(1, 1); tft.print(knownSsid.substring(0, tftcharwidth > 1 ? tftcharwidth - 1 : 0)); - // Print `~` char to indicate that SSID is longer, than our dicplay + // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > tftcharwidth) tft.print("~"); @@ -177,58 +177,15 @@ void userLoop() { // Third row with mode name tft.setCursor(1, 68); - uint8_t qComma = 0; - bool insideQuotes = false; - uint8_t printedChars = 0; - char singleJsonSymbol; - // Find the mode name in JSON - for (size_t i = 0; i < strlen_P(JSON_mode_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_mode_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownMode)) - break; - tft.print(singleJsonSymbol); - printedChars++; - } - if ((qComma > knownMode) || (printedChars > tftcharwidth - 1)) - break; - } + char lineBuffer[tftcharwidth+1]; + extractModeName(knownMode, JSON_mode_names, lineBuffer, tftcharwidth); + tft.print(lineBuffer); + // Fourth row with palette name tft.setCursor(1, 90); - qComma = 0; - insideQuotes = false; - printedChars = 0; - // Looking for palette name in JSON. - for (size_t i = 0; i < strlen_P(JSON_palette_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_palette_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownPalette)) - break; - tft.print(singleJsonSymbol); - printedChars++; - } - // The following is modified from the code from the u8g2/u8g8 based code (knownPalette was knownMode) - if ((qComma > knownPalette) || (printedChars > tftcharwidth - 1)) - break; - } + extractModeName(knownPalette, JSON_palette_names, lineBuffer, tftcharwidth); + tft.print(lineBuffer); + // Fifth row with estimated mA usage tft.setCursor(1, 112); // Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate). diff --git a/usermods/Temperature/platformio_override.ini b/usermods/Temperature/platformio_override.ini index d9e3fbace1..0e354da9ef 100644 --- a/usermods/Temperature/platformio_override.ini +++ b/usermods/Temperature/platformio_override.ini @@ -1,13 +1,12 @@ ; Options ; ------- ; USERMOD_DALLASTEMPERATURE - define this to have this user mod included wled00\usermods_list.cpp -; USERMOD_DALLASTEMPERATURE_CELSIUS - define this to report temperatures in degrees celsius, otherwise fahrenheit will be reported ; USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds -; USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT - the number of milliseconds after boot to take first measurement, defaults to 20 seconds ; [env:d1_mini_usermod_dallas_temperature_C] extends = env:d1_mini -build_flags = ${common.build_flags_esp8266} -D USERMOD_DALLASTEMPERATURE -D USERMOD_DALLASTEMPERATURE_CELSIUS +build_flags = ${common.build_flags_esp8266} -D USERMOD_DALLASTEMPERATURE lib_deps = ${env.lib_deps} - milesburton/DallasTemperature@^3.9.0 - OneWire@~2.3.5 + paulstoffregen/OneWire@~2.3.7 +# you may want to use following with ESP32 +; https://github.com/blazoncek/OneWire.git # fixes Sensor error on ESP32 \ No newline at end of file diff --git a/usermods/Temperature/readme.md b/usermods/Temperature/readme.md index ac2823c3f8..b41e3e1199 100644 --- a/usermods/Temperature/readme.md +++ b/usermods/Temperature/readme.md @@ -1,22 +1,24 @@ # Temperature usermod -Based on the excellent `QuinLED_Dig_Uno_Temp_MQTT` by srg74 and 400killer! -This usermod will read from an attached DS18B20 temperature sensor (as available on the QuinLED Dig-Uno) -The temperature is displayed both in the Info section of the web UI as well as published to the `/temperature` MQTT topic if enabled. -This usermod may be expanded with support for different sensor types in the future. +Based on the excellent `QuinLED_Dig_Uno_Temp_MQTT` usermod by srg74 and 400killer! +Reads an attached DS18B20 temperature sensor (as available on the QuinLED Dig-Uno) +Temperature is displayed in both the Info section of the web UI as well as published to the `/temperature` MQTT topic, if enabled. +May be expanded with support for different sensor types in the future. If temperature sensor is not detected during boot, this usermod will be disabled. +Maintained by @blazoncek + ## Installation Copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. ### Define Your Options -* `USERMOD_DALLASTEMPERATURE` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 20 seconds +* `USERMOD_DALLASTEMPERATURE` - enables this user mod wled00/usermods_list.cpp +* `USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL` - number of milliseconds between measurements, defaults to 60000 ms (60s) -All parameters can be configured at runtime using Usermods settings page, including pin, selection to display temerature in degrees Celsius or Farenheit mand measurement interval. +All parameters can be configured at runtime via the Usermods settings page, including pin, temperature in degrees Celsius or Fahrenheit and measurement interval. ## Project link @@ -27,7 +29,6 @@ All parameters can be configured at runtime using Usermods settings page, includ If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:d1_mini_usermod_dallas_temperature_C`. - If you are not using `platformio_override.ini`, you might have to uncomment `OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: ```ini @@ -43,16 +44,20 @@ default_envs = d1_mini lib_deps = ... #For Dallas sensor uncomment following line - OneWire@~2.3.5 -... + OneWire@~2.3.7 + # ... or you may want to use following with ESP32 +; https://github.com/blazoncek/OneWire.git # fixes Sensor error on ESP32... ``` ## Change Log 2020-09-12 -* Changed to use async, non-blocking implementation -* Do not report low temperatures that indicate an error to mqtt +* Changed to use async non-blocking implementation +* Do not report erroneous low temperatures to MQTT * Disable plugin if temperature sensor not detected * Report the number of seconds until the first read in the info screen instead of sensor error 2021-04 -* Adaptation for runtime configuration. \ No newline at end of file +* Adaptation for runtime configuration. +2023-05 +* Rewrite to conform to newer recommendations. +* Recommended @blazoncek fork of OneWire for ESP32 to avoid Sensor error \ No newline at end of file diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index 40df0e5333..a15baf8785 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -29,6 +29,7 @@ class UsermodTemperature : public Usermod { bool degC = true; // using parasite power on the sensor bool parasite = false; + int8_t parasitePin = -1; // how often do we read from sensor? unsigned long readingInterval = USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; // set last reading as "40 sec before boot", so first reading is taken after 20 sec @@ -46,285 +47,386 @@ class UsermodTemperature : public Usermod { bool enabled = true; + bool HApublished = false; + // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _readInterval[]; static const char _parasite[]; + static const char _parasitePin[]; //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 - float readDallas() { - byte data[9]; - int16_t result; // raw data from sensor - float retVal = -127.0f; - if (oneWire->reset()) { // if reset() fails there are no OneWire devices - oneWire->skip(); // skip ROM - oneWire->write(0xBE); // read (temperature) from EEPROM - oneWire->read_bytes(data, 9); // first 2 bytes contain temperature - #ifdef WLED_DEBUG - if (OneWire::crc8(data,8) != data[8]) { - DEBUG_PRINTLN(F("CRC error reading temperature.")); - for (byte i=0; i < 9; i++) DEBUG_PRINTF("0x%02X ", data[i]); - DEBUG_PRINT(F(" => ")); - DEBUG_PRINTF("0x%02X\n", OneWire::crc8(data,8)); - } - #endif - switch(sensorFound) { - case 0x10: // DS18S20 has 9-bit precision - result = (data[1] << 8) | data[0]; - retVal = float(result) * 0.5f; - break; - case 0x22: // DS18B20 - case 0x28: // DS1822 - case 0x3B: // DS1825 - case 0x42: // DS28EA00 - result = (data[1]<<4) | (data[0]>>4); // we only need whole part, we will add fraction when returning - if (data[1] & 0x80) result |= 0xF000; // fix negative value - retVal = float(result) + ((data[0] & 0x08) ? 0.5f : 0.0f); - break; - } - } - for (byte i=1; i<9; i++) data[0] &= data[i]; - return data[0]==0xFF ? -127.0f : retVal; - } - - void requestTemperatures() { - DEBUG_PRINTLN(F("Requesting temperature.")); - oneWire->reset(); - oneWire->skip(); // skip ROM - oneWire->write(0x44,parasite); // request new temperature reading (TODO: parasite would need special handling) - lastTemperaturesRequest = millis(); - waitingForConversion = true; - } - - void readTemperature() { - temperature = readDallas(); - lastMeasurement = millis(); - waitingForConversion = false; - //DEBUG_PRINTF("Read temperature %2.1f.\n", temperature); // does not work properly on 8266 - DEBUG_PRINT(F("Read temperature ")); - DEBUG_PRINTLN(temperature); - } - - bool findSensor() { - DEBUG_PRINTLN(F("Searching for sensor...")); - uint8_t deviceAddress[8] = {0,0,0,0,0,0,0,0}; - // find out if we have DS18xxx sensor attached - oneWire->reset_search(); - delay(10); - while (oneWire->search(deviceAddress)) { - DEBUG_PRINTLN(F("Found something...")); - if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) { - switch (deviceAddress[0]) { - case 0x10: // DS18S20 - case 0x22: // DS18B20 - case 0x28: // DS1822 - case 0x3B: // DS1825 - case 0x42: // DS28EA00 - DEBUG_PRINTLN(F("Sensor found.")); - sensorFound = deviceAddress[0]; - DEBUG_PRINTF("0x%02X\n", sensorFound); - return true; - } - } - } - DEBUG_PRINTLN(F("Sensor NOT found.")); - return false; - } + float readDallas(); + void requestTemperatures(); + void readTemperature(); + bool findSensor(); +#ifndef WLED_DISABLE_MQTT + void publishHomeAssistantAutodiscovery(); +#endif public: - void setup() { - int retries = 10; - sensorFound = 0; - temperature = -127.0f; // default to -127, DS18B20 only goes down to -50C - if (enabled) { - // config says we are enabled - DEBUG_PRINTLN(F("Allocating temperature pin...")); - // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (temperaturePin >= 0 && pinManager.allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { - oneWire = new OneWire(temperaturePin); - if (oneWire->reset()) { - while (!findSensor() && retries--) { - delay(25); // try to find sensor - } - } - } else { - if (temperaturePin >= 0) { - DEBUG_PRINTLN(F("Temperature pin allocation failed.")); - } - temperaturePin = -1; // allocation failed - } - } - lastMeasurement = millis() - readingInterval + 10000; - initDone = true; - } - - void loop() { - if (!enabled || !sensorFound || strip.isUpdating()) return; - - static uint8_t errorCount = 0; - unsigned long now = millis(); - - // check to see if we are due for taking a measurement - // lastMeasurement will not be updated until the conversion - // is complete the the reading is finished - if (now - lastMeasurement < readingInterval) return; - - // we are due for a measurement, if we are not already waiting - // for a conversion to complete, then make a new request for temps - if (!waitingForConversion) { - requestTemperatures(); - return; - } - - // we were waiting for a conversion to complete, have we waited log enough? - if (now - lastTemperaturesRequest >= 750 /* 93.75ms per the datasheet but can be up to 750ms */) { - readTemperature(); - if (getTemperatureC() < -100.0f) { - if (++errorCount > 10) sensorFound = 0; - lastMeasurement = now - readingInterval + 300; // force new measurement in 300ms - return; - } - errorCount = 0; - - if (WLED_MQTT_CONNECTED) { - char subuf[64]; - strcpy(subuf, mqttDeviceTopic); - if (temperature > -100.0f) { - // dont publish super low temperature as the graph will get messed up - // the DallasTemperature library returns -127C or -196.6F when problem - // reading the sensor - strcat_P(subuf, PSTR("/temperature")); - mqtt->publish(subuf, 0, false, String(getTemperatureC()).c_str()); - strcat_P(subuf, PSTR("_f")); - mqtt->publish(subuf, 0, false, String(getTemperatureF()).c_str()); - } else { - // publish something else to indicate status? - } - } - } - } - /* * API calls te enable data exchange between WLED modules */ - inline float getTemperatureC() { - return (float)temperature; - } - inline float getTemperatureF() { - return (float)temperature * 1.8f + 32; - } - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - void addToJsonInfo(JsonObject& root) { - // dont add temperature to info if we are disabled - if (!enabled) return; + inline float getTemperatureC() { return temperature; } + inline float getTemperatureF() { return temperature * 1.8f + 32.0f; } + float getTemperature(); + const char *getTemperatureUnit(); + uint16_t getId() { return USERMOD_ID_TEMPERATURE; } + + void setup(); + void loop(); + //void connected(); +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent); +#endif + //void onUpdateBegin(bool init); - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); + //bool handleButton(uint8_t b); + //void handleOverlayDraw(); - JsonArray temp = user.createNestedArray(FPSTR(_name)); - //temp.add(F("Loaded.")); + void addToJsonInfo(JsonObject& root); + //void addToJsonState(JsonObject &root); + //void readFromJsonState(JsonObject &root); + void addToConfig(JsonObject &root); + bool readFromConfig(JsonObject &root); - if (temperature <= -100.0f) { - temp.add(0); - temp.add(F(" Sensor Error!")); - return; - } + void appendConfigData(); +}; - temp.add(degC ? getTemperatureC() : getTemperatureF()); - if (degC) temp.add(F("°C")); - else temp.add(F("°F")); +//Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 +float UsermodTemperature::readDallas() { + byte data[9]; + int16_t result; // raw data from sensor + float retVal = -127.0f; + if (oneWire->reset()) { // if reset() fails there are no OneWire devices + oneWire->skip(); // skip ROM + oneWire->write(0xBE); // read (temperature) from EEPROM + oneWire->read_bytes(data, 9); // first 2 bytes contain temperature + #ifdef WLED_DEBUG + if (OneWire::crc8(data,8) != data[8]) { + DEBUG_PRINTLN(F("CRC error reading temperature.")); + for (byte i=0; i < 9; i++) DEBUG_PRINTF("0x%02X ", data[i]); + DEBUG_PRINT(F(" => ")); + DEBUG_PRINTF("0x%02X\n", OneWire::crc8(data,8)); } - - /** - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - //void addToJsonState(JsonObject &root) - //{ - //} - - /** - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - * Read "_" from json state and and change settings (i.e. GPIO pin) used. - */ - //void readFromJsonState(JsonObject &root) { - // if (!initDone) return; // prevent crash on boot applyPreset() - //} - - /** - * addToConfig() (called from set.cpp) stores persistent properties to cfg.json - */ - void addToConfig(JsonObject &root) { - // we add JSON object: {"Temperature": {"pin": 0, "degC": true}} - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; - top["pin"] = temperaturePin; // usermodparam - top["degC"] = degC; // usermodparam - top[FPSTR(_readInterval)] = readingInterval / 1000; - top[FPSTR(_parasite)] = parasite; - DEBUG_PRINTLN(F("Temperature config saved.")); + #endif + switch(sensorFound) { + case 0x10: // DS18S20 has 9-bit precision + result = (data[1] << 8) | data[0]; + retVal = float(result) * 0.5f; + break; + case 0x22: // DS18B20 + case 0x28: // DS1822 + case 0x3B: // DS1825 + case 0x42: // DS28EA00 + result = (data[1]<<4) | (data[0]>>4); // we only need whole part, we will add fraction when returning + if (data[1] & 0x80) result |= 0xF000; // fix negative value + retVal = float(result) + ((data[0] & 0x08) ? 0.5f : 0.0f); + break; } - - /** - * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - * - * The function should return true if configuration was successfully loaded or false if there was no configuration. - */ - bool readFromConfig(JsonObject &root) { - // we look for JSON object: {"Temperature": {"pin": 0, "degC": true}} - int8_t newTemperaturePin = temperaturePin; - DEBUG_PRINT(FPSTR(_name)); - - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; + } + for (byte i=1; i<9; i++) data[0] &= data[i]; + return data[0]==0xFF ? -127.0f : retVal; +} + +void UsermodTemperature::requestTemperatures() { + DEBUG_PRINTLN(F("Requesting temperature.")); + oneWire->reset(); + oneWire->skip(); // skip ROM + oneWire->write(0x44,parasite); // request new temperature reading + if (parasite && parasitePin >=0 ) digitalWrite(parasitePin, HIGH); // has to happen within 10us (open MOSFET) + lastTemperaturesRequest = millis(); + waitingForConversion = true; +} + +void UsermodTemperature::readTemperature() { + if (parasite && parasitePin >=0 ) digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET) + temperature = readDallas(); + lastMeasurement = millis(); + waitingForConversion = false; + //DEBUG_PRINTF("Read temperature %2.1f.\n", temperature); // does not work properly on 8266 + DEBUG_PRINT(F("Read temperature ")); + DEBUG_PRINTLN(temperature); +} + +bool UsermodTemperature::findSensor() { + DEBUG_PRINTLN(F("Searching for sensor...")); + uint8_t deviceAddress[8] = {0,0,0,0,0,0,0,0}; + // find out if we have DS18xxx sensor attached + oneWire->reset_search(); + delay(10); + while (oneWire->search(deviceAddress)) { + DEBUG_PRINTLN(F("Found something...")); + if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) { + switch (deviceAddress[0]) { + case 0x10: // DS18S20 + case 0x22: // DS18B20 + case 0x28: // DS1822 + case 0x3B: // DS1825 + case 0x42: // DS28EA00 + DEBUG_PRINTLN(F("Sensor found.")); + sensorFound = deviceAddress[0]; + DEBUG_PRINTF("0x%02X\n", sensorFound); + return true; } + } + } + DEBUG_PRINTLN(F("Sensor NOT found.")); + return false; +} + +#ifndef WLED_DISABLE_MQTT +void UsermodTemperature::publishHomeAssistantAutodiscovery() { + if (!WLED_MQTT_CONNECTED) return; + + char json_str[1024], buf[128]; + size_t payload_size; + StaticJsonDocument<1024> json; + + sprintf_P(buf, PSTR("%s Temperature"), serverDescription); + json[F("name")] = buf; + strcpy(buf, mqttDeviceTopic); + strcat_P(buf, PSTR("/temperature")); + json[F("state_topic")] = buf; + json[F("device_class")] = F("temperature"); + json[F("unique_id")] = escapedMac.c_str(); + json[F("unit_of_measurement")] = F("°C"); + payload_size = serializeJson(json, json_str); + + sprintf_P(buf, PSTR("homeassistant/sensor/%s/config"), escapedMac.c_str()); + mqtt->publish(buf, 0, true, json_str, payload_size); + HApublished = true; +} +#endif - enabled = top[FPSTR(_enabled)] | enabled; - newTemperaturePin = top["pin"] | newTemperaturePin; - degC = top["degC"] | degC; - readingInterval = top[FPSTR(_readInterval)] | readingInterval/1000; - readingInterval = min(120,max(10,(int)readingInterval)) * 1000; // convert to ms - parasite = top[FPSTR(_parasite)] | parasite; - - if (!initDone) { - // first run: reading from cfg.json - temperaturePin = newTemperaturePin; - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing paramters from settings page - if (newTemperaturePin != temperaturePin) { - DEBUG_PRINTLN(F("Re-init temperature.")); - // deallocate pin and release memory - delete oneWire; - pinManager.deallocatePin(temperaturePin, PinOwner::UM_Temperature); - temperaturePin = newTemperaturePin; - // initialise - setup(); +void UsermodTemperature::setup() { + int retries = 10; + sensorFound = 0; + temperature = -127.0f; // default to -127, DS18B20 only goes down to -50C + if (enabled) { + // config says we are enabled + DEBUG_PRINTLN(F("Allocating temperature pin...")); + // pin retrieved from cfg.json (readFromConfig()) prior to running setup() + if (temperaturePin >= 0 && pinManager.allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { + oneWire = new OneWire(temperaturePin); + if (oneWire->reset()) { + while (!findSensor() && retries--) { + delay(25); // try to find sensor } } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_parasite)].isNull(); + if (parasite && pinManager.allocatePin(parasitePin, true, PinOwner::UM_Temperature)) { + pinMode(parasitePin, OUTPUT); + digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET) + } else { + parasitePin = -1; + } + } else { + if (temperaturePin >= 0) { + DEBUG_PRINTLN(F("Temperature pin allocation failed.")); + } + temperaturePin = -1; // allocation failed } + } + lastMeasurement = millis() - readingInterval + 10000; + initDone = true; +} + +void UsermodTemperature::loop() { + if (!enabled || !sensorFound || strip.isUpdating()) return; + + static uint8_t errorCount = 0; + unsigned long now = millis(); + + // check to see if we are due for taking a measurement + // lastMeasurement will not be updated until the conversion + // is complete the the reading is finished + if (now - lastMeasurement < readingInterval) return; + + // we are due for a measurement, if we are not already waiting + // for a conversion to complete, then make a new request for temps + if (!waitingForConversion) { + requestTemperatures(); + return; + } + + // we were waiting for a conversion to complete, have we waited log enough? + if (now - lastTemperaturesRequest >= 750 /* 93.75ms per the datasheet but can be up to 750ms */) { + readTemperature(); + if (getTemperatureC() < -100.0f) { + if (++errorCount > 10) sensorFound = 0; + lastMeasurement = now - readingInterval + 300; // force new measurement in 300ms + return; + } + errorCount = 0; + +#ifndef WLED_DISABLE_MQTT + if (WLED_MQTT_CONNECTED) { + char subuf[64]; + strcpy(subuf, mqttDeviceTopic); + if (temperature > -100.0f) { + // dont publish super low temperature as the graph will get messed up + // the DallasTemperature library returns -127C or -196.6F when problem + // reading the sensor + strcat_P(subuf, PSTR("/temperature")); + mqtt->publish(subuf, 0, false, String(getTemperatureC()).c_str()); + strcat_P(subuf, PSTR("_f")); + mqtt->publish(subuf, 0, false, String(getTemperatureF()).c_str()); + } else { + // publish something else to indicate status? + } + } +#endif + } +} + +/** + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ +//void UsermodTemperature::connected() {} + +#ifndef WLED_DISABLE_MQTT +/** + * subscribe to MQTT topic if needed + */ +void UsermodTemperature::onMqttConnect(bool sessionPresent) { + //(re)subscribe to required topics + //char subuf[64]; + if (mqttDeviceTopic[0] != 0) { + publishHomeAssistantAutodiscovery(); + } +} +#endif - uint16_t getId() - { - return USERMOD_ID_TEMPERATURE; +/* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ +void UsermodTemperature::addToJsonInfo(JsonObject& root) { + // dont add temperature to info if we are disabled + if (!enabled) return; + + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray temp = user.createNestedArray(FPSTR(_name)); + + if (temperature <= -100.0f) { + temp.add(0); + temp.add(F(" Sensor Error!")); + return; + } + + temp.add(getTemperature()); + temp.add(getTemperatureUnit()); + + JsonObject sensor = root[F("sensor")]; + if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); + temp = sensor.createNestedArray(F("temperature")); + temp.add(getTemperature()); + temp.add(getTemperatureUnit()); +} + +/** + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ +//void UsermodTemperature::addToJsonState(JsonObject &root) +//{ +//} + +/** + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + * Read "_" from json state and and change settings (i.e. GPIO pin) used. + */ +//void UsermodTemperature::readFromJsonState(JsonObject &root) { +// if (!initDone) return; // prevent crash on boot applyPreset() +//} + +/** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ +void UsermodTemperature::addToConfig(JsonObject &root) { + // we add JSON object: {"Temperature": {"pin": 0, "degC": true}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top["pin"] = temperaturePin; // usermodparam + top["degC"] = degC; // usermodparam + top[FPSTR(_readInterval)] = readingInterval / 1000; + top[FPSTR(_parasite)] = parasite; + top[FPSTR(_parasitePin)] = parasitePin; + DEBUG_PRINTLN(F("Temperature config saved.")); +} + +/** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ +bool UsermodTemperature::readFromConfig(JsonObject &root) { + // we look for JSON object: {"Temperature": {"pin": 0, "degC": true}} + int8_t newTemperaturePin = temperaturePin; + DEBUG_PRINT(FPSTR(_name)); + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_enabled)] | enabled; + newTemperaturePin = top["pin"] | newTemperaturePin; + degC = top["degC"] | degC; + readingInterval = top[FPSTR(_readInterval)] | readingInterval/1000; + readingInterval = min(120,max(10,(int)readingInterval)) * 1000; // convert to ms + parasite = top[FPSTR(_parasite)] | parasite; + parasitePin = top[FPSTR(_parasitePin)] | parasitePin; + + if (!initDone) { + // first run: reading from cfg.json + temperaturePin = newTemperaturePin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing paramters from settings page + if (newTemperaturePin != temperaturePin) { + DEBUG_PRINTLN(F("Re-init temperature.")); + // deallocate pin and release memory + delete oneWire; + pinManager.deallocatePin(temperaturePin, PinOwner::UM_Temperature); + temperaturePin = newTemperaturePin; + pinManager.deallocatePin(parasitePin, PinOwner::UM_Temperature); + // initialise + setup(); } -}; + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_parasitePin)].isNull(); +} + +void UsermodTemperature::appendConfigData() { + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasite)).c_str()); + oappend(SET_F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); + oappend(SET_F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field +} + +float UsermodTemperature::getTemperature() { + return degC ? getTemperatureC() : getTemperatureF(); +} + +const char *UsermodTemperature::getTemperatureUnit() { + return degC ? "°C" : "°F"; +} // strings to reduce flash memory usage (used more than twice) const char UsermodTemperature::_name[] PROGMEM = "Temperature"; const char UsermodTemperature::_enabled[] PROGMEM = "enabled"; const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s"; const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr"; +const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin"; diff --git a/usermods/Temperature/usermods_list.cpp b/usermods/Temperature/usermods_list.cpp deleted file mode 100644 index 50dd7816ba..0000000000 --- a/usermods/Temperature/usermods_list.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "wled.h" -/* - * Register your v2 usermods here! - */ - -/* - * Add/uncomment your usermod filename here (and once more below) - * || || || - * \/ \/ \/ - */ -//#include "usermod_v2_example.h" -#ifdef USERMOD_DALLASTEMPERATURE -#include "../usermods/Temperature/usermod_temperature.h" -#endif - -//#include "usermod_v2_empty.h" - -void registerUsermods() -{ - /* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //usermods.add(new MyExampleUsermod()); -#ifdef USERMOD_DALLASTEMPERATURE - usermods.add(new UsermodTemperature()); -#endif - - //usermods.add(new UsermodRenameMe()); -} \ No newline at end of file diff --git a/usermods/UserModv2_SunRiseAndSet/README.md b/usermods/UserModv2_SunRiseAndSet/README.md deleted file mode 100644 index e989f08906..0000000000 --- a/usermods/UserModv2_SunRiseAndSet/README.md +++ /dev/null @@ -1,15 +0,0 @@ -WLED v2 UserMod for running macros at sunrise and sunset. - -At the time of this text, this user mod requires code to be changed to set certain variables: - 1. To reflect the user's graphical location (latitude/longitude) used for calculating apparent sunrise/sunset - 2. To specify which macros will be run at sunrise and/or sunset. (defaults to 15 at sunrise and 16 at sunset) - 3. To optionally provide an offset from sunrise/sunset, in minutes (max of +/- 2 hours), when the macro will be run. - -In addition, WLED must be configured to get time from NTP (and the time must be retrieved via NTP.) - -Please open the UserMod_SunRiseAndSet.h file for instructions on what needs to be changed, where to copy files, etc. - -If this usermod proves useful enough, the code might eventually be updated to allow prompting for the required information -via the web interface and to store settings in EEPROM instead of hard-coding in the .h file. - -This usermod has only been tested on the esp32dev platform, but there's no reason it wouldn't work on other platforms. diff --git a/usermods/UserModv2_SunRiseAndSet/UserMod_SunRiseAndSet.h b/usermods/UserModv2_SunRiseAndSet/UserMod_SunRiseAndSet.h deleted file mode 100644 index ef1bb37ec7..0000000000 --- a/usermods/UserModv2_SunRiseAndSet/UserMod_SunRiseAndSet.h +++ /dev/null @@ -1,166 +0,0 @@ -#pragma once - -#include "wled.h" -#include - -/* - * - * REQUIREMENTS: - * The Dusk2Dawn library must be installed. This can be found at https://github.com/dmkishi/Dusk2Dawn. The 1.0.1 version of this library found via - * Arduino or platformio library managers is buggy and won't compile. The latest version from github should be used. - * - * NTP must be enabled and functional. It simply makes no sense to have events on sunrise/sunset when an accurate time isn't available. - * - * The user's geographical latitude and longitude must be configured (in decimal, not degrees/minutes/etc) using m_fLatitude and m_fLongitude - * - * if desired, an offset of up to +/- 2 hours can be specified for each of sunrise/sunset using m_sunriseOffset and m_sunsetOffset (defaults to 0) - * - * The specific macro to run at sunrise and/or sunset can be changed using m_sunriseMacro and m_sunsetMacro. (defaults to 15 and 16) - * - * From the Dusk2Dawn library: - * HINT: An easy way to find the longitude and latitude for any location is - * to find the spot in Google Maps, right click the place on the map, and - * select "What's here?". At the bottom, you’ll see a card with the - * coordinates. - * - * Once configured, copy UserMod_SunRiseAndSet.h to the sketch file (the same folder as wled00.ino exists), - * and then edit "usermods_list.cpp": - * Add '#include "UserMod_SunRiseAndSet.h"' in the 'includes' area - * Add 'usermods.add(new UserMod_SunRiseAndSet());' in the registerUsermods() area - * - */ - -class UserMod_SunRiseAndSet : public Usermod -{ -private: - - /**** USER SETTINGS ****/ - - float m_fLatitude = 40.6; // latitude where sunrise/set are calculated - float m_fLongitude = -79.80; // longitude where sunrise/set are calculated - int8_t m_sunriseOffset = 0; // offset from sunrise, in minutes, when macro should be run (negative for before sunrise, positive for after sunrise) - int8_t m_sunsetOffset = 0; // offset from sunset, in minutes, when macro should be run (negative for before sunset, positive for after sunset) - uint8_t m_sunriseMacro = 15; // macro number to run at sunrise - uint8_t m_sunsetMacro = 16; // macro number to run at sunset - - /**** END OF USER SETTINGS. DO NOT EDIT BELOW THIS LINE! ****/ - - - Dusk2Dawn *m_pD2D = NULL; // this must be dynamically allocated in order for parameters to be loaded from EEPROM - - int m_nUserSunrise = -1; // time, in minutes from midnight, of sunrise - int m_nUserSunset = -1; // time, in minutes from midnight, of sunset - - byte m_nLastRunMinute = -1; // indicates what minute the userloop was last run - used so that the code only runs once per minute - -public: - - virtual void setup(void) - { - /* TODO: From EEPROM, load the following variables: - * - * int16_t latitude16 = 4060; // user provided latitude, multiplied by 100 and rounded - * int16_t longitude16 = -7980; // user provided longitude, multiplied by 100 and rounded. - * int8_t sunrise_offset = 0; // number of minutes to offset the sunrise macro trigger (positive for minutes after sunrise, negative for minutes before) - * int8_t sunset_offset = 0; // number of minutes to offset the sunset macro trigger (positive for minutes after sunset, negative for minutes before) - * - * then: - * m_fLatitude = (float)latitude / 100.0; - * m_fLongitude = (float)longitude / 100.0; - * m_sunriseOffset = sunrise_offset; - * m_sunsetOffset = sunset_offset; - */ - - if ((0.0 != m_fLatitude) || (0.0 != m_fLongitude)) - { - m_pD2D = new Dusk2Dawn (m_fLatitude, m_fLongitude, 0 /* UTC */); - // can't really check for failures. if the alloc fails, the mod just doesn't work. - } - } - - void loop(void) - { - // without NTP, or a configured lat/long, none of this stuff is going to work... - // As an alternative, need to figure out how to determine if the user has manually set the clock or not. - if (m_pD2D && (999000000L != ntpLastSyncTime)) - { - // to prevent needing to import all the timezone stuff from other modules, work completely in UTC - time_t timeUTC = toki.second(); - tmElements_t tmNow; - breakTime(timeUTC, tmNow); - int nCurMinute = tmNow.Minute; - - if (m_nLastRunMinute != nCurMinute) //only check once a new minute begins - { - m_nLastRunMinute = nCurMinute; - int numMinutes = (60 * tmNow.Hour) + m_nLastRunMinute; // how many minutes into the day are we? - - // check to see if sunrise/sunset should be re-determined. Only do this if neither sunrise nor sunset - // are set. That happens when the device has just stated, and after both sunrise/sunset have already run. - if ((-1 == m_nUserSunrise) && (-1 == m_nUserSunset)) - { - m_nUserSunrise = m_pD2D->sunrise(tmNow.Year + 1970, tmNow.Month, tmNow.Day, false) % 1440; - m_nUserSunset = m_pD2D->sunset(tmNow.Year + 1970, tmNow.Month, tmNow.Day, false) % 1440; - if (m_nUserSunrise > numMinutes) // has sunrise already passed? if so, recompute for tomorrow - { - breakTime(timeUTC + (60*60*24), tmNow); - m_nUserSunrise = m_pD2D->sunrise(tmNow.Year + 1970, tmNow.Month, tmNow.Day, false) % 1440; - if (m_nUserSunset > numMinutes) // if sunset has also passed, recompute that as well - { - m_nUserSunset = m_pD2D->sunset(tmNow.Year + 1970, tmNow.Month, tmNow.Day, false) % 1440; - } - } - // offset by user provided values. becuase the offsets are signed bytes, the max offset is just over 2 hours. - m_nUserSunrise += m_sunriseOffset; - m_nUserSunset += m_sunsetOffset; - } - - if (numMinutes == m_nUserSunrise) // Good Morning! - { - if (m_sunriseMacro) - applyMacro(m_sunriseMacro); // run macro 15 - m_nUserSunrise = -1; - } - else if (numMinutes == m_nUserSunset) // Good Night! - { - if (m_sunsetMacro) - applyMacro(m_sunsetMacro); // run macro 16 - m_nUserSunset = -1; - } - } // if (m_nLastRunMinute != nCurMinute) - } // if (m_pD2D && (999000000L != ntpLastSyncTime)) - } - - void addToJsonState(JsonObject& root) - { - JsonObject user = root["SunRiseAndSet"]; - if (user.isNull()) user = root.createNestedObject("SunRiseAndSet"); - - char buf[10]; - if (-1 != m_nUserSunrise) - { - snprintf(buf, 10, "%02d:%02d UTC", m_nUserSunrise / 60, m_nUserSunrise % 60); - user["rise"] = buf; - } - if (-1 != m_nUserSunset) - { - snprintf(buf, 10, "%02d:%02d UTC", m_nUserSunset / 60, m_nUserSunset % 60); - user["set"] = buf; - } - JsonObject vars = user.createNestedObject("vars"); - vars["lat"] = m_fLatitude; - vars["long"] = m_fLongitude; - vars["rise_mac"] = m_sunriseMacro; - vars["set_mac"] = m_sunsetMacro; - vars["rise_off"] = m_sunriseOffset; - vars["set_off"] = m_sunsetOffset; - } - - ~UserMod_SunRiseAndSet(void) - { - if (m_pD2D) delete m_pD2D; - } -}; - - - diff --git a/usermods/VL53L0X_gestures/readme.md b/usermods/VL53L0X_gestures/readme.md index dec8413008..a230b1a655 100644 --- a/usermods/VL53L0X_gestures/readme.md +++ b/usermods/VL53L0X_gestures/readme.md @@ -1,17 +1,16 @@ # Description -That usermod implements support of simple hand gestures with VL53L0X sensor: on/off and brightness correction. -It can be useful for kitchen strips to avoid any touches. - - on/off - just swipe a hand below your sensor ("shortPressAction" is called and can be customized through WLED macros) - - brightness correction - keep your hand below sensor for 1 second to switch to "brightness" mode. - Configure brightness by changing distance to the sensor (see parameters below for customization). - "macroLongPress" is also called here. - -## Installation +Implements support of simple hand gestures via a VL53L0X sensor: on/off and brightness adjustment. +Useful for controlling strips when you want to avoid touching anything. + - on/off - swipe your hand below the sensor ("shortPressAction" is called. Can be customized via WLED macros) + - brightness adjustment - hold your hand below the sensor for 1 second to switch to "brightness" mode. + adjust the brightness by changing the distance between your hand and the sensor (see parameters below for customization). + +## Installation 1. Attach VL53L0X sensor to i2c pins according to default pins for your board. 2. Add `-D USERMOD_VL53L0X_GESTURES` to your build flags at platformio.ini (plaformio_override.ini) for needed environment. -In my case, for example: `build_flags = ${common.build_flags_esp8266} -D RLYPIN=12 -D USERMOD_VL53L0X_GESTURES` +In my case, for example: `build_flags = ${env.build_flags} -D USERMOD_VL53L0X_GESTURES` 3. Add "pololu/VL53L0X" dependency below to `lib_deps` like this: ```ini lib_deps = ${env.lib_deps} @@ -21,15 +20,10 @@ lib_deps = ${env.lib_deps} My entire `platformio_override.ini` for example (for nodemcu board): ```ini [platformio] -default_envs = nodemcu +default_envs = nodemcuv2 -[env:nodemcu] -board = nodemcu -platform = ${common.platform_wled_default} -platform_packages = ${common.platform_packages} -board_build.ldscript = ${common.ldscript_4m1m} -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D RLYPIN=12 -D USERMOD_VL53L0X_GESTURES +[env:nodemcuv2] +build_flags = ${env.build_flags} -D USERMOD_VL53L0X_GESTURES lib_deps = ${env.lib_deps} - pololu/VL53L0X @ ^1.3.0 -``` \ No newline at end of file + pololu/VL53L0X @ ^1.3.0 +``` diff --git a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h b/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h index 210ec3f585..fe6b958f55 100644 --- a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h +++ b/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h @@ -3,14 +3,13 @@ * It can be useful for kitchen strips to avoid any touches. * - on/off - just swipe a hand below your sensor ("shortPressAction" is called and can be customized through WLED macros) * - brightness correction - keep your hand below sensor for 1 second to switch to "brightness" mode. - * Configure brightness by changing distance to the sensor (see parameters below for customization). - * "macroLongPress" is also called here. + Configure brightness by changing distance to the sensor (see parameters below for customization). * - * Enabling this mod usermod: + * Enabling this usermod: * 1. Attach VL53L0X sensor to i2c pins according to default pins for your board. - * 2. Add "-D USERMOD_VL53L0X_GESTURES" to your build flags at platformio.ini (plaformio_override.ini) for needed environment. - * In my case, for example: build_flags = ${common.build_flags_esp8266} -D RLYPIN=12 -D USERMOD_VL53L0X_GESTURES - * 3. Add "pololu/VL53L0X" dependency to lib_deps like this: + * 2. Add `-D USERMOD_VL53L0X_GESTURES` to your build flags at platformio.ini (plaformio_override.ini) for needed environment. + * In my case, for example: `build_flags = ${env.build_flags} -D USERMOD_VL53L0X_GESTURES` + * 3. Add "pololu/VL53L0X" dependency below to `lib_deps` like this: * lib_deps = ${env.lib_deps} * pololu/VL53L0X @ ^1.3.0 */ @@ -21,28 +20,20 @@ #include #include -#ifdef ARDUINO_ARCH_ESP32 - #define HW_PIN_SCL 22 - #define HW_PIN_SDA 21 -#else - #define HW_PIN_SCL 5 - #define HW_PIN_SDA 4 -#endif - #ifndef VL53L0X_MAX_RANGE_MM -#define VL53L0X_MAX_RANGE_MM 230 // max height in millimiters to react for motions +#define VL53L0X_MAX_RANGE_MM 230 // max height in millimeters to react for motions #endif #ifndef VL53L0X_MIN_RANGE_OFFSET -#define VL53L0X_MIN_RANGE_OFFSET 60 // minimal range in millimiters that sensor can detect. Used in long motions to correct brightnes calculation. +#define VL53L0X_MIN_RANGE_OFFSET 60 // minimal range in millimeters that sensor can detect. Used in long motions to correct brightness calculation. #endif #ifndef VL53L0X_DELAY_MS -#define VL53L0X_DELAY_MS 100 // how often to get data from sensor +#define VL53L0X_DELAY_MS 100 // how often to get data from sensor #endif #ifndef VL53L0X_LONG_MOTION_DELAY_MS -#define VL53L0X_LONG_MOTION_DELAY_MS 1000 // how often to get data from sensor +#define VL53L0X_LONG_MOTION_DELAY_MS 1000 // switch onto "long motion" action after this delay #endif class UsermodVL53L0XGestures : public Usermod { @@ -55,13 +46,11 @@ class UsermodVL53L0XGestures : public Usermod { bool wasMotionBefore = false; bool isLongMotion = false; unsigned long motionStartTime = 0; - + public: void setup() { - PinManagerPinType pins[2] = { { HW_PIN_SCL, true }, { HW_PIN_SDA, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { enabled = false; return; } - Wire.begin(); + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } sensor.setTimeout(150); if (!sensor.init()) @@ -80,40 +69,34 @@ class UsermodVL53L0XGestures : public Usermod { lastTime = millis(); int range = sensor.readRangeSingleMillimeters(); - DEBUG_PRINTF(F("range: %d, brightness: %d"), range, bri); + DEBUG_PRINTF("range: %d, brightness: %d\r\n", range, bri); if (range < VL53L0X_MAX_RANGE_MM) { if (!wasMotionBefore) { motionStartTime = millis(); - DEBUG_PRINTF(F("motionStartTime: %d"), motionStartTime); + DEBUG_PRINTF("motionStartTime: %d\r\n", motionStartTime); } wasMotionBefore = true; if (millis() - motionStartTime > VL53L0X_LONG_MOTION_DELAY_MS) //long motion { - DEBUG_PRINTF(F("long motion: %d"), motionStartTime); + DEBUG_PRINTF("long motion: %d\r\n", motionStartTime); if (!isLongMotion) { - if (macroLongPress) - { - applyMacro(macroLongPress); - } isLongMotion = true; } // set brightness according to range bri = (VL53L0X_MAX_RANGE_MM - max(range, VL53L0X_MIN_RANGE_OFFSET)) * 255 / (VL53L0X_MAX_RANGE_MM - VL53L0X_MIN_RANGE_OFFSET); - DEBUG_PRINTF(F("new brightness: %d"), bri); + DEBUG_PRINTF("new brightness: %d", bri); stateUpdated(1); } } else if (wasMotionBefore) { //released - long dur = millis() - motionStartTime; - if (!isLongMotion) { //short press - DEBUG_PRINTF(F("shortPressAction...")); + DEBUG_PRINTLN(F("shortPressAction...")); shortPressAction(); } wasMotionBefore = false; @@ -127,13 +110,13 @@ class UsermodVL53L0XGestures : public Usermod { * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ - void addToConfig(JsonObject& root) - { - JsonObject top = root.createNestedObject("VL53L0x"); - JsonArray pins = top.createNestedArray("pin"); - pins.add(HW_PIN_SCL); - pins.add(HW_PIN_SDA); - } +// void addToConfig(JsonObject& root) +// { +// JsonObject top = root.createNestedObject("VL53L0x"); +// JsonArray pins = top.createNestedArray("pin"); +// pins.add(i2c_scl); +// pins.add(i2c_sda); +// } /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). diff --git a/usermods/Wemos_D1_mini+Wemos32_mini_shield/readme.md b/usermods/Wemos_D1_mini+Wemos32_mini_shield/readme.md index eebc50da24..105f2a24f4 100644 --- a/usermods/Wemos_D1_mini+Wemos32_mini_shield/readme.md +++ b/usermods/Wemos_D1_mini+Wemos32_mini_shield/readme.md @@ -9,10 +9,10 @@ ## Features - SSD1306 128x32 or 128x64 I2C OLED display - On screen IP address, SSID and controller status (e.g. ON or OFF, recent effect) -- Auto display shutoff for saving display lifetime +- Auto display shutoff for extending display lifetime - Dallas temperature sensor - Reporting temperature to MQTT broker -- Relay for energy saving +- Relay for saving energy ## Hardware ![Shield](https://github.com/srg74/WLED-wemos-shield/blob/master/resources/Images/Assembly_8.jpg) diff --git a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp index 79241b5e20..e7d1212a14 100644 --- a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp +++ b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp @@ -34,30 +34,30 @@ uint8_t DALLAS_PIN =23; uint8_t SCL_PIN = 5; uint8_t SDA_PIN = 4; uint8_t DALLAS_PIN =13; -// uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 +// uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif //The SCL and SDA pins are defined here. //ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21 #define U8X8_PIN_SCL SCL_PIN #define U8X8_PIN_SDA SDA_PIN -//#define U8X8_PIN_RESET RST_PIN // Uncoment for Heltec WiFi-Kit-8 +//#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8 // Dallas sensor reading timer long temptimer = millis(); long lastMeasure = 0; -#define Celsius // Show temperature mesaurement in Celcius otherwise is in Fahrenheit +#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit // If display does not work or looks corrupted check the // constructor reference: // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp // or check the gallery: // https://github.com/olikraus/u8g2/wiki/gallery -// --> First choise of cheap I2C OLED 128X32 0.91" +// --> First choice of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Second choise of cheap I2C OLED 128X64 0.96" or 1.3" +// --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Third choise of Heltec WiFi-Kit-8 OLED 128X32 0.91" +// --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91" //U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8 // gets called once at boot. Do all initialization that doesn't depend on network here void userSetup() { @@ -97,15 +97,16 @@ void userLoop() { //----> Dallas temperature sensor MQTT publishing temptimer = millis(); -// Timer to publishe new temperature every 60 seconds +// Timer to publish new temperature every 60 seconds if (temptimer - lastMeasure > 60000) { lastMeasure = temptimer; +#ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (mqtt != nullptr) { // Serial.println(Dallas(DALLAS_PIN,0)); -//Gets prefered temperature scale based on selection in definitions section +//Gets preferred temperature scale based on selection in definitions section #ifdef Celsius int16_t board_temperature = Dallas(DALLAS_PIN,0); #else @@ -116,6 +117,7 @@ void userLoop() { t += "/temperature"; mqtt->publish(t.c_str(), 0, true, String(board_temperature).c_str()); } + #endif } // Check if we time interval for redrawing passes. @@ -171,11 +173,11 @@ void userLoop() { // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); - // Print `~` char to indicate that SSID is longer, than owr dicplay + // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); - // Second row with IP or Psssword + // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) @@ -185,58 +187,14 @@ void userLoop() { // Third row with mode name u8x8.setCursor(2, 2); - uint8_t qComma = 0; - bool insideQuotes = false; - uint8_t printedChars = 0; - char singleJsonSymbol; - - // Find the mode name in JSON - for (size_t i = 0; i < strlen_P(JSON_mode_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_mode_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownMode)) - break; - u8x8.print(singleJsonSymbol); - printedChars++; - } - if ((qComma > knownMode) || (printedChars > u8x8.getCols() - 2)) - break; - } + char lineBuffer[17]; + extractModeName(knownMode, JSON_mode_names, lineBuffer, 16); + u8x8.print(lineBuffer); + // Fourth row with palette name u8x8.setCursor(2, 3); - qComma = 0; - insideQuotes = false; - printedChars = 0; - // Looking for palette name in JSON. - for (size_t i = 0; i < strlen_P(JSON_palette_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_palette_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownPalette)) - break; - u8x8.print(singleJsonSymbol); - printedChars++; - } - if ((qComma > knownMode) || (printedChars > u8x8.getCols() - 2)) - break; - } + extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16); + u8x8.print(lineBuffer); u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); u8x8.drawGlyph(0, 0, 80); // wifi icon diff --git a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp index fe53a46283..ff1cf7e534 100644 --- a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp +++ b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp @@ -6,7 +6,7 @@ void UpdateBME280Data(); -#define Celsius // Show temperature mesaurement in Celcius otherwise is in Fahrenheit +#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit BME280I2C bme; // Default : forced mode, standby time = 1000 ms // Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off, @@ -16,25 +16,25 @@ uint8_t SDA_PIN = 21; #else //ESP8266 boards uint8_t SCL_PIN = 5; uint8_t SDA_PIN = 4; -// uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 +// uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif //The SCL and SDA pins are defined here. //ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21 #define U8X8_PIN_SCL SCL_PIN #define U8X8_PIN_SDA SDA_PIN -//#define U8X8_PIN_RESET RST_PIN // Uncoment for Heltec WiFi-Kit-8 +//#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8 // If display does not work or looks corrupted check the // constructor reference: // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp // or check the gallery: // https://github.com/olikraus/u8g2/wiki/gallery -// --> First choise of cheap I2C OLED 128X32 0.91" +// --> First choice of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Second choise of cheap I2C OLED 128X64 0.96" or 1.3" +// --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Third choise of Heltec WiFi-Kit-8 OLED 128X32 0.91" +// --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91" //U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8 // gets called once at boot. Do all initialization that doesn't depend on network here @@ -103,6 +103,7 @@ void userLoop() { { lastMeasure = tempTimer; +#ifndef WLED_DISABLE_MQTT // Check if MQTT Connected, otherwise it will crash the 8266 if (mqtt != nullptr) { @@ -122,6 +123,7 @@ void userLoop() { h += "/humidity"; mqtt->publish(h.c_str(), 0, true, String(board_humidity).c_str()); } + #endif } // Check if we time interval for redrawing passes. @@ -177,11 +179,11 @@ void userLoop() { // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); - // Print `~` char to indicate that SSID is longer, than owr dicplay + // Print `~` char to indicate that SSID is longer, than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); - // Second row with IP or Psssword + // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) @@ -191,58 +193,14 @@ void userLoop() { // Third row with mode name u8x8.setCursor(2, 2); - uint8_t qComma = 0; - bool insideQuotes = false; - uint8_t printedChars = 0; - char singleJsonSymbol; + char lineBuffer[17]; + extractModeName(knownMode, JSON_mode_names, lineBuffer, 16); + u8x8.print(lineBuffer); - // Find the mode name in JSON - for (size_t i = 0; i < strlen_P(JSON_mode_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_mode_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownMode)) - break; - u8x8.print(singleJsonSymbol); - printedChars++; - } - if ((qComma > knownMode) || (printedChars > u8x8.getCols() - 2)) - break; - } // Fourth row with palette name u8x8.setCursor(2, 3); - qComma = 0; - insideQuotes = false; - printedChars = 0; - // Looking for palette name in JSON. - for (size_t i = 0; i < strlen_P(JSON_palette_names); i++) { - singleJsonSymbol = pgm_read_byte_near(JSON_palette_names + i); - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownPalette)) - break; - u8x8.print(singleJsonSymbol); - printedChars++; - } - if ((qComma > knownMode) || (printedChars > u8x8.getCols() - 2)) - break; - } + extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16); + u8x8.print(lineBuffer); u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); u8x8.drawGlyph(0, 0, 80); // wifi icon diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h new file mode 100644 index 0000000000..7169539570 --- /dev/null +++ b/usermods/audioreactive/audio_reactive.h @@ -0,0 +1,1851 @@ +#pragma once + +#include "wled.h" +#include +#include + +#ifndef ARDUINO_ARCH_ESP32 + #error This audio reactive usermod does not support the ESP8266. +#endif + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#include +#endif + +/* + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * + * This is an audioreactive v2 usermod. + * .... + */ + +#if !defined(FFTTASK_PRIORITY) +#define FFTTASK_PRIORITY 1 // standard: looptask prio +//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp +//#define FFTTASK_PRIORITY 4 // above asyc_tcp +#endif + +// Comment/Uncomment to toggle usb serial debugging +// #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter) +// #define FFT_SAMPLING_LOG // FFT result debugging +// #define SR_DEBUG // generic SR DEBUG messages + +#ifdef SR_DEBUG + #define DEBUGSR_PRINT(x) DEBUGOUT.print(x) + #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) +#else + #define DEBUGSR_PRINT(x) + #define DEBUGSR_PRINTLN(x) + #define DEBUGSR_PRINTF(x...) +#endif + +#if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) + #define PLOT_PRINT(x) DEBUGOUT.print(x) + #define PLOT_PRINTLN(x) DEBUGOUT.println(x) + #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) +#else + #define PLOT_PRINT(x) + #define PLOT_PRINTLN(x) + #define PLOT_PRINTF(x...) +#endif + +// use audio source class (ESP32 specific) +#include "audio_source.h" +constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) +constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) + +// globals +static uint8_t inputLevel = 128; // UI slider value +#ifndef SR_SQUELCH + uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value) +#else + uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value) +#endif +#ifndef SR_GAIN + uint8_t sampleGain = 60; // sample gain (config value) +#else + uint8_t sampleGain = SR_GAIN; // sample gain (config value) +#endif +static uint8_t soundAgc = 1; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) +static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group + +// user settable parameters for limitSoundDynamics() +#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF +static bool limiterOn = false; // bool: enable / disable dynamics limiter +#else +static bool limiterOn = true; +#endif +static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec +static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec +// user settable options for FFTResult scaling +static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root + +// +// AGC presets +// Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const" +// +#define AGC_NUM_PRESETS 3 // AGC presets: normal, vivid, lazy +const double agcSampleDecay[AGC_NUM_PRESETS] = { 0.9994f, 0.9985f, 0.9997f}; // decay factor for sampleMax, in case the current sample is below sampleMax +const float agcZoneLow[AGC_NUM_PRESETS] = { 32, 28, 36}; // low volume emergency zone +const float agcZoneHigh[AGC_NUM_PRESETS] = { 240, 240, 248}; // high volume emergency zone +const float agcZoneStop[AGC_NUM_PRESETS] = { 336, 448, 304}; // disable AGC integrator if we get above this level +const float agcTarget0[AGC_NUM_PRESETS] = { 112, 144, 164}; // first AGC setPoint -> between 40% and 65% +const float agcTarget0Up[AGC_NUM_PRESETS] = { 88, 64, 116}; // setpoint switching value (a poor man's bang-bang) +const float agcTarget1[AGC_NUM_PRESETS] = { 220, 224, 216}; // second AGC setPoint -> around 85% +const double agcFollowFast[AGC_NUM_PRESETS] = { 1/192.f, 1/128.f, 1/256.f}; // quickly follow setpoint - ~0.15 sec +const double agcFollowSlow[AGC_NUM_PRESETS] = {1/6144.f,1/4096.f,1/8192.f}; // slowly follow setpoint - ~2-15 secs +const double agcControlKp[AGC_NUM_PRESETS] = { 0.6f, 1.5f, 0.65f}; // AGC - PI control, proportional gain parameter +const double agcControlKi[AGC_NUM_PRESETS] = { 1.7f, 1.85f, 1.2f}; // AGC - PI control, integral gain parameter +const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) +// AGC presets end + +static AudioSource *audioSource = nullptr; +static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. +static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. + +// audioreactive variables shared with FFT task +static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point +static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier +static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) +static float sampleAgc = 0.0f; // Smoothed AGC sample + +// peak detection +static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() +static uint8_t maxVol = 31; // Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) +static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) +static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData +static unsigned long timeOfPeak = 0; // time of last sample peak detection. +static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) +static void autoResetPeak(void); // peak auto-reset function + + +//////////////////// +// Begin FFT Code // +//////////////////// + +// some prototypes, to ensure consistent interfaces +static float mapf(float x, float in_min, float in_max, float out_min, float out_max); // map function for float +static float fftAddAvg(int from, int to); // average of several FFT result bins +void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results +static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels + +#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! + +static TaskHandle_t FFT_Task = nullptr; + +// Table of multiplication factors so that we can even out the frequency response. +static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; + +// globals and FFT Output variables shared with animations +static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency +static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency +static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects +#if defined(WLED_DEBUG) || defined(SR_DEBUG) +static uint64_t fftTime = 0; +static uint64_t sampleTime = 0; +#endif + +// FFT Task variables (filtering and post-processing) +static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. +static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) +#ifdef SR_DEBUG +static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. +#endif + +// audio source parameters and constant +constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms +//constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms +//constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms +//constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms +#define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling +//#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling +//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling +//#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling + +// FFT Constants +constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 +constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information. +// the following are observed values, supported by a bit of "educated guessing" +//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels +#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels +#define LOG_256 5.54517744f // log(256) + +// These are the input and output vectors. Input vectors receive computed results from FFT. +static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins +static float vImag[samplesFFT] = {0.0f}; // imaginary parts + +// Create FFT object +#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT + // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 + // these options actually cause slow-downs on all esp32 processors, don't use them. + // #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 + // #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 + // Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() + #define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 + #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 +#else + // around 40% slower on -S2 + // lib_deps += https://github.com/blazoncek/arduinoFFT.git +#endif + +#include + +#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT +#if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 + // arduinoFFT 2.x has a slightly different API + static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); +#else + static float windowWeighingFactors[samplesFFT] = {0.0f}; // cache for FFT windowing factors + static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); +#endif +#else + static arduinoFFT FFT = arduinoFFT(vReal, vImag, samplesFFT, SAMPLE_RATE); +#endif + +// Helper functions + +// float version of map() +static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + +// compute average of several FFT result bins +static float fftAddAvg(int from, int to) { + float result = 0.0f; + for (int i = from; i <= to; i++) { + result += vReal[i]; + } + return result / float(to - from + 1); +} + +// +// FFT main task +// +void FFTcode(void * parameter) +{ + DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); + + // see https://www.freertos.org/vtaskdelayuntil.html + const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; + + TickType_t xLastWakeTime = xTaskGetTickCount(); + for(;;) { + delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. + // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. + + // Don't run FFT computing code if we're in Receive mode or in realtime mode + if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { + vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + continue; + } + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + uint64_t start = esp_timer_get_time(); + bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid +#endif + + // get a fresh batch of samples from I2S + if (audioSource) audioSource->getSamples(vReal, samplesFFT); + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + if (start < esp_timer_get_time()) { // filter out overflows + uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding + sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10; // smooth + } + start = esp_timer_get_time(); // start measuring FFT time +#endif + + xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay + + // band pass filter - can reduce noise floor by a factor of 50 + // downside: frequencies below 100Hz will be ignored + if (useBandPassFilter) runMicFilter(samplesFFT, vReal); + + // find highest sample in the batch + float maxSample = 0.0f; // max sample from FFT batch + for (int i=0; i < samplesFFT; i++) { + // set imaginary parts to 0 + vImag[i] = 0; + // pick our our current mic sample - we take the max value from all samples that go into FFT + if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts + if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); + } + // release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function + // early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results. + micDataReal = maxSample; + +#ifdef SR_DEBUG + if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization +#else + if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. +#endif + + // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) +#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT + FFT.dcRemoval(); // remove DC offset + FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy + //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection + FFT.compute( FFTDirection::Forward ); // Compute FFT + FFT.complexToMagnitude(); // Compute magnitudes +#else + FFT.DCRemoval(); // let FFT lib remove DC component, so we don't need to care about this in getSamples() + + //FFT.Windowing( FFT_WIN_TYP_HAMMING, FFT_FORWARD ); // Weigh data - standard Hamming window + //FFT.Windowing( FFT_WIN_TYP_BLACKMAN, FFT_FORWARD ); // Blackman window - better side freq rejection + //FFT.Windowing( FFT_WIN_TYP_BLACKMAN_HARRIS, FFT_FORWARD );// Blackman-Harris - excellent sideband rejection + FFT.Windowing( FFT_WIN_TYP_FLT_TOP, FFT_FORWARD ); // Flat Top Window - better amplitude accuracy + FFT.Compute( FFT_FORWARD ); // Compute FFT + FFT.ComplexToMagnitude(); // Compute magnitudes +#endif + +#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT + #if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 + // arduinoFFT 2.x has a slightly different API + FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant + #else + FFT.majorPeak(FFT_MajorPeak, FFT_Magnitude); // let the effects know which freq was most dominant + #endif +#else + FFT.MajorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant +#endif + FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + haveDoneFFT = true; +#endif + + } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. + memset(vReal, 0, sizeof(vReal)); + FFT_MajorPeak = 1; + FFT_Magnitude = 0.001; + } + + for (int i = 0; i < samplesFFT; i++) { + float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way + vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. + } // for() + + // mapping of FFT result bins to frequency channels + if (fabsf(sampleAvg) > 0.5f) { // noise gate open +#if 0 + /* This FFT post processing is a DIY endeavour. What we really need is someone with sound engineering expertise to do a great job here AND most importantly, that the animations look GREAT as a result. + * + * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap. + * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255. + * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then determine the bins. + * End frequency = Start frequency * multiplier ^ 16 + * Multiplier = (End frequency/ Start frequency) ^ 1/16 + * Multiplier = 1.320367784 + */ // Range + fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 + fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 + fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 + fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 + fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 + fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 + fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 + fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 + fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 + fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 + fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 + fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 + fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 + fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 + fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 + fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate +#else + /* new mapping, optimized for 22050 Hz by softhack007 */ + // bins frequency range + if (useBandPassFilter) { + // skip frequencies below 100hz + fftCalc[ 0] = 0.8f * fftAddAvg(3,4); + fftCalc[ 1] = 0.9f * fftAddAvg(4,5); + fftCalc[ 2] = fftAddAvg(5,6); + fftCalc[ 3] = fftAddAvg(6,7); + // don't use the last bins from 206 to 255. + fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping + } else { + fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass + fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass + fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass + fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange + // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) + fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping + } + fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange + fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange + fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange + fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center ! + fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange + fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange + fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid + fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid + fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid + fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid + fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping +#endif + } else { // noise gate closed - just decay old values + for (int i=0; i < NUM_GEQ_CHANNELS; i++) { + fftCalc[i] *= 0.85f; // decay to zero + if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f; + } + } + + // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) + postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows + uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding + fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth + } +#endif + // run peak detection + autoResetPeak(); + detectSamplePeak(); + + #if !defined(I2S_GRAB_ADC1_COMPLETELY) + if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC + #endif + vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + + } // for(;;)ever +} // FFTcode() task end + + +/////////////////////////// +// Pre / Postprocessing // +/////////////////////////// + +static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass) +{ + // low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency + //constexpr float alpha = 0.04f; // 150Hz + //constexpr float alpha = 0.03f; // 110Hz + constexpr float alpha = 0.0225f; // 80hz + //constexpr float alpha = 0.01693f;// 60hz + // high frequency cutoff parameter + //constexpr float beta1 = 0.75f; // 11Khz + //constexpr float beta1 = 0.82f; // 15Khz + //constexpr float beta1 = 0.8285f; // 18Khz + constexpr float beta1 = 0.85f; // 20Khz + + constexpr float beta2 = (1.0f - beta1) / 2.0f; + static float last_vals[2] = { 0.0f }; // FIR high freq cutoff filter + static float lowfilt = 0.0f; // IIR low frequency cutoff filter + + for (int i=0; i < numSamples; i++) { + // FIR lowpass, to remove high frequency noise + float highFilteredSample; + if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes + else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // special handling for last sample in array + last_vals[1] = last_vals[0]; + last_vals[0] = sampleBuffer[i]; + sampleBuffer[i] = highFilteredSample; + // IIR highpass, to remove low frequency noise + lowfilt += alpha * (sampleBuffer[i] - lowfilt); + sampleBuffer[i] = sampleBuffer[i] - lowfilt; + } +} + +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels +{ + for (int i=0; i < numberOfChannels; i++) { + + if (noiseGateOpen) { // noise gate open + // Adjustment for frequency curves. + fftCalc[i] *= fftResultPink[i]; + if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function + // Manual linear adjustment of gain using sampleGain adjustment for different input types. + fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //apply gain, with inputLevel adjustment + if(fftCalc[i] < 0) fftCalc[i] = 0; + } + + // smooth results - rise fast, fall slower + if(fftCalc[i] > fftAvg[i]) // rise fast + fftAvg[i] = fftCalc[i] *0.75f + 0.25f*fftAvg[i]; // will need approx 2 cycles (50ms) for converging against fftCalc[i] + else { // fall slow + if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i]; // approx 5 cycles (225ms) for falling to zero + else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // default - approx 9 cycles (225ms) for falling to zero + else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i]; // approx 14 cycles (350ms) for falling to zero + else fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // approx 20 cycles (500ms) for falling to zero + } + // constrain internal vars - just to be sure + fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f); + fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f); + + float currentResult; + if(limiterOn == true) + currentResult = fftAvg[i]; + else + currentResult = fftCalc[i]; + + switch (FFTScalingMode) { + case 1: + // Logarithmic scaling + currentResult *= 0.42f; // 42 is the answer ;-) + currentResult -= 8.0f; // this skips the lowest row, giving some room for peaks + if (currentResult > 1.0f) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function + else currentResult = 0.0f; // special handling, because log(1) = 0; log(0) = undefined + currentResult *= 0.85f + (float(i)/18.0f); // extra up-scaling for high frequencies + currentResult = mapf(currentResult, 0, LOG_256, 0, 255); // map [log(1) ... log(255)] to [0 ... 255] + break; + case 2: + // Linear scaling + currentResult *= 0.30f; // needs a bit more damping, get stay below 255 + currentResult -= 4.0f; // giving a bit more room for peaks + if (currentResult < 1.0f) currentResult = 0.0f; + currentResult *= 0.85f + (float(i)/1.8f); // extra up-scaling for high frequencies + break; + case 3: + // square root scaling + currentResult *= 0.38f; + currentResult -= 6.0f; + if (currentResult > 1.0f) currentResult = sqrtf(currentResult); + else currentResult = 0.0f; // special handling, because sqrt(0) = undefined + currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies + currentResult = mapf(currentResult, 0.0, 16.0, 0.0, 255.0); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] + break; + + case 0: + default: + // no scaling - leave freq bins as-is + currentResult -= 4; // just a bit more room for peaks + break; + } + + // Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely. + if (soundAgc > 0) { // apply extra "GEQ Gain" if set by user + float post_gain = (float)inputLevel/128.0f; + if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; + currentResult *= post_gain; + } + fftResult[i] = constrain((int)currentResult, 0, 255); + } +} +//////////////////// +// Peak detection // +//////////////////// + +// peak detection is called from FFT task when vReal[] contains valid FFT results +static void detectSamplePeak(void) { + bool havePeak = false; + // softhack007: this code continuously triggers while amplitude in the selected bin is above a certain threshold. So it does not detect peaks - it detects high activity in a frequency bin. + // Poor man's beat detection by seeing if sample > Average + some value. + // This goes through ALL of the 255 bins - but ignores stupid settings + // Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync. + if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { + havePeak = true; + } + + if (havePeak) { + samplePeak = true; + timeOfPeak = millis(); + udpSamplePeak = true; + } +} + +static void autoResetPeak(void) { + uint16_t MinShowDelay = MAX(50, strip.getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC + if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. + samplePeak = false; + if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData + } +} + + +//////////////////// +// usermod class // +//////////////////// + +//class name. Use something descriptive and leave the ": public Usermod" part :) +class AudioReactive : public Usermod { + + private: + #ifndef AUDIOPIN + int8_t audioPin = -1; + #else + int8_t audioPin = AUDIOPIN; + #endif + #ifndef SR_DMTYPE // I2S mic type + uint8_t dmType = 1; // 0=none/disabled/analog; 1=generic I2S + #define SR_DMTYPE 1 // default type = I2S + #else + uint8_t dmType = SR_DMTYPE; + #endif + #ifndef I2S_SDPIN // aka DOUT + int8_t i2ssdPin = 32; + #else + int8_t i2ssdPin = I2S_SDPIN; + #endif + #ifndef I2S_WSPIN // aka LRCL + int8_t i2swsPin = 15; + #else + int8_t i2swsPin = I2S_WSPIN; + #endif + #ifndef I2S_CKPIN // aka BCLK + int8_t i2sckPin = 14; /*PDM: set to I2S_PIN_NO_CHANGE*/ + #else + int8_t i2sckPin = I2S_CKPIN; + #endif + #ifndef MCLK_PIN + int8_t mclkPin = I2S_PIN_NO_CHANGE; /* ESP32: only -1, 0, 1, 3 allowed*/ + #else + int8_t mclkPin = MCLK_PIN; + #endif + + // new "V2" audiosync struct - 40 Bytes + struct audioSyncPacket { + char header[6]; // 06 Bytes + float sampleRaw; // 04 Bytes - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting + float sampleSmth; // 04 Bytes - either "sampleAvg" or "sampleAgc" depending on soundAgc setting + uint8_t samplePeak; // 01 Bytes - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude + uint8_t reserved1; // 01 Bytes - for future extensions - not used yet + uint8_t fftResult[16]; // 16 Bytes + float FFT_Magnitude; // 04 Bytes + float FFT_MajorPeak; // 04 Bytes + }; + + // old "V1" audiosync struct - 83 Bytes - for backwards compatibility + struct audioSyncPacket_v1 { + char header[6]; // 06 Bytes + uint8_t myVals[32]; // 32 Bytes + int sampleAgc; // 04 Bytes + int sampleRaw; // 04 Bytes + float sampleAvg; // 04 Bytes + bool samplePeak; // 01 Bytes + uint8_t fftResult[16]; // 16 Bytes + double FFT_Magnitude; // 08 Bytes + double FFT_MajorPeak; // 08 Bytes + }; + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + #ifdef UM_AUDIOREACTIVE_ENABLE + bool enabled = true; + #else + bool enabled = false; + #endif + + bool initDone = false; + + // variables for UDP sound sync + WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) + unsigned long lastTime = 0; // last time of running UDP Microphone Sync + const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED + uint16_t audioSyncPort= 11988;// default port for UDP sound sync + + // used for AGC + int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) + double control_integrated = 0.0; // persistent across calls to agcAvg(); "integrator control" = accumulated error + + // variables used by getSample() and agcAvg() + int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed + double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controller. + double micLev = 0.0; // Used to convert returned value to have '0' as minimum. A leveller + float expAdjF = 0.0f; // Used for exponential filter. + float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. + int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) + int16_t rawSampleAgc = 0; // not smoothed AGC sample + + // variables used in effects + float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample + int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc + float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc + + // used to feed "Info" Page + unsigned long last_UDPTime = 0; // time of last valid UDP sound sync datapacket + int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) + float maxSample5sec = 0.0f; // max sample (after AGC) in last 5 seconds + unsigned long sampleMaxTimer = 0; // last time maxSample5sec was reset + #define CYCLE_SAMPLEMAX 3500 // time window for merasuring + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _inputLvl[]; + static const char _analogmic[]; + static const char _digitalmic[]; + static const char UDP_SYNC_HEADER[]; + static const char UDP_SYNC_HEADER_v1[]; + + // private methods + + //////////////////// + // Debug support // + //////////////////// + void logAudio() + { + if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable + #ifdef MIC_LOGGER + // Debugging functions for audio input and sound processing. Comment out the values you want to see + PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); + PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); + //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); + PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); + //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); + //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); + //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); + //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); + PLOT_PRINTLN(); + #endif + + #ifdef FFT_SAMPLING_LOG + #if 0 + for(int i=0; i maxVal) maxVal = fftResult[i]; + if(fftResult[i] < minVal) minVal = fftResult[i]; + } + for(int i = 0; i < NUM_GEQ_CHANNELS; i++) { + PLOT_PRINT(i); PLOT_PRINT(":"); + PLOT_PRINTF("%04ld ", map(fftResult[i], 0, (scaleValuesFromCurrentMaxVal ? maxVal : defaultScalingFromHighValue), (mapValuesToPlotterSpace*i*scalingToHighValue)+0, (mapValuesToPlotterSpace*i*scalingToHighValue)+scalingToHighValue-1)); + } + if(printMaxVal) { + PLOT_PRINTF("maxVal:%04d ", maxVal + (mapValuesToPlotterSpace ? 16*256 : 0)); + } + if(printMinVal) { + PLOT_PRINTF("%04d:minVal ", minVal); // printed with value first, then label, so negative values can be seen in Serial Monitor but don't throw off y axis in Serial Plotter + } + if(mapValuesToPlotterSpace) + PLOT_PRINTF("max:%04d ", (printMaxVal ? 17 : 16)*256); // print line above the maximum value we expect to see on the plotter to avoid autoscaling y axis + else { + PLOT_PRINTF("max:%04d ", 256); + } + PLOT_PRINTLN(); + #endif // FFT_SAMPLING_LOG + } // logAudio() + + + ////////////////////// + // Audio Processing // + ////////////////////// + + /* + * A "PI controller" multiplier to automatically adjust sound sensitivity. + * + * A few tricks are implemented so that sampleAgc does't only utilize 0% and 100%: + * 0. don't amplify anything below squelch (but keep previous gain) + * 1. gain input = maximum signal observed in the last 5-10 seconds + * 2. we use two setpoints, one at ~60%, and one at ~80% of the maximum signal + * 3. the amplification depends on signal level: + * a) normal zone - very slow adjustment + * b) emergency zone (<10% or >90%) - very fast adjustment + */ + void agcAvg(unsigned long the_time) + { + const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function + + float lastMultAgc = multAgc; // last multiplier used + float multAgcTemp = multAgc; // new multiplier + float tmpAgc = sampleReal * multAgc; // what-if amplified signal + + float control_error; // "control error" input for PI control + + if (last_soundAgc != soundAgc) + control_integrated = 0.0; // new preset - reset integrator + + // For PI controller, we need to have a constant "frequency" + // so let's make sure that the control loop is not running at insane speed + static unsigned long last_time = 0; + unsigned long time_now = millis(); + if ((the_time > 0) && (the_time < time_now)) time_now = the_time; // allow caller to override my clock + + if (time_now - last_time > 2) { + last_time = time_now; + + if((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0)) { + // MIC signal is "squelched" - deliver silence + tmpAgc = 0; + // we need to "spin down" the intgrated error buffer + if (fabs(control_integrated) < 0.01) control_integrated = 0.0; + else control_integrated *= 0.91; + } else { + // compute new setpoint + if (tmpAgc <= agcTarget0Up[AGC_preset]) + multAgcTemp = agcTarget0[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = first setpoint + else + multAgcTemp = agcTarget1[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = second setpoint + } + // limit amplification + if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; + if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; + + // compute error terms + control_error = multAgcTemp - lastMultAgc; + + if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f)) //integrator anti-windup by clamping + && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) + control_integrated += control_error * 0.002 * 0.25; // 2ms = integration time; 0.25 for damping + else + control_integrated *= 0.9; // spin down that beasty integrator + + // apply PI Control + tmpAgc = sampleReal * lastMultAgc; // check "zone" of the signal using previous gain + if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower energy zone + multAgcTemp = lastMultAgc + agcFollowFast[AGC_preset] * agcControlKp[AGC_preset] * control_error; + multAgcTemp += agcFollowFast[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; + } else { // "normal zone" + multAgcTemp = lastMultAgc + agcFollowSlow[AGC_preset] * agcControlKp[AGC_preset] * control_error; + multAgcTemp += agcFollowSlow[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; + } + + // limit amplification again - PI controller sometimes "overshoots" + //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32 + if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; + if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; + } + + // NOW finally amplify the signal + tmpAgc = sampleReal * multAgcTemp; // apply gain to signal + if (fabsf(sampleReal) < 2.0f) tmpAgc = 0.0f; // apply squelch threshold + //tmpAgc = constrain(tmpAgc, 0, 255); + if (tmpAgc > 255) tmpAgc = 255.0f; // limit to 8bit + if (tmpAgc < 1) tmpAgc = 0.0f; // just to be sure + + // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc + multAgc = multAgcTemp; + rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; + // update smoothed AGC sample + if (fabsf(tmpAgc) < 1.0f) + sampleAgc = 0.5f * tmpAgc + 0.5f * sampleAgc; // fast path to zero + else + sampleAgc += agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path + + sampleAgc = fabsf(sampleAgc); // // make sure we have a positive value + last_soundAgc = soundAgc; + } // agcAvg() + + // post-processing and filtering of MIC sample (micDataReal) from FFTcode() + void getSample() + { + float sampleAdj; // Gain adjusted sample value + float tmpSample; // An interim sample variable used for calculations. + const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release. + const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function + + #ifdef WLED_DISABLE_SOUND + micIn = inoise8(millis(), millis()); // Simulated analog read + micDataReal = micIn; + #else + #ifdef ARDUINO_ARCH_ESP32 + micIn = int(micDataReal); // micDataSm = ((micData * 3) + micData)/4; + #else + // this is the minimal code for reading analog mic input on 8266. + // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. + static unsigned long lastAnalogTime = 0; + static float lastAnalogValue = 0.0f; + if (millis() - lastAnalogTime > 20) { + micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only. + lastAnalogTime = millis(); + lastAnalogValue = micDataReal; + yield(); + } else micDataReal = lastAnalogValue; + micIn = int(micDataReal); + #endif + #endif + + micLev += (micDataReal-micLev) / 12288.0f; + if(micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // align MicLev to lowest input signal + + micIn -= micLev; // Let's center it to 0 now + // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. + float micInNoDC = fabsf(micDataReal - micLev); + expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); + expAdjF = fabsf(expAdjF); // Now (!) take the absolute value + + expAdjF = (expAdjF <= soundSquelch) ? 0: expAdjF; // simple noise gate + if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0; // do something meaningfull when "squelch = 0" + + tmpSample = expAdjF; + micIn = abs(micIn); // And get the absolute value of each sample + + sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel/128.0f + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment + sampleReal = tmpSample; + + sampleAdj = fmax(fmin(sampleAdj, 255), 0); // Question: why are we limiting the value to 8 bits ??? + sampleRaw = (int16_t)sampleAdj; // ONLY update sample ONCE!!!! + + // keep "peak" sample, but decay value if current sample is below peak + if ((sampleMax < sampleReal) && (sampleReal > 0.5f)) { + sampleMax = sampleMax + 0.5f * (sampleReal - sampleMax); // new peak - with some filtering + // another simple way to detect samplePeak - cannot detect beats, but reacts on peak volume + if (((binNum < 12) || ((maxVol < 1))) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { + samplePeak = true; + timeOfPeak = millis(); + udpSamplePeak = true; + } + } else { + if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) + sampleMax += 0.5f * (sampleReal - sampleMax); // over AGC Zone - get back quickly + else + sampleMax *= agcSampleDecay[AGC_preset]; // signal to zero --> 5-8sec + } + if (sampleMax < 0.5f) sampleMax = 0.0f; + + sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. + sampleAvg = fabsf(sampleAvg); // make sure we have a positive value + } // getSample() + + + /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). + * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) + */ + // effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly) + void limitSampleDynamics(void) { + const float bigChange = 196; // just a representative number - a large, expected sample value + static unsigned long last_time = 0; + static float last_volumeSmth = 0.0f; + + if (limiterOn == false) return; + + long delta_time = millis() - last_time; + delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up + float deltaSample = volumeSmth - last_volumeSmth; + + if (attackTime > 0) { // user has defined attack time > 0 + float maxAttack = bigChange * float(delta_time) / float(attackTime); + if (deltaSample > maxAttack) deltaSample = maxAttack; + } + if (decayTime > 0) { // user has defined decay time > 0 + float maxDecay = - bigChange * float(delta_time) / float(decayTime); + if (deltaSample < maxDecay) deltaSample = maxDecay; + } + + volumeSmth = last_volumeSmth + deltaSample; + + last_volumeSmth = volumeSmth; + last_time = millis(); + } + + + ////////////////////// + // UDP Sound Sync // + ////////////////////// + + // try to establish UDP sound sync connection + void connectUDPSoundSync(void) { + // This function tries to establish a UDP sync connection if needed + // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection + static unsigned long last_connection_attempt = 0; + + if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled + if (udpSyncConnected) return; // already connected + if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable + if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds + + // if we arrive here, we need a UDP connection but don't have one + last_connection_attempt = millis(); + connected(); // try to start UDP + } + + void transmitAudioData() + { + if (!udpSyncConnected) return; + //DEBUGSR_PRINTLN("Transmitting UDP Mic Packet"); + + audioSyncPacket transmitData; + memset(reinterpret_cast(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including "invisible" padding bytes added by the compiler - is fully initialized + + strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6); + // transmit samples that were not modified by limitSampleDynamics() + transmitData.sampleRaw = (soundAgc) ? rawSampleAgc: sampleRaw; + transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; + transmitData.samplePeak = udpSamplePeak ? 1:0; + udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it + transmitData.reserved1 = 0; + + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { + transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); + } + + transmitData.FFT_Magnitude = my_magnitude; + transmitData.FFT_MajorPeak = FFT_MajorPeak; + + if (fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error + fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); + fftUdp.endPacket(); + } + return; + } // transmitAudioData() + + static bool isValidUdpSyncVersion(const char *header) { + return strncmp_P(header, PSTR(UDP_SYNC_HEADER), 6) == 0; + } + static bool isValidUdpSyncVersion_v1(const char *header) { + return strncmp_P(header, PSTR(UDP_SYNC_HEADER_v1), 6) == 0; + } + + void decodeAudioData(int packetSize, uint8_t *fftBuff) { + audioSyncPacket *receivedPacket = reinterpret_cast(fftBuff); + // update samples for effects + volumeSmth = fmaxf(receivedPacket->sampleSmth, 0.0f); + volumeRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); + // update internal samples + sampleRaw = volumeRaw; + sampleAvg = volumeSmth; + rawSampleAgc = volumeRaw; + sampleAgc = volumeSmth; + multAgc = 1.0f; + // Only change samplePeak IF it's currently false. + // If it's true already, then the animation still needs to respond. + autoResetPeak(); + if (!samplePeak) { + samplePeak = receivedPacket->samplePeak >0 ? true:false; + if (samplePeak) timeOfPeak = millis(); + //userVar1 = samplePeak; + } + //These values are only available on the ESP32 + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; + my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); + FFT_Magnitude = my_magnitude; + FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + } + + void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { + audioSyncPacket_v1 *receivedPacket = reinterpret_cast(fftBuff); + // update samples for effects + volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); + volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample + // update internal samples + sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); + sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; + sampleAgc = volumeSmth; + rawSampleAgc = volumeRaw; + multAgc = 1.0f; + // Only change samplePeak IF it's currently false. + // If it's true already, then the animation still needs to respond. + autoResetPeak(); + if (!samplePeak) { + samplePeak = receivedPacket->samplePeak >0 ? true:false; + if (samplePeak) timeOfPeak = millis(); + //userVar1 = samplePeak; + } + //These values are only available on the ESP32 + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; + my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0); + FFT_Magnitude = my_magnitude; + FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0, 11025.0); // restrict value to range expected by effects + } + + bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received. + { + if (!udpSyncConnected) return false; + bool haveFreshData = false; + + size_t packetSize = fftUdp.parsePacket(); + if (packetSize > 5) { + //DEBUGSR_PRINTLN("Received UDP Sync Packet"); + uint8_t fftBuff[packetSize]; + fftUdp.read(fftBuff, packetSize); + + // VERIFY THAT THIS IS A COMPATIBLE PACKET + if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) { + decodeAudioData(packetSize, fftBuff); + //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); + haveFreshData = true; + receivedFormat = 2; + } else { + if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) { + decodeAudioData_v1(packetSize, fftBuff); + //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); + haveFreshData = true; + receivedFormat = 1; + } else receivedFormat = 0; // unknown format + } + } + return haveFreshData; + } + + + ////////////////////// + // usermod functions// + ////////////////////// + + public: + //Functions called by WLED or other usermods + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + * It is called *AFTER* readFromConfig() + */ + void setup() + { + disableSoundProcessing = true; // just to be sure + if (!initDone) { + // usermod exchangeable data + // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers + um_data = new um_data_t; + um_data->u_size = 8; + um_data->u_type = new um_types_t[um_data->u_size]; + um_data->u_data = new void*[um_data->u_size]; + um_data->u_data[0] = &volumeSmth; //*used (New) + um_data->u_type[0] = UMT_FLOAT; + um_data->u_data[1] = &volumeRaw; // used (New) + um_data->u_type[1] = UMT_UINT16; + um_data->u_data[2] = fftResult; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi) + um_data->u_type[2] = UMT_BYTE_ARR; + um_data->u_data[3] = &samplePeak; //*used (Puddlepeak, Ripplepeak, Waterfall) + um_data->u_type[3] = UMT_BYTE; + um_data->u_data[4] = &FFT_MajorPeak; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall) + um_data->u_type[4] = UMT_FLOAT; + um_data->u_data[5] = &my_magnitude; // used (New) + um_data->u_type[5] = UMT_FLOAT; + um_data->u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) + um_data->u_type[6] = UMT_BYTE; + um_data->u_data[7] = &binNum; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) + um_data->u_type[7] = UMT_BYTE; + } + + // Reset I2S peripheral for good measure + i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed + #if !defined(CONFIG_IDF_TARGET_ESP32C3) + delay(100); + periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 + #endif + delay(100); // Give that poor microphone some time to setup. + + useBandPassFilter = false; + switch (dmType) { + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + // stub cases for not-yet-supported I2S modes on other ESP32 chips + case 0: //ADC analog + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + case 5: //PDM Microphone + #endif + #endif + case 1: + DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); + break; + case 2: + DEBUGSR_PRINTLN(F("AR: ES7243 Microphone (right channel only).")); + audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + case 3: + DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); + audioSource = new SPH0654(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); + break; + case 4: + DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + case 5: + DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); + useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); + break; + #endif + case 6: + DEBUGSR_PRINTLN(F("AR: ES8388 Source")); + audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + // ADC over I2S is only possible on "classic" ESP32 + case 0: + default: + DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); + audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + useBandPassFilter = true; // PDM bandpass filter seems to help for bad quality analog + if (audioSource) audioSource->initialize(audioPin); + break; + #endif + } + delay(250); // give microphone enough time to initialise + + if (!audioSource) enabled = false; // audio failed to initialise + if (enabled) onUpdateBegin(false); // create FFT task + if (FFT_Task == nullptr) enabled = false; // FFT task creation failed + if (enabled) disableSoundProcessing = false; // all good - enable audio processing + + if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync + #ifdef WLED_DEBUG + DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); + #else + DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); + #endif + disableSoundProcessing = true; + } + + if (enabled) connectUDPSoundSync(); + initDone = true; + } + + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection + udpSyncConnected = false; + fftUdp.stop(); + } + + if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { + #ifndef ESP8266 + udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); + #else + udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); + #endif + } + } + + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + * + * Tips: + * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. + * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. + * + * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. + * Instead, use a timer check as shown here. + */ + void loop() + { + static unsigned long lastUMRun = millis(); + + if (!enabled) { + disableSoundProcessing = true; // keep processing suspended (FFT task) + lastUMRun = millis(); // update time keeping + return; + } + // We cannot wait indefinitely before processing audio data + if (strip.isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice + + // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) + if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed + &&( (realtimeMode == REALTIME_MODE_GENERIC) + ||(realtimeMode == REALTIME_MODE_E131) + ||(realtimeMode == REALTIME_MODE_UDP) + ||(realtimeMode == REALTIME_MODE_ADALIGHT) + ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed + { + #ifdef WLED_DEBUG + if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" + DEBUG_PRINTLN("[AR userLoop] realtime mode active - audio processing suspended."); + DEBUG_PRINTF( " RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); + } + #endif + disableSoundProcessing = true; + } else { + #ifdef WLED_DEBUG + if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" + DEBUG_PRINTLN("[AR userLoop] realtime mode ended - audio processing resumed."); + DEBUG_PRINTF( " RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); + } + #endif + if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping + disableSoundProcessing = false; + } + + 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 + if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source + + + // Only run the sampling code IF we're not in Receive mode or realtime mode + if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) { + if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) + + unsigned long t_now = millis(); // remember current time + int userloopDelay = int(t_now - lastUMRun); + if (lastUMRun == 0) userloopDelay=0; // startup - don't have valid data from last run. + + #ifdef WLED_DEBUG + // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. + // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS + //if ((userloopDelay > 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) { + //DEBUG_PRINTF("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n", userloopDelay); + //} + #endif + + // run filters, and repeat in case of loop delays (hick-up compensation) + if (userloopDelay <2) userloopDelay = 0; // minor glitch, no problem + if (userloopDelay >200) userloopDelay = 200; // limit number of filter re-runs + do { + getSample(); // run microphone sampling filters + agcAvg(t_now - userloopDelay); // Calculated the PI adjusted value as sampleAvg + userloopDelay -= 2; // advance "simulated time" by 2ms + } while (userloopDelay > 0); + lastUMRun = t_now; // update time keeping + + // update samples for effects (raw, smooth) + volumeSmth = (soundAgc) ? sampleAgc : sampleAvg; + volumeRaw = (soundAgc) ? rawSampleAgc: sampleRaw; + // update FFTMagnitude, taking into account AGC amplification + my_magnitude = FFT_Magnitude; // / 16.0f, 8.0f, 4.0f done in effects + if (soundAgc) my_magnitude *= multAgc; + if (volumeSmth < 1 ) my_magnitude = 0.001f; // noise gate closed - mute + + limitSampleDynamics(); + } // if (!disableSoundProcessing) + + autoResetPeak(); // auto-reset sample peak after strip minShowDelay + if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected + + connectUDPSoundSync(); // ensure we have a connection - if needed + + // UDP Microphone Sync - receive mode + if ((audioSyncEnabled & 0x02) && udpSyncConnected) { + // Only run the audio listener code if we're in Receive mode + static float syncVolumeSmth = 0; + bool have_new_sample = false; + if (millis() - lastTime > delayMs) { + have_new_sample = receiveAudioData(); + if (have_new_sample) last_UDPTime = millis(); +#ifdef ARDUINO_ARCH_ESP32 + else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266. +#endif + lastTime = millis(); + } + if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample + else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter + limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups + } + + #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) + static unsigned long lastMicLoggerTime = 0; + if (millis()-lastMicLoggerTime > 20) { + lastMicLoggerTime = millis(); + logAudio(); + } + #endif + + // Info Page: keep max sample from last 5 seconds + if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { + sampleMaxTimer = millis(); + maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing + if (sampleAvg < 1) maxSample5sec = 0; // noise gate + } else { + if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume + } + + //UDP Microphone Sync - transmit mode + if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { + // Only run the transmit code IF we're in Transmit mode + transmitAudioData(); + lastTime = millis(); + } + + } + + + bool getUMData(um_data_t **data) + { + if (!data || !enabled) return false; // no pointer provided by caller or not enabled -> exit + *data = um_data; + return true; + } + + + void onUpdateBegin(bool init) + { +#ifdef WLED_DEBUG + fftTime = sampleTime = 0; +#endif + // gracefully suspend FFT task (if running) + disableSoundProcessing = true; + + // reset sound data + micDataReal = 0.0f; + volumeRaw = 0; volumeSmth = 0; + sampleAgc = 0; sampleAvg = 0; + sampleRaw = 0; rawSampleAgc = 0; + my_magnitude = 0; FFT_Magnitude = 0; FFT_MajorPeak = 1; + multAgc = 1; + // reset FFT data + memset(fftCalc, 0, sizeof(fftCalc)); + memset(fftAvg, 0, sizeof(fftAvg)); + memset(fftResult, 0, sizeof(fftResult)); + for(int i=(init?0:1); i=0 + && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) + ) { + return true; + } + return false; + } + + + //////////////////////////// + // Settings and Info Page // + //////////////////////////// + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + void addToJsonInfo(JsonObject& root) + { + char myStringBuffer[16]; // buffer for snprintf() + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + + String uiDomString = F(""); + infoArr.add(uiDomString); + + if (enabled) { + // Input Level Slider + if (disableSoundProcessing == false) { // only show slider when audio processing is running + if (soundAgc > 0) { + infoArr = user.createNestedArray(F("GEQ Input Level")); // if AGC is on, this slider only affects fftResult[] frequencies + } else { + infoArr = user.createNestedArray(F("Audio Input Level")); + } + uiDomString = F("
"); // + infoArr.add(uiDomString); + } + + // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG + + // current Audio input + infoArr = user.createNestedArray(F("Audio Source")); + if (audioSyncEnabled & 0x02) { + // UDP sound sync - receive mode + infoArr.add(F("UDP sound sync")); + if (udpSyncConnected) { + if (millis() - last_UDPTime < 2500) + infoArr.add(F(" - receiving")); + else + infoArr.add(F(" - idle")); + } else { + infoArr.add(F(" - no connection")); + } + } else { + // Analog or I2S digital input + if (audioSource && (audioSource->isInitialized())) { + // audio source successfully configured + if (audioSource->getType() == AudioSource::Type_I2SAdc) { + infoArr.add(F("ADC analog")); + } else { + infoArr.add(F("I2S digital")); + } + // input level or "silence" + if (maxSample5sec > 1.0f) { + float my_usage = 100.0f * (maxSample5sec / 255.0f); + snprintf_P(myStringBuffer, 15, PSTR(" - peak %3d%%"), int(my_usage)); + infoArr.add(myStringBuffer); + } else { + infoArr.add(F(" - quiet")); + } + } else { + // error during audio source setup + infoArr.add(F("not initialized")); + infoArr.add(F(" - check pin settings")); + } + } + + // Sound processing (FFT and input filters) + infoArr = user.createNestedArray(F("Sound Processing")); + if (audioSource && (disableSoundProcessing == false)) { + infoArr.add(F("running")); + } else { + infoArr.add(F("suspended")); + } + + // AGC or manual Gain + if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + infoArr = user.createNestedArray(F("Manual Gain")); + float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // non-AGC gain from presets + infoArr.add(roundf(myGain*100.0f) / 100.0f); + infoArr.add("x"); + } + if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + infoArr = user.createNestedArray(F("AGC Gain")); + infoArr.add(roundf(multAgc*100.0f) / 100.0f); + infoArr.add("x"); + } + + // UDP Sound Sync status + infoArr = user.createNestedArray(F("UDP Sound Sync")); + if (audioSyncEnabled) { + if (audioSyncEnabled & 0x01) { + infoArr.add(F("send mode")); + if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); + } else if (audioSyncEnabled & 0x02) { + infoArr.add(F("receive mode")); + } + } else + infoArr.add("off"); + if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); + if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { + if (receivedFormat == 1) infoArr.add(F(" v1")); + if (receivedFormat == 2) infoArr.add(F(" v2")); + } + + #if defined(WLED_DEBUG) || defined(SR_DEBUG) + infoArr = user.createNestedArray(F("Sampling time")); + infoArr.add(float(sampleTime)/100.0f); + infoArr.add(" ms"); + + infoArr = user.createNestedArray(F("FFT time")); + infoArr.add(float(fftTime)/100.0f); + if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow + infoArr.add("! ms"); + else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability + infoArr.add(" ms!"); + else + infoArr.add(" ms"); + + DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); + DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); + #endif + } + } + + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) + { + if (!initDone) return; // prevent crash on boot applyPreset() + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) { + usermod = root.createNestedObject(FPSTR(_name)); + } + usermod["on"] = enabled; + } + + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) + { + if (!initDone) return; // prevent crash on boot applyPreset() + bool prevEnabled = enabled; + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) { + if (usermod[FPSTR(_enabled)].is()) { + enabled = usermod[FPSTR(_enabled)].as(); + if (prevEnabled != enabled) onUpdateBegin(!enabled); + } + if (usermod[FPSTR(_inputLvl)].is()) { + inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); + } + } + } + + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will make your settings editable through the Usermod Settings page automatically. + * + * Usermod Settings Overview: + * - Numeric values are treated as floats in the browser. + * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float + * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and + * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. + * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. + * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a + * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. + * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type + * used in the Usermod when reading the value from ArduinoJson. + * - Pin values can be treated differently from an integer value by using the key name "pin" + * - "pin" can contain a single or array of integer values + * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins + * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) + * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used + * + * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings + * + * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. + * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. + * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); + amic["pin"] = audioPin; + #endif + + JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); + dmic[F("type")] = dmType; + JsonArray pinArray = dmic.createNestedArray("pin"); + pinArray.add(i2ssdPin); + pinArray.add(i2swsPin); + pinArray.add(i2sckPin); + pinArray.add(mclkPin); + + JsonObject cfg = top.createNestedObject("config"); + cfg[F("squelch")] = soundSquelch; + cfg[F("gain")] = sampleGain; + cfg[F("AGC")] = soundAgc; + + JsonObject dynLim = top.createNestedObject("dynamics"); + dynLim[F("limiter")] = limiterOn; + dynLim[F("rise")] = attackTime; + dynLim[F("fall")] = decayTime; + + JsonObject freqScale = top.createNestedObject("frequency"); + freqScale[F("scale")] = FFTScalingMode; + + JsonObject sync = top.createNestedObject("sync"); + sync[F("port")] = audioSyncPort; + sync[F("mode")] = audioSyncEnabled; + } + + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + * + * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present + * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them + * + * This function is guaranteed to be called on boot, but could also be called every time settings are updated + */ + bool readFromConfig(JsonObject& root) + { + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); + #else + audioPin = -1; // MCU does not support analog mic + #endif + + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM + #endif + #endif + + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][1], i2swsPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][2], i2sckPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][3], mclkPin); + + configComplete &= getJsonValue(top["config"][F("squelch")], soundSquelch); + configComplete &= getJsonValue(top["config"][F("gain")], sampleGain); + configComplete &= getJsonValue(top["config"][F("AGC")], soundAgc); + + configComplete &= getJsonValue(top["dynamics"][F("limiter")], limiterOn); + configComplete &= getJsonValue(top["dynamics"][F("rise")], attackTime); + configComplete &= getJsonValue(top["dynamics"][F("fall")], decayTime); + + configComplete &= getJsonValue(top["frequency"][F("scale")], FFTScalingMode); + + configComplete &= getJsonValue(top["sync"][F("port")], audioSyncPort); + configComplete &= getJsonValue(top["sync"][F("mode")], audioSyncEnabled); + + return configComplete; + } + + + void appendConfigData() + { + oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + oappend(SET_F("addOption(dd,'Generic Analog',0);")); + #endif + oappend(SET_F("addOption(dd,'Generic I2S',1);")); + oappend(SET_F("addOption(dd,'ES7243',2);")); + oappend(SET_F("addOption(dd,'SPH0654',3);")); + oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); + #endif + oappend(SET_F("addOption(dd,'ES8388',6);")); + + oappend(SET_F("dd=addDropdown('AudioReactive','config:AGC');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'Normal',1);")); + oappend(SET_F("addOption(dd,'Vivid',2);")); + oappend(SET_F("addOption(dd,'Lazy',3);")); + + oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:limiter');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'On',1);")); + oappend(SET_F("addInfo('AudioReactive:dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('AudioReactive:dynamics:rise',1,'ms (♪ effects only)');")); + oappend(SET_F("addInfo('AudioReactive:dynamics:fall',1,'ms (♪ effects only)');")); + + oappend(SET_F("dd=addDropdown('AudioReactive','frequency:scale');")); + oappend(SET_F("addOption(dd,'None',0);")); + oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); + oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); + oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); + + oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'Send',1);")); + oappend(SET_F("addOption(dd,'Receive',2);")); + oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',0,'sd/data/dout','I2S SD');")); + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',1,'ws/clk/lrck','I2S WS');")); + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',2,'sck/bclk','I2S SCK');")); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'only use -1, 0, 1 or 3','I2S MCLK');")); + #else + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); + #endif + } + + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + //void handleOverlayDraw() + //{ + //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + //} + + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_AUDIOREACTIVE; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char AudioReactive::_name[] PROGMEM = "AudioReactive"; +const char AudioReactive::_enabled[] PROGMEM = "enabled"; +const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel"; +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) +const char AudioReactive::_analogmic[] PROGMEM = "analogmic"; +#endif +const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic"; +const char AudioReactive::UDP_SYNC_HEADER[] PROGMEM = "00002"; // new sync header version, as format no longer compatible with previous structure +const char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = "00001"; // old sync header version - need to add backwards-compatibility feature diff --git a/usermods/audioreactive/audio_source.h b/usermods/audioreactive/audio_source.h new file mode 100644 index 0000000000..e970e19c6d --- /dev/null +++ b/usermods/audioreactive/audio_source.h @@ -0,0 +1,773 @@ +#pragma once +#ifdef ARDUINO_ARCH_ESP32 +#include "wled.h" +#include +#include +#include // needed for SPH0465 timing workaround (classic ESP32) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#include +#include +#endif +// type of i2s_config_t.SampleRate was changed from "int" to "unsigned" in IDF 4.4.x +#define SRate_t uint32_t +#else +#define SRate_t int +#endif + +//#include +//#include +//#include +//#include + +// see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/chip-series-comparison.html#related-documents +// and https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/i2s.html#overview-of-all-modes +#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) + // there are two things in these MCUs that could lead to problems with audio processing: + // * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x) + // * single core, so FFT task might slow down other things like LED updates + #if !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1) + #error This audio reactive usermod does not support ESP32-C2 or ESP32-C3. + #else + #warning This audio reactive usermod does not support ESP32-C2 and ESP32-C3. + #endif +#endif + +/* ToDo: remove. ES7243 is controlled via compiler defines + Until this configuration is moved to the webinterface +*/ + +// if you have problems to get your microphone work on the left channel, uncomment the following line +//#define I2S_USE_RIGHT_CHANNEL // (experimental) define this to use right channel (digital mics only) + +// Uncomment the line below to utilize ADC1 _exclusively_ for I2S sound input. +// benefit: analog mic inputs will be sampled contiously -> better response times and less "glitches" +// WARNING: this option WILL lock-up your device in case that any other analogRead() operation is performed; +// for example if you want to read "analog buttons" +//#define I2S_GRAB_ADC1_COMPLETELY // (experimental) continuously sample analog ADC microphone. WARNING will cause analogRead() lock-up + +// data type requested from the I2S driver - currently we always use 32bit +//#define I2S_USE_16BIT_SAMPLES // (experimental) define this to request 16bit - more efficient but possibly less compatible + +#ifdef I2S_USE_16BIT_SAMPLES +#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_16BIT +#define I2S_datatype int16_t +#define I2S_unsigned_datatype uint16_t +#define I2S_data_size I2S_BITS_PER_CHAN_16BIT +#undef I2S_SAMPLE_DOWNSCALE_TO_16BIT +#else +#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_32BIT +//#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_24BIT +#define I2S_datatype int32_t +#define I2S_unsigned_datatype uint32_t +#define I2S_data_size I2S_BITS_PER_CHAN_32BIT +#define I2S_SAMPLE_DOWNSCALE_TO_16BIT +#endif + +/* There are several (confusing) options in IDF 4.4.x: + * I2S_CHANNEL_FMT_RIGHT_LEFT, I2S_CHANNEL_FMT_ALL_RIGHT and I2S_CHANNEL_FMT_ALL_LEFT stands for stereo mode, which means two channels will transport different data. + * I2S_CHANNEL_FMT_ONLY_RIGHT and I2S_CHANNEL_FMT_ONLY_LEFT they are mono mode, both channels will only transport same data. + * I2S_CHANNEL_FMT_MULTIPLE means TDM channels, up to 16 channel will available, and they are stereo as default. + * if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case. +*/ + +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 4)) +// espressif bug: only_left has no sound, left and right are swapped +// https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138) +// https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918) +// https://github.com/espressif/esp-idf/issues/6625 I2S: left/right channels are swapped for read (IDFGH-4826) +#ifdef I2S_USE_RIGHT_CHANNEL +#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT +#define I2S_MIC_CHANNEL_TEXT "right channel only (work-around swapped channel bug in IDF 4.4)." +#define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT +#define I2S_PDM_MIC_CHANNEL_TEXT "right channel only" +#else +//#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ALL_LEFT +//#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_RIGHT_LEFT +#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT +#define I2S_MIC_CHANNEL_TEXT "left channel only (work-around swapped channel bug in IDF 4.4)." +#define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT +#define I2S_PDM_MIC_CHANNEL_TEXT "left channel only." +#endif + +#else +// not swapped +#ifdef I2S_USE_RIGHT_CHANNEL +#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT +#define I2S_MIC_CHANNEL_TEXT "right channel only." +#else +#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT +#define I2S_MIC_CHANNEL_TEXT "left channel only." +#endif +#define I2S_PDM_MIC_CHANNEL I2S_MIC_CHANNEL +#define I2S_PDM_MIC_CHANNEL_TEXT I2S_MIC_CHANNEL_TEXT + +#endif + + +/* Interface class + AudioSource serves as base class for all microphone types + This enables accessing all microphones with one single interface + which simplifies the caller code +*/ +class AudioSource { + public: + /* All public methods are virtual, so they can be overridden + Everything but the destructor is also removed, to make sure each mic + Implementation provides its version of this function + */ + virtual ~AudioSource() {}; + + /* Initialize + This function needs to take care of anything that needs to be done + before samples can be obtained from the microphone. + */ + virtual void initialize(int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) = 0; + + /* Deinitialize + Release all resources and deactivate any functionality that is used + by this microphone + */ + virtual void deinitialize() = 0; + + /* getSamples + Read num_samples from the microphone, and store them in the provided + buffer + */ + virtual void getSamples(float *buffer, uint16_t num_samples) = 0; + + /* check if the audio source driver was initialized successfully */ + virtual bool isInitialized(void) {return(_initialized);} + + /* identify Audiosource type - I2S-ADC or I2S-digital */ + typedef enum{Type_unknown=0, Type_I2SAdc=1, Type_I2SDigital=2} AudioSourceType; + virtual AudioSourceType getType(void) {return(Type_I2SDigital);} // default is "I2S digital source" - ADC type overrides this method + + protected: + /* Post-process audio sample - currently on needed for I2SAdcSource*/ + virtual I2S_datatype postProcessSample(I2S_datatype sample_in) {return(sample_in);} // default method can be overriden by instances (ADC) that need sample postprocessing + + // Private constructor, to make sure it is not callable except from derived classes + AudioSource(SRate_t sampleRate, int blockSize, float sampleScale) : + _sampleRate(sampleRate), + _blockSize(blockSize), + _initialized(false), + _sampleScale(sampleScale) + {}; + + SRate_t _sampleRate; // Microphone sampling rate + int _blockSize; // I2S block size + bool _initialized; // Gets set to true if initialization is successful + float _sampleScale; // pre-scaling factor for I2S samples +}; + +/* Basic I2S microphone source + All functions are marked virtual, so derived classes can replace them +*/ +class I2SSource : public AudioSource { + public: + I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : + AudioSource(sampleRate, blockSize, sampleScale) { + _config = { + .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), + .sample_rate = _sampleRate, + .bits_per_sample = I2S_SAMPLE_RESOLUTION, + .channel_format = I2S_MIC_CHANNEL, +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), + //.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2, + .dma_buf_count = 8, + .dma_buf_len = _blockSize, + .use_apll = 0, + .bits_per_chan = I2S_data_size, +#else + .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .dma_buf_count = 8, + .dma_buf_len = _blockSize, + .use_apll = false +#endif + }; + } + + virtual void initialize(int8_t i2swsPin = I2S_PIN_NO_CHANGE, int8_t i2ssdPin = I2S_PIN_NO_CHANGE, int8_t i2sckPin = I2S_PIN_NO_CHANGE, int8_t mclkPin = I2S_PIN_NO_CHANGE) { + DEBUGSR_PRINTLN("I2SSource:: initialize()."); + if (i2swsPin != I2S_PIN_NO_CHANGE && i2ssdPin != I2S_PIN_NO_CHANGE) { + if (!pinManager.allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || + !pinManager.allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206 + DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: ws=%d, sd=%d\n", i2swsPin, i2ssdPin); + return; + } + } + + // i2ssckPin needs special treatment, since it might be unused on PDM mics + if (i2sckPin != I2S_PIN_NO_CHANGE) { + if (!pinManager.allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { + DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: sck=%d\n", i2sckPin); + return; + } + } else { + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + #if !defined(SOC_I2S_SUPPORTS_PDM_RX) + #warning this MCU does not support PDM microphones + #endif + #endif + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + // This is an I2S PDM microphone, these microphones only use a clock and + // data line, to make it simpler to debug, use the WS pin as CLK and SD pin as DATA + // example from espressif: https://github.com/espressif/esp-idf/blob/release/v4.4/examples/peripherals/i2s/i2s_audio_recorder_sdcard/main/i2s_recorder_main.c + + // note to self: PDM has known bugs on S3, and does not work on C3 + // * S3: PDM sample rate only at 50% of expected rate: https://github.com/espressif/esp-idf/issues/9893 + // * S3: I2S PDM has very low amplitude: https://github.com/espressif/esp-idf/issues/8660 + // * C3: does not support PDM to PCM input. SoC would allow PDM RX, but there is no hardware to directly convert to PCM so it will not work. https://github.com/espressif/esp-idf/issues/8796 + + _config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); // Change mode to pdm if clock pin not provided. PDM is not supported on ESP32-S2. PDM RX not supported on ESP32-C3 + _config.channel_format =I2S_PDM_MIC_CHANNEL; // seems that PDM mono mode always uses left channel. + _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality + #endif + } + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + if (mclkPin != I2S_PIN_NO_CHANGE) { + _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality, and to avoid glitches. + // //_config.fixed_mclk = 512 * _sampleRate; + // //_config.fixed_mclk = 256 * _sampleRate; + } + + #if !defined(SOC_I2S_SUPPORTS_APLL) + #warning this MCU does not have an APLL high accuracy clock for audio + // S3: not supported; S2: supported; C3: not supported + _config.use_apll = false; // APLL not supported on this MCU + #endif + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0 + #endif +#endif + + // Reserve the master clock pin if provided + _mclkPin = mclkPin; + if (mclkPin != I2S_PIN_NO_CHANGE) { + if(!pinManager.allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { + DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); + return; + } else + _routeMclk(mclkPin); + } + + _pinConfig = { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + .mck_io_num = mclkPin, // "classic" ESP32 supports setting MCK on GPIO0/GPIO1/GPIO3 only. i2s_set_pin() will fail if wrong mck_io_num is provided. +#endif + .bck_io_num = i2sckPin, + .ws_io_num = i2swsPin, + .data_out_num = I2S_PIN_NO_CHANGE, + .data_in_num = i2ssdPin + }; + + //DEBUGSR_PRINTF("[AR] I2S: SD=%d, WS=%d, SCK=%d, MCLK=%d\n", i2ssdPin, i2swsPin, i2sckPin, mclkPin); + + esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); + if (err != ESP_OK) { + DEBUGSR_PRINTF("AR: Failed to install i2s driver: %d\n", err); + return; + } + + DEBUGSR_PRINTF("AR: I2S#0 driver %s aPLL; fixed_mclk=%d.\n", _config.use_apll? "uses":"without", _config.fixed_mclk); + DEBUGSR_PRINTF("AR: %d bits, Sample scaling factor = %6.4f\n", _config.bits_per_sample, _sampleScale); + if (_config.mode & I2S_MODE_PDM) { + DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in PDM MASTER mode.")); + } else { + DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in MASTER mode.")); + } + + err = i2s_set_pin(I2S_NUM_0, &_pinConfig); + if (err != ESP_OK) { + DEBUGSR_PRINTF("AR: Failed to set i2s pin config: %d\n", err); + i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver + return; + } + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + err = i2s_set_clk(I2S_NUM_0, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. + if (err != ESP_OK) { + DEBUGSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); + i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver + return; + } +#endif + _initialized = true; + } + + virtual void deinitialize() { + _initialized = false; + esp_err_t err = i2s_driver_uninstall(I2S_NUM_0); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); + return; + } + if (_pinConfig.ws_io_num != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_pinConfig.ws_io_num, PinOwner::UM_Audioreactive); + if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive); + if (_pinConfig.bck_io_num != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_pinConfig.bck_io_num, PinOwner::UM_Audioreactive); + // Release the master clock pin + if (_mclkPin != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_mclkPin, PinOwner::UM_Audioreactive); + } + + virtual void getSamples(float *buffer, uint16_t num_samples) { + if (_initialized) { + esp_err_t err; + size_t bytes_read = 0; /* Counter variable to check if we actually got enough data */ + I2S_datatype newSamples[num_samples]; /* Intermediary sample storage */ + + err = i2s_read(I2S_NUM_0, (void *)newSamples, sizeof(newSamples), &bytes_read, portMAX_DELAY); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to get samples: %d\n", err); + return; + } + + // For correct operation, we need to read exactly sizeof(samples) bytes from i2s + if (bytes_read != sizeof(newSamples)) { + DEBUGSR_PRINTF("Failed to get enough samples: wanted: %d read: %d\n", sizeof(newSamples), bytes_read); + return; + } + + // Store samples in sample buffer and update DC offset + for (int i = 0; i < num_samples; i++) { + + newSamples[i] = postProcessSample(newSamples[i]); // perform postprocessing (needed for ADC samples) + + float currSample = 0.0f; +#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT + currSample = (float) newSamples[i] / 65536.0f; // 32bit input -> 16bit; keeping lower 16bits as decimal places +#else + currSample = (float) newSamples[i]; // 16bit input -> use as-is +#endif + buffer[i] = currSample; + buffer[i] *= _sampleScale; // scale samples + } + } + } + + protected: + void _routeMclk(int8_t mclkPin) { +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + // MCLK routing by writing registers is not needed any more with IDF > 4.4.0 + #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) + // this way of MCLK routing only works on "classic" ESP32 + /* Enable the mclk routing depending on the selected mclk pin (ESP32: only 0,1,3) + Only I2S_NUM_0 is supported + */ + if (mclkPin == GPIO_NUM_0) { + PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1); + WRITE_PERI_REG(PIN_CTRL,0xFFF0); + } else if (mclkPin == GPIO_NUM_1) { + PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD_CLK_OUT3); + WRITE_PERI_REG(PIN_CTRL, 0xF0F0); + } else { + PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD_CLK_OUT2); + WRITE_PERI_REG(PIN_CTRL, 0xFF00); + } + #endif +#endif + } + + i2s_config_t _config; + i2s_pin_config_t _pinConfig; + int8_t _mclkPin; +}; + +/* ES7243 Microphone + This is an I2S microphone that requires initialization over + I2C before I2S data can be received +*/ +class ES7243 : public I2SSource { + private: + + void _es7243I2cWrite(uint8_t reg, uint8_t val) { + #ifndef ES7243_ADDR + #define ES7243_ADDR 0x13 // default address + #endif + Wire.beginTransmission(ES7243_ADDR); + Wire.write((uint8_t)reg); + Wire.write((uint8_t)val); + uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK + if (i2cErr != 0) { + DEBUGSR_PRINTF("AR: ES7243 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES7243_ADDR, reg, val); + } + } + + void _es7243InitAdc() { + _es7243I2cWrite(0x00, 0x01); + _es7243I2cWrite(0x06, 0x00); + _es7243I2cWrite(0x05, 0x1B); + _es7243I2cWrite(0x01, 0x00); // 0x00 for 24 bit to match INMP441 - not sure if this needs adjustment to get 16bit samples from I2S + _es7243I2cWrite(0x08, 0x43); + _es7243I2cWrite(0x05, 0x13); + } + +public: + ES7243(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : + I2SSource(sampleRate, blockSize, sampleScale) { + _config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; + }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("ES7243:: initialize();"); + if ((i2sckPin < 0) || (mclkPin < 0)) { + DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + return; + } + + // First route mclk, then configure ADC over I2C, then configure I2S + _es7243InitAdc(); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + } + + void deinitialize() { + I2SSource::deinitialize(); + } +}; + +/* ES8388 Sound Module + This is an I2S sound processing unit that requires initialization over + I2C before I2S data can be received. +*/ +class ES8388Source : public I2SSource { + private: + + void _es8388I2cWrite(uint8_t reg, uint8_t val) { +#ifndef ES8388_ADDR + Wire.beginTransmission(0x10); + #define ES8388_ADDR 0x10 // default address +#else + Wire.beginTransmission(ES8388_ADDR); +#endif + Wire.write((uint8_t)reg); + Wire.write((uint8_t)val); + uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK + if (i2cErr != 0) { + DEBUGSR_PRINTF("AR: ES8388 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES8388_ADDR, reg, val); + } + } + + void _es8388InitAdc() { + // https://dl.radxa.com/rock2/docs/hw/ds/ES8388%20user%20Guide.pdf Section 10.1 + // http://www.everest-semi.com/pdf/ES8388%20DS.pdf Better spec sheet, more clear. + // https://docs.google.com/spreadsheets/d/1CN3MvhkcPVESuxKyx1xRYqfUit5hOdsG45St9BCUm-g/edit#gid=0 generally + // Sets ADC to around what AudioReactive expects, and loops line-in to line-out/headphone for monitoring. + // Registries are decimal, settings are binary as that's how everything is listed in the docs + // ...which makes it easier to reference the docs. + // + _es8388I2cWrite( 8,0b00000000); // I2S to slave + _es8388I2cWrite( 2,0b11110011); // Power down DEM and STM + _es8388I2cWrite(43,0b10000000); // Set same LRCK + _es8388I2cWrite( 0,0b00000101); // Set chip to Play & Record Mode + _es8388I2cWrite(13,0b00000010); // Set MCLK/LRCK ratio to 256 + _es8388I2cWrite( 1,0b01000000); // Power up analog and lbias + _es8388I2cWrite( 3,0b00000000); // Power up ADC, Analog Input, and Mic Bias + _es8388I2cWrite( 4,0b11111100); // Power down DAC, Turn on LOUT1 and ROUT1 and LOUT2 and ROUT2 power + _es8388I2cWrite( 2,0b01000000); // Power up DEM and STM and undocumented bit for "turn on line-out amp" + + // #define use_es8388_mic + + #ifdef use_es8388_mic + // The mics *and* line-in are BOTH connected to LIN2/RIN2 on the AudioKit + // so there's no way to completely eliminate the mics. It's also hella noisy. + // Line-in works OK on the AudioKit, generally speaking, as the mics really need + // amplification to be noticeable in a quiet room. If you're in a very loud room, + // the mics on the AudioKit WILL pick up sound even in line-in mode. + // TL;DR: Don't use the AudioKit for anything, use the LyraT. + // + // The LyraT does a reasonable job with mic input as configured below. + + // Pick one of these. If you have to use the mics, use a LyraT over an AudioKit if you can: + _es8388I2cWrite(10,0b00000000); // Use Lin1/Rin1 for ADC input (mic on LyraT) + //_es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input (mic *and* line-in on AudioKit) + + _es8388I2cWrite( 9,0b10001000); // Select Analog Input PGA Gain for ADC to +24dB (L+R) + _es8388I2cWrite(16,0b00000000); // Set ADC digital volume attenuation to 0dB (left) + _es8388I2cWrite(17,0b00000000); // Set ADC digital volume attenuation to 0dB (right) + _es8388I2cWrite(38,0b00011011); // Mixer - route LIN1/RIN1 to output after mic gain + + _es8388I2cWrite(39,0b01000000); // Mixer - route LIN to mixL, +6dB gain + _es8388I2cWrite(42,0b01000000); // Mixer - route RIN to mixR, +6dB gain + _es8388I2cWrite(46,0b00100001); // LOUT1VOL - 0b00100001 = +4.5dB + _es8388I2cWrite(47,0b00100001); // ROUT1VOL - 0b00100001 = +4.5dB + _es8388I2cWrite(48,0b00100001); // LOUT2VOL - 0b00100001 = +4.5dB + _es8388I2cWrite(49,0b00100001); // ROUT2VOL - 0b00100001 = +4.5dB + + // Music ALC - the mics like Auto Level Control + // You can also use this for line-in, but it's not really needed. + // + _es8388I2cWrite(18,0b11111000); // ALC: stereo, max gain +35.5dB, min gain -12dB + _es8388I2cWrite(19,0b00110000); // ALC: target -1.5dB, 0ms hold time + _es8388I2cWrite(20,0b10100110); // ALC: gain ramp up = 420ms/93ms, gain ramp down = check manual for calc + _es8388I2cWrite(21,0b00000110); // ALC: use "ALC" mode, no zero-cross, window 96 samples + _es8388I2cWrite(22,0b01011001); // ALC: noise gate threshold, PGA gain constant, noise gate enabled + #else + _es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input ("line-in") + _es8388I2cWrite( 9,0b00000000); // Select Analog Input PGA Gain for ADC to 0dB (L+R) + _es8388I2cWrite(16,0b01000000); // Set ADC digital volume attenuation to -32dB (left) + _es8388I2cWrite(17,0b01000000); // Set ADC digital volume attenuation to -32dB (right) + _es8388I2cWrite(38,0b00001001); // Mixer - route LIN2/RIN2 to output + + _es8388I2cWrite(39,0b01010000); // Mixer - route LIN to mixL, 0dB gain + _es8388I2cWrite(42,0b01010000); // Mixer - route RIN to mixR, 0dB gain + _es8388I2cWrite(46,0b00011011); // LOUT1VOL - 0b00011110 = +0dB, 0b00011011 = LyraT balance fix + _es8388I2cWrite(47,0b00011110); // ROUT1VOL - 0b00011110 = +0dB + _es8388I2cWrite(48,0b00011110); // LOUT2VOL - 0b00011110 = +0dB + _es8388I2cWrite(49,0b00011110); // ROUT2VOL - 0b00011110 = +0dB + #endif + + } + + public: + ES8388Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + I2SSource(sampleRate, blockSize, sampleScale) { + _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; + }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("ES8388Source:: initialize();"); + if ((i2sckPin < 0) || (mclkPin < 0)) { + DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + return; + } + + // First route mclk, then configure ADC over I2C, then configure I2S + _es8388InitAdc(); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + } + + void deinitialize() { + I2SSource::deinitialize(); + } + +}; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) +#if !defined(SOC_I2S_SUPPORTS_ADC) && !defined(SOC_I2S_SUPPORTS_ADC_DAC) + #warning this MCU does not support analog sound input +#endif +#endif + +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) +// ADC over I2S is only availeable in "classic" ESP32 + +/* ADC over I2S Microphone + This microphone is an ADC pin sampled via the I2S interval + This allows to use the I2S API to obtain ADC samples with high sample rates + without the need of manual timing of the samples +*/ +class I2SAdcSource : public I2SSource { + public: + I2SAdcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : + I2SSource(sampleRate, blockSize, sampleScale) { + _config = { + .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), + .sample_rate = _sampleRate, + .bits_per_sample = I2S_SAMPLE_RESOLUTION, + .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), +#else + .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), +#endif + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .dma_buf_count = 8, + .dma_buf_len = _blockSize, + .use_apll = false, + .tx_desc_auto_clear = false, + .fixed_mclk = 0 + }; + } + + /* identify Audiosource type - I2S-ADC*/ + AudioSourceType getType(void) {return(Type_I2SAdc);} + + void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { + DEBUGSR_PRINTLN("I2SAdcSource:: initialize()."); + _myADCchannel = 0x0F; + if(!pinManager.allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) { + DEBUGSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin); + return; + } + _audioPin = audioPin; + + // Determine Analog channel. Only Channels on ADC1 are supported + int8_t channel = digitalPinToAnalogChannel(_audioPin); + if (channel > 9) { + DEBUGSR_PRINTF("Incompatible GPIO used for analog audio input: %d\n", _audioPin); + return; + } else { + adc_gpio_init(ADC_UNIT_1, adc_channel_t(channel)); + _myADCchannel = channel; + } + + // Install Driver + esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to install i2s driver: %d\n", err); + return; + } + + adc1_config_width(ADC_WIDTH_BIT_12); // ensure that ADC runs with 12bit resolution + + // Enable I2S mode of ADC + err = i2s_set_adc_mode(ADC_UNIT_1, adc1_channel_t(channel)); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to set i2s adc mode: %d\n", err); + return; + } + + // see example in https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/I2S/HiFreq_ADC/HiFreq_ADC.ino + adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_11); // configure ADC input amplification + + #if defined(I2S_GRAB_ADC1_COMPLETELY) + // according to docs from espressif, the ADC needs to be started explicitly + // fingers crossed + err = i2s_adc_enable(I2S_NUM_0); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to enable i2s adc: %d\n", err); + //return; + } + #else + // bugfix: do not disable ADC initially - its already disabled after driver install. + //err = i2s_adc_disable(I2S_NUM_0); + // //err = i2s_stop(I2S_NUM_0); + //if (err != ESP_OK) { + // DEBUGSR_PRINTF("Failed to initially disable i2s adc: %d\n", err); + //} + #endif + + _initialized = true; + } + + + I2S_datatype postProcessSample(I2S_datatype sample_in) { + static I2S_datatype lastADCsample = 0; // last good sample + static unsigned int broken_samples_counter = 0; // number of consecutive broken (and fixed) ADC samples + I2S_datatype sample_out = 0; + + // bring sample down down to 16bit unsigned + I2S_unsigned_datatype rawData = * reinterpret_cast (&sample_in); // C++ acrobatics to get sample as "unsigned" + #ifndef I2S_USE_16BIT_SAMPLES + rawData = (rawData >> 16) & 0xFFFF; // scale input down from 32bit -> 16bit + I2S_datatype lastGoodSample = lastADCsample / 16384 ; // prepare "last good sample" accordingly (26bit-> 12bit with correct sign handling) + #else + rawData = rawData & 0xFFFF; // input is already in 16bit, just mask off possible junk + I2S_datatype lastGoodSample = lastADCsample * 4; // prepare "last good sample" accordingly (10bit-> 12bit) + #endif + + // decode ADC sample data fields + uint16_t the_channel = (rawData >> 12) & 0x000F; // upper 4 bit = ADC channel + uint16_t the_sample = rawData & 0x0FFF; // lower 12bit -> ADC sample (unsigned) + I2S_datatype finalSample = (int(the_sample) - 2048); // convert unsigned sample to signed (centered at 0); + + if ((the_channel != _myADCchannel) && (_myADCchannel != 0x0F)) { // 0x0F means "don't know what my channel is" + // fix bad sample + finalSample = lastGoodSample; // replace with last good ADC sample + broken_samples_counter ++; + if (broken_samples_counter > 256) _myADCchannel = 0x0F; // too many bad samples in a row -> disable sample corrections + //Serial.print("\n!ADC rogue sample 0x"); Serial.print(rawData, HEX); Serial.print("\tchannel:");Serial.println(the_channel); + } else broken_samples_counter = 0; // good sample - reset counter + + // back to original resolution + #ifndef I2S_USE_16BIT_SAMPLES + finalSample = finalSample << 16; // scale up from 16bit -> 32bit; + #endif + + finalSample = finalSample / 4; // mimic old analog driver behaviour (12bit -> 10bit) + sample_out = (3 * finalSample + lastADCsample) / 4; // apply low-pass filter (2-tap FIR) + //sample_out = (finalSample + lastADCsample) / 2; // apply stronger low-pass filter (2-tap FIR) + + lastADCsample = sample_out; // update ADC last sample + return(sample_out); + } + + + void getSamples(float *buffer, uint16_t num_samples) { + /* Enable ADC. This has to be enabled and disabled directly before and + * after sampling, otherwise Wifi dies + */ + if (_initialized) { + #if !defined(I2S_GRAB_ADC1_COMPLETELY) + // old code - works for me without enable/disable, at least on ESP32. + //esp_err_t err = i2s_start(I2S_NUM_0); + esp_err_t err = i2s_adc_enable(I2S_NUM_0); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to enable i2s adc: %d\n", err); + return; + } + #endif + + I2SSource::getSamples(buffer, num_samples); + + #if !defined(I2S_GRAB_ADC1_COMPLETELY) + // old code - works for me without enable/disable, at least on ESP32. + err = i2s_adc_disable(I2S_NUM_0); //i2s_adc_disable() may cause crash with IDF 4.4 (https://github.com/espressif/arduino-esp32/issues/6832) + //err = i2s_stop(I2S_NUM_0); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to disable i2s adc: %d\n", err); + return; + } + #endif + } + } + + void deinitialize() { + pinManager.deallocatePin(_audioPin, PinOwner::UM_Audioreactive); + _initialized = false; + _myADCchannel = 0x0F; + + esp_err_t err; + #if defined(I2S_GRAB_ADC1_COMPLETELY) + // according to docs from espressif, the ADC needs to be stopped explicitly + // fingers crossed + err = i2s_adc_disable(I2S_NUM_0); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to disable i2s adc: %d\n", err); + } + #endif + + i2s_stop(I2S_NUM_0); + err = i2s_driver_uninstall(I2S_NUM_0); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); + return; + } + } + + private: + int8_t _audioPin; + int8_t _myADCchannel = 0x0F; // current ADC channel for analog input. 0x0F means "undefined" +}; +#endif + +/* SPH0645 Microphone + This is an I2S microphone with some timing quirks that need + special consideration. +*/ + +// https://github.com/espressif/esp-idf/issues/7192 SPH0645 i2s microphone issue when migrate from legacy esp-idf version (IDFGH-5453) +// a user recommended this: Try to set .communication_format to I2S_COMM_FORMAT_STAND_I2S and call i2s_set_clk() after i2s_set_pin(). +class SPH0654 : public I2SSource { + public: + SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : + I2SSource(sampleRate, blockSize, sampleScale) + {} + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE) { + DEBUGSR_PRINTLN("SPH0654:: initialize();"); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin); +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) +// these registers are only existing in "classic" ESP32 + REG_SET_BIT(I2S_TIMING_REG(I2S_NUM_0), BIT(9)); + REG_SET_BIT(I2S_CONF_REG(I2S_NUM_0), I2S_RX_MSB_SHIFT); +#else + #warning FIX ME! Please. +#endif + } +}; +#endif \ No newline at end of file diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md new file mode 100644 index 0000000000..8959021ba7 --- /dev/null +++ b/usermods/audioreactive/readme.md @@ -0,0 +1,78 @@ +# Audioreactive usermod + +Enables controlling LEDs via audio input. Audio source can be a microphone or analog-in (AUX) using an appropriate adapter. +Supported microphones range from analog (MAX4466, MAX9814, ...) to digital (INMP441, ICS-43434, ...). + +Does audio processing and provides data structure that specially written effects can use. + +**does not** provide effects or draw anything to an LED strip/matrix. + +## Additional Documentation +This usermod is an evolution of [SR-WLED](https://github.com/atuline/WLED), and a lot of documentation and information can be found in the [SR-WLED wiki](https://github.com/atuline/WLED/wiki): +* [getting started with audio](https://github.com/atuline/WLED/wiki/First-Time-Setup#sound) +* [Sound settings](https://github.com/atuline/WLED/wiki/Sound-Settings) - similar to options on the usemod settings page in WLED. +* [Digital Audio](https://github.com/atuline/WLED/wiki/Digital-Microphone-Hookup) +* [Analog Audio](https://github.com/atuline/WLED/wiki/Analog-Audio-Input-Options) +* [UDP Sound sync](https://github.com/atuline/WLED/wiki/UDP-Sound-Sync) + + +## Supported MCUs +This audioreactive usermod works best on "classic ESP32" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support. + +It will compile successfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. + +Analog audio is only possible on "classic" ESP32, but not on other MCUs like ESP32-S3. + +Currently ESP8266 is not supported, due to low speed and small RAM of this chip. +There are however plans to create a lightweight audioreactive for the 8266, with reduced features. +## Installation + +### using customised _arduinoFFT_ library for use with this usermod +Add `-D USERMOD_AUDIOREACTIVE` to your PlatformIO environment `build_flags`, as well as `https://github.com/blazoncek/arduinoFFT.git` to your `lib_deps`. +If you are not using PlatformIO (which you should) try adding `#define USERMOD_AUDIOREACTIVE` to *my_config.h* and make sure you have _arduinoFFT_ library downloaded and installed. + +Customised _arduinoFFT_ library for use with this usermod can be found at https://github.com/blazoncek/arduinoFFT.git + +### using latest (develop) _arduinoFFT_ library +Alternatively, you can use the latest arduinoFFT development version. +ArduinoFFT `develop` library is slightly more accurate, and slightly faster than our customised library, however also needs additional 2kB RAM. + +* `build_flags` = `-D USERMOD_AUDIOREACTIVE` `-D UM_AUDIOREACTIVE_USE_NEW_FFT` +* `lib_deps`= `https://github.com/kosme/arduinoFFT#419d7b0` + +## Configuration + +All parameters are runtime configurable. Some may require a hard reset after changing them (I2S microphone or selected GPIOs). + +If you want to define default GPIOs during compile time, use the following (default values in parentheses): + +- `-D SR_DMTYPE=x` : defines digital microphone type: 0=analog, 1=generic I2S (default), 2=ES7243 I2S, 3=SPH0645 I2S, 4=generic I2S with master clock, 5=PDM I2S +- `-D AUDIOPIN=x` : GPIO for analog microphone/AUX-in (36) +- `-D I2S_SDPIN=x` : GPIO for SD pin on digital microphone (32) +- `-D I2S_WSPIN=x` : GPIO for WS pin on digital microphone (15) +- `-D I2S_CKPIN=x` : GPIO for SCK pin on digital microphone (14) +- `-D MCLK_PIN=x` : GPIO for master clock pin on digital Line-In boards (-1) +- `-D ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1) +- `-D ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1) + +Other options: + +- `-D UM_AUDIOREACTIVE_ENABLE` : makes usermod default enabled (not the same as include into build option!) +- `-D UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF` : disables rise/fall limiter default + +**NOTE** I2S is used for analog audio sampling. Hence, the analog *buttons* (i.e. potentiometers) are disabled when running this usermod with an analog microphone. + +### Advanced Compile-Time Options +You can use the following additional flags in your `build_flags` +* `-D SR_SQUELCH=x` : Default "squelch" setting (10) +* `-D SR_GAIN=x` : Default "gain" setting (60) +* `-D I2S_USE_RIGHT_CHANNEL`: Use RIGHT instead of LEFT channel (not recommended unless you strictly need this). +* `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM ressources (not recommended unless you absolutely need this). +* `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continuously sample analog ADC microphone. Only effective on ESP32. WARNING this _will_ cause conflicts(lock-up) with any analogRead() call. +* `-D MIC_LOGGER` : (debugging) Logs samples from the microphone to serial USB. Use with serial plotter (Arduino IDE) +* `-D SR_DEBUG` : (debugging) Additional error diagnostics and debug info on serial USB. + +## Release notes + +* 2022-06 Ported from [soundreactive WLED](https://github.com/atuline/WLED) - by @blazoncek (AKA Blaz Kristan) and the [SR-WLED team](https://github.com/atuline/WLED/wiki#sound-reactive-wled-fork-team). +* 2022-11 Updated to align with "[MoonModules/WLED](https://amg.wled.me)" audioreactive usermod - by @softhack007 (AKA Frank Möhle). diff --git a/usermods/battery_status_basic/assets/battery_info_screen.png b/usermods/battery_status_basic/assets/battery_info_screen.png deleted file mode 100644 index 50eb53465b..0000000000 Binary files a/usermods/battery_status_basic/assets/battery_info_screen.png and /dev/null differ diff --git a/usermods/battery_status_basic/readme.md b/usermods/battery_status_basic/readme.md deleted file mode 100644 index 276b23c19c..0000000000 --- a/usermods/battery_status_basic/readme.md +++ /dev/null @@ -1,69 +0,0 @@ -# :battery: Battery status/level Usermod :battery: - -This Usermod allows you to monitor the battery level of your battery powered project. - -You can see the battery level and voltage in the `info modal`. - -For this to work the positive side of the (18650) battery must be connected to pin `A0` of the d1mini/esp8266 with a 100k ohm resistor (see [Useful Links](#useful-links)). - -If you have a esp32 board it is best to connect the positive side of the battery to ADC1 (GPIO32 - GPIO39) - -

- -

- -## Installation - -define `USERMOD_BATTERY_STATUS_BASIC` in `my_config.h` - -### Basic wiring diagram -

- -

- -### Define Your Options - -* `USERMOD_BATTERY_STATUS_BASIC` - define this (in `my_config.h`) to have this user mod included wled00\usermods_list.cpp -* `USERMOD_BATTERY_MEASUREMENT_PIN` - defaults to A0 on esp8266 and GPIO32 on esp32 -* `USERMOD_BATTERY_MEASUREMENT_INTERVAL` - the frequency to check the battery, defaults to 30 seconds -* `USERMOD_BATTERY_MIN_VOLTAGE` - minimum voltage of the Battery used, default is 2.6 (18650 battery standard) -* `USERMOD_BATTERY_MAX_VOLTAGE` - maximum voltage of the Battery used, default is 4.2 (18650 battery standard) - -All parameters can be configured at runtime using Usermods settings page. - -## Important :warning: -* Make sure you know your battery specification ! not every battery is the same ! -* Example: - -| Your battery specification table | | Options you can define | -| :-------------------------------- |:--------------- | :---------------------------- | -| Capacity | 3500mAh 12,5 Wh | | -| Minimum capacity | 3350mAh 11,9 Wh | | -| Rated voltage | 3.6V - 3.7V | | -| **Charging end voltage** | **4,2V ± 0,05** | `USERMOD_BATTERY_MAX_VOLTAGE` | -| **Discharge voltage** | **2,5V** | `USERMOD_BATTERY_MIN_VOLTAGE` | -| Max. discharge current (constant) | 10A (10000mA) | | -| max. charging current | 1.7A (1700mA) | | -| ... | ... | ... | -| .. | .. | .. | - -Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6V - 3.7V](https://www.akkuteile.de/lithium-ionen-akkus/18650/molicel/molicel-inr18650-m35a-3500mah-10a-lithium-ionen-akku-3-6v-3-7v_100833) - -## Useful Links -* https://lazyzero.de/elektronik/esp8266/wemos_d1_mini_a0/start -* https://arduinodiy.wordpress.com/2016/12/25/monitoring-lipo-battery-voltage-with-wemos-d1-minibattery-shield-and-thingspeak/ - -## Change Log -2021-09-02 -* added "Battery voltage" to info -* added circuit diagram to readme -* added MQTT support, sending battery voltage -* minor fixes - -2021-08-15 -* changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries -* Updated readme, added specification table - -2021-08-10 -* Created - diff --git a/usermods/battery_status_basic/usermod_v2_battery_status_basic.h b/usermods/battery_status_basic/usermod_v2_battery_status_basic.h deleted file mode 100644 index cb3c0867ce..0000000000 --- a/usermods/battery_status_basic/usermod_v2_battery_status_basic.h +++ /dev/null @@ -1,398 +0,0 @@ -#pragma once - -#include "wled.h" - - - - -// pin defaults -// for the esp32 it is best to use the ADC1: GPIO32 - GPIO39 -// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html -#ifndef USERMOD_BATTERY_MEASUREMENT_PIN - #ifdef ARDUINO_ARCH_ESP32 - #define USERMOD_BATTERY_MEASUREMENT_PIN 32 - #else //ESP8266 boards - #define USERMOD_BATTERY_MEASUREMENT_PIN A0 - #endif -#endif - -// esp32 has a 12bit adc resolution -// esp8266 only 10bit -#ifndef USERMOD_BATTERY_ADC_PRECISION - #ifdef ARDUINO_ARCH_ESP32 - // 12 bits - #define USERMOD_BATTERY_ADC_PRECISION 4095.0f - #else - // 10 bits - #define USERMOD_BATTERY_ADC_PRECISION 1024.0f - #endif -#endif - - -// the frequency to check the battery, 30 sec -#ifndef USERMOD_BATTERY_MEASUREMENT_INTERVAL - #define USERMOD_BATTERY_MEASUREMENT_INTERVAL 30000 -#endif - - -// default for 18650 battery -// https://batterybro.com/blogs/18650-wholesale-battery-reviews/18852515-when-to-recycle-18650-batteries-and-how-to-start-a-collection-center-in-your-vape-shop -// Discharge voltage: 2.5 volt + .1 for personal safety -#ifndef USERMOD_BATTERY_MIN_VOLTAGE - #define USERMOD_BATTERY_MIN_VOLTAGE 2.6f -#endif - -#ifndef USERMOD_BATTERY_MAX_VOLTAGE - #define USERMOD_BATTERY_MAX_VOLTAGE 4.2f -#endif - -class UsermodBatteryBasic : public Usermod -{ - private: - // battery pin can be defined in my_config.h - int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; - // how often to read the battery voltage - unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; - unsigned long nextReadTime = 0; - unsigned long lastReadTime = 0; - // battery min. voltage - float minBatteryVoltage = USERMOD_BATTERY_MIN_VOLTAGE; - // battery max. voltage - float maxBatteryVoltage = USERMOD_BATTERY_MAX_VOLTAGE; - // 0 - 1024 for esp8266 (10-bit resolution) - // 0 - 4095 for esp32 (Default is 12-bit resolution) - float adcPrecision = USERMOD_BATTERY_ADC_PRECISION; - // raw analog reading - float rawValue = 0.0; - // calculated voltage - float voltage = 0.0; - // mapped battery level based on voltage - long batteryLevel = 0; - bool initDone = false; - bool initializing = true; - - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _readInterval[]; - - - // custom map function - // https://forum.arduino.cc/t/floating-point-using-map-function/348113/2 - double mapf(double x, double in_min, double in_max, double out_min, double out_max) - { - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; - } - - float truncate(float val, byte dec) - { - float x = val * pow(10, dec); - float y = round(x); - float z = x - y; - if ((int)z == 5) - { - y++; - } - x = y / pow(10, dec); - return x; - } - - - - public: - //Functions called by WLED - - /* - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - */ - void setup() - { - #ifdef ARDUINO_ARCH_ESP32 - DEBUG_PRINTLN(F("Allocating battery pin...")); - if (batteryPin >= 0 && pinManager.allocatePin(batteryPin, false)) - { - DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); - } else { - if (batteryPin >= 0) DEBUG_PRINTLN(F("Battery pin allocation failed.")); - batteryPin = -1; // allocation failed - } - #else //ESP8266 boards have only one analog input pin A0 - - pinMode(batteryPin, INPUT); - #endif - - nextReadTime = millis() + readingInterval; - lastReadTime = millis(); - - initDone = true; - } - - - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() - { - //Serial.println("Connected to WiFi!"); - } - - - /* - * loop() is called continuously. Here you can check for events, read sensors, etc. - * - */ - void loop() - { - if(strip.isUpdating()) return; - - // check the battery level every USERMOD_BATTERY_MEASUREMENT_INTERVAL (ms) - if (millis() < nextReadTime) return; - - - nextReadTime = millis() + readingInterval; - lastReadTime = millis(); - initializing = false; - - // read battery raw input - rawValue = analogRead(batteryPin); - - // calculate the voltage - voltage = (rawValue / adcPrecision) * maxBatteryVoltage ; - // check if voltage is within specified voltage range - voltage = voltagemaxBatteryVoltage?-1.0f:voltage; - - // translate battery voltage into percentage - /* - the standard "map" function doesn't work - https://www.arduino.cc/reference/en/language/functions/math/map/ notes and warnings at the bottom - */ - batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); - - - // SmartHome stuff - if (WLED_MQTT_CONNECTED) { - char subuf[64]; - strcpy(subuf, mqttDeviceTopic); - strcat_P(subuf, PSTR("/voltage")); - mqtt->publish(subuf, 0, false, String(voltage).c_str()); - } - - } - - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - void addToJsonInfo(JsonObject& root) - { - - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - // info modal display names - JsonArray batteryPercentage = user.createNestedArray("Battery level"); - JsonArray batteryVoltage = user.createNestedArray("Battery voltage"); - - if (initializing) { - batteryPercentage.add((nextReadTime - millis()) / 1000); - batteryPercentage.add(" sec"); - batteryVoltage.add((nextReadTime - millis()) / 1000); - batteryVoltage.add(" sec"); - return; - } - - if(batteryLevel < 0) { - batteryPercentage.add(F("invalid")); - } else { - batteryPercentage.add(batteryLevel); - } - batteryPercentage.add(F(" %")); - - if(voltage < 0) { - batteryVoltage.add(F("invalid")); - } else { - batteryVoltage.add(truncate(voltage, 2)); - } - batteryVoltage.add(F(" V")); - } - - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - /* - void addToJsonState(JsonObject& root) - { - - } - */ - - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - /* - void readFromJsonState(JsonObject& root) - { - } - */ - - - /* - * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. - * It will be called by WLED when settings are actually saved (for example, LED settings are saved) - * If you want to force saving the current state, use serializeConfig() in your loop(). - * - * CAUTION: serializeConfig() will initiate a filesystem write operation. - * It might cause the LEDs to stutter and will cause flash wear if called too often. - * Use it sparingly and always in the loop, never in network callbacks! - * - * addToConfig() will make your settings editable through the Usermod Settings page automatically. - * - * Usermod Settings Overview: - * - Numeric values are treated as floats in the browser. - * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float - * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and - * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. - * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. - * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a - * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. - * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type - * used in the Usermod when reading the value from ArduinoJson. - * - Pin values can be treated differently from an integer value by using the key name "pin" - * - "pin" can contain a single or array of integer values - * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins - * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) - * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used - * - * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings - * - * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. - * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. - * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED - * - * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! - */ - void addToConfig(JsonObject& root) - { - // created JSON object: - /* - { - "Battery-Level": { - "pin": "A0", <--- only when using esp32 boards - "minBatteryVoltage": 2.6, - "maxBatteryVoltage": 4.2, - "read-interval-ms": 30000 - } - } - */ - JsonObject battery = root.createNestedObject(FPSTR(_name)); // usermodname - #ifdef ARDUINO_ARCH_ESP32 - battery["pin"] = batteryPin; // usermodparam - #endif - battery["minBatteryVoltage"] = minBatteryVoltage; // usermodparam - battery["maxBatteryVoltage"] = maxBatteryVoltage; // usermodparam - battery[FPSTR(_readInterval)] = readingInterval; - - DEBUG_PRINTLN(F("Battery config saved.")); - } - - - /* - * readFromConfig() can be used to read back the custom settings you added with addToConfig(). - * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) - * - * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), - * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. - * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) - * - * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) - * - * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present - * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them - * - * This function is guaranteed to be called on boot, but could also be called every time settings are updated - */ - bool readFromConfig(JsonObject& root) - { - // looking for JSON object: - /* - { - "BatteryLevel": { - "pin": "A0", <--- only when using esp32 boards - "minBatteryVoltage": 2.6, - "maxBatteryVoltage": 4.2, - "read-interval-ms": 30000 - } - } - */ - #ifdef ARDUINO_ARCH_ESP32 - int8_t newBatteryPin = batteryPin; - #endif - - JsonObject battery = root[FPSTR(_name)]; - if (battery.isNull()) - { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - - #ifdef ARDUINO_ARCH_ESP32 - newBatteryPin = battery["pin"] | newBatteryPin; - #endif - minBatteryVoltage = battery["minBatteryVoltage"] | minBatteryVoltage; - //minBatteryVoltage = min(12.0f, (int)readingInterval); - maxBatteryVoltage = battery["maxBatteryVoltage"] | maxBatteryVoltage; - //maxBatteryVoltage = min(14.4f, max(3.3f,(int)readingInterval)); - readingInterval = battery["read-interval-ms"] | readingInterval; - readingInterval = max(3000, (int)readingInterval); // minimum repetition is >5000ms (5s) - - DEBUG_PRINT(FPSTR(_name)); - - #ifdef ARDUINO_ARCH_ESP32 - if (!initDone) - { - // first run: reading from cfg.json - newBatteryPin = batteryPin; - DEBUG_PRINTLN(F(" config loaded.")); - } - else - { - DEBUG_PRINTLN(F(" config (re)loaded.")); - - // changing paramters from settings page - if (newBatteryPin != batteryPin) - { - // deallocate pin - pinManager.deallocatePin(batteryPin); - batteryPin = newBatteryPin; - // initialise - setup(); - } - } - #endif - - return !battery[FPSTR(_readInterval)].isNull(); - } - - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_BATTERY_STATUS_BASIC; - } -}; - -// strings to reduce flash memory usage (used more than twice) -const char UsermodBatteryBasic::_name[] PROGMEM = "Battery-level"; -const char UsermodBatteryBasic::_readInterval[] PROGMEM = "read-interval-ms"; \ No newline at end of file diff --git a/usermods/blynk_relay_control/README.md b/usermods/blynk_relay_control/README.md deleted file mode 100644 index b6494b46e7..0000000000 --- a/usermods/blynk_relay_control/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Blynk controllable relay -This usermod allows controlling a relay state from the user variables. It also allows the user variables to be set over Blynk. - -Optionally, the servo can have a reset timer to go back to it's default state after an interval. This interval is set through userVar1. - -## Instalation - -Replace the WLED06_usermod.ino file in Aircoookies WLED folder with the one here. - -## Customizations - -Update the following parameters in WLED06_usermod.ino to configure the mod's behavior: - -```cpp -//Which pin is the relay connected to -#define RELAY_PIN 5 -//Which pin state should the relay default to -#define RELAY_PIN_DEFAULT LOW -//If >0 The controller returns to RELAY_PIN_DEFAULT after this time in milliseconds -#define RELAY_PIN_TIMER_DEFAULT 3000 - -//Blynk virtual pin for controlling relay -#define BLYNK_USER_VAR0_PIN V9 -//Blynk virtual pin for controlling relay timer -#define BLYNK_USER_VAR1_PIN V10 -//Number of milliseconds between updating blynk -#define BLYNK_RELAY_UPDATE_INTERVAL 5000 -``` diff --git a/usermods/blynk_relay_control/wled06_usermod.ino b/usermods/blynk_relay_control/wled06_usermod.ino deleted file mode 100644 index d4028ea5da..0000000000 --- a/usermods/blynk_relay_control/wled06_usermod.ino +++ /dev/null @@ -1,96 +0,0 @@ -/* - * This file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality - * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in wled_eeprom.h) - * bytes 2400+ are currently ununsed, but might be used for future wled features - */ - -//Use userVar0 (API calls &U0=, uint16_t) to set relay state -#define relayPinState userVar0 -//Use userVar1 (API calls &U1=, uint16_t) to set relay timer duration -//Ignored if 0, otherwise number of milliseconds to allow relay to stay in -//non default state. -#define relayTimerInterval userVar1 - -//Which pin is the relay connected to -#define RELAY_PIN 5 -//Which pin state should the relay default to -#define RELAY_PIN_DEFAULT LOW -//If >0 The controller returns to RELAY_PIN_DEFAULT after this time in milliseconds -#define RELAY_PIN_TIMER_DEFAULT 3000 - -//Blynk virtual pin for controlling relay -#define BLYNK_USER_VAR0_PIN V9 -//Blynk virtual pin for controlling relay timer -#define BLYNK_USER_VAR1_PIN V10 -//Number of milliseconds between updating blynk -#define BLYNK_RELAY_UPDATE_INTERVAL 5000 - -//Is the timer for resetting the relay active -bool relayTimerStarted = false; -//millis() time after which relay will be reset -unsigned long relayTimeToDefault = 0; -//millis() time after which relay vars in Blynk will be sent -unsigned long relayBlynkUpdateTime = 0; - -//gets called once at boot. Do all initialization that doesn't depend on network here -void userSetup() -{ - relayPinState = RELAY_PIN_DEFAULT; - relayTimerInterval = RELAY_PIN_TIMER_DEFAULT; - pinMode(RELAY_PIN, OUTPUT); - digitalWrite(RELAY_PIN, relayPinState); -} - -//gets called every time WiFi is (re-)connected. Initialize own network interfaces here -void userConnected() -{ -} - -//loop. You can use "if (WLED_CONNECTED)" to check for successful connection -void userLoop() -{ - //Normalize relayPinState to an accepted value - if (relayPinState != HIGH && relayPinState != LOW) { - relayPinState = RELAY_PIN_DEFAULT; - } - //If relay changes and relayTimerInterval is set, start a timer to change back - if (relayTimerInterval != 0 && - relayPinState != RELAY_PIN_DEFAULT && - !relayTimerStarted ) { - relayTimerStarted = true; - relayTimeToDefault = millis() + relayTimerInterval; - } - //If manually changed back to default, cancel timer - if (relayTimerStarted && relayPinState == RELAY_PIN_DEFAULT ) { - relayTimerStarted = false; - } - //If timer completes, set relay back to default - if (relayTimerStarted && millis() > relayTimeToDefault) { - relayPinState = RELAY_PIN_DEFAULT; - relayTimerStarted = false; - } - digitalWrite(RELAY_PIN, relayPinState); - updateRelayBlynk(); -} - -//Update Blynk with state of userVars at BLYNK_RELAY_UPDATE_INTERVAL -void updateRelayBlynk() -{ - if (!WLED_CONNECTED) return; - if (relayBlynkUpdateTime > millis()) return; - Blynk.virtualWrite(BLYNK_USER_VAR0_PIN, userVar0); - Blynk.virtualWrite(BLYNK_USER_VAR1_PIN, userVar1); - relayBlynkUpdateTime = millis() + BLYNK_RELAY_UPDATE_INTERVAL; -} - -//Add Blynk callback for setting userVar0 -BLYNK_WRITE(BLYNK_USER_VAR0_PIN) -{ - userVar0 = param.asInt(); -} -//Add Blynk callback for setting userVar1 -BLYNK_WRITE(BLYNK_USER_VAR1_PIN) -{ - userVar1 = param.asInt(); -} diff --git a/usermods/boblight/boblight.h b/usermods/boblight/boblight.h new file mode 100644 index 0000000000..a1e2577586 --- /dev/null +++ b/usermods/boblight/boblight.h @@ -0,0 +1,459 @@ +#pragma once + +#include "wled.h" + +/* + * Usermod that implements BobLight "ambilight" protocol + * + * See the accompanying README.md file for more info. + */ + +#ifndef BOB_PORT + #define BOB_PORT 19333 // Default boblightd port +#endif + +class BobLightUsermod : public Usermod { + typedef struct _LIGHT { + char lightname[5]; + float hscan[2]; + float vscan[2]; + } light_t; + + private: + unsigned long lastTime = 0; + bool enabled = false; + bool initDone = false; + + light_t *lights = nullptr; + uint16_t numLights = 0; // 16 + 9 + 16 + 9 + uint16_t top, bottom, left, right; // will be filled in readFromConfig() + uint16_t pct; + + WiFiClient bobClient; + WiFiServer *bob; + uint16_t bobPort = BOB_PORT; + + static const char _name[]; + static const char _enabled[]; + + /* + # boblight + # Copyright (C) Bob 2009 + # + # makeboblight.sh created by Adam Boeglin + # + # boblight is free software: you can redistribute it and/or modify it + # under the terms of the GNU General Public License as published by the + # Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # boblight is distributed in the hope that it will be useful, but + # WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + # See the GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License along + # with this program. If not, see . + */ + + // fills the lights[] array with position & depth of scan for each LED + void fillBobLights(int bottom, int left, int top, int right, float pct_scan) { + + int lightcount = 0; + int total = top+left+right+bottom; + int bcount; + + if (total > strip.getLengthTotal()) { + DEBUG_PRINTLN(F("BobLight: Too many lights.")); + return; + } + + // start left part of bottom strip (clockwise direction, 1st half) + if (bottom > 0) { + bcount = 1; + float brange = 100.0/bottom; + float bcurrent = 50.0; + if (bottom < top) { + int diff = top - bottom; + brange = 100.0/top; + bcurrent -= (diff/2)*brange; + } + while (bcount <= bottom/2) { + float btop = bcurrent - brange; + String name = "b"+String(bcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = btop; + lights[lightcount].hscan[1] = bcurrent; + lights[lightcount].vscan[0] = 100 - pct_scan; + lights[lightcount].vscan[1] = 100; + lightcount+=1; + bcurrent = btop; + bcount+=1; + } + } + + // left side + if (left > 0) { + int lcount = 1; + float lrange = 100.0/left; + float lcurrent = 100.0; + while (lcount <= left) { + float ltop = lcurrent - lrange; + String name = "l"+String(lcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = 0; + lights[lightcount].hscan[1] = pct_scan; + lights[lightcount].vscan[0] = ltop; + lights[lightcount].vscan[1] = lcurrent; + lightcount+=1; + lcurrent = ltop; + lcount+=1; + } + } + + // top side + if (top > 0) { + int tcount = 1; + float trange = 100.0/top; + float tcurrent = 0; + while (tcount <= top) { + float ttop = tcurrent + trange; + String name = "t"+String(tcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = tcurrent; + lights[lightcount].hscan[1] = ttop; + lights[lightcount].vscan[0] = 0; + lights[lightcount].vscan[1] = pct_scan; + lightcount+=1; + tcurrent = ttop; + tcount+=1; + } + } + + // right side + if (right > 0) { + int rcount = 1; + float rrange = 100.0/right; + float rcurrent = 0; + while (rcount <= right) { + float rtop = rcurrent + rrange; + String name = "r"+String(rcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = 100-pct_scan; + lights[lightcount].hscan[1] = 100; + lights[lightcount].vscan[0] = rcurrent; + lights[lightcount].vscan[1] = rtop; + lightcount+=1; + rcurrent = rtop; + rcount+=1; + } + } + + // right side of bottom strip (2nd half) + if (bottom > 0) { + float brange = 100.0/bottom; + float bcurrent = 100; + if (bottom < top) { + brange = 100.0/top; + } + while (bcount <= bottom) { + float btop = bcurrent - brange; + String name = "b"+String(bcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = btop; + lights[lightcount].hscan[1] = bcurrent; + lights[lightcount].vscan[0] = 100 - pct_scan; + lights[lightcount].vscan[1] = 100; + lightcount+=1; + bcurrent = btop; + bcount+=1; + } + } + + numLights = lightcount; + + #if WLED_DEBUG + DEBUG_PRINTLN(F("Fill light data: ")); + DEBUG_PRINTF(" lights %d\n", numLights); + for (int i=0; i strip.getLengthTotal() ) { + DEBUG_PRINTLN(F("BobLight: Too many lights.")); + DEBUG_PRINTF("%d+%d+%d+%d>%d\n", bottom, left, top, right, strip.getLengthTotal()); + totalLights = strip.getLengthTotal(); + top = bottom = (uint16_t) roundf((float)totalLights * 16.0f / 50.0f); + left = right = (uint16_t) roundf((float)totalLights * 9.0f / 50.0f); + } + lights = new light_t[totalLights]; + if (lights) fillBobLights(bottom, left, top, right, float(pct)); // will fill numLights + else enable(false); + initDone = true; + } + + void connected() { + // we can only start server when WiFi is connected + if (!bob) bob = new WiFiServer(bobPort, 1); + bob->begin(); + bob->setNoDelay(true); + } + + void loop() { + if (!enabled || strip.isUpdating()) return; + if (millis() - lastTime > 10) { + lastTime = millis(); + pollBob(); + } + } + + void enable(bool en) { enabled = en; } + +#ifndef WLED_DISABLE_MQTT + /** + * handling of MQTT message + * topic only contains stripped topic (part after /wled/MAC) + * topic should look like: /swipe with amessage of [up|down] + */ + bool onMqttMessage(char* topic, char* payload) { + //if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/subtopic"), 6) == 0) { + // String action = payload; + // if (action == "on") { + // enable(true); + // return true; + // } else if (action == "off") { + // enable(false); + // return true; + // } + //} + return false; + } + + /** + * subscribe to MQTT topic for controlling usermod + */ + void onMqttConnect(bool sessionPresent) { + //char subuf[64]; + //if (mqttDeviceTopic[0] != 0) { + // strcpy(subuf, mqttDeviceTopic); + // strcat_P(subuf, PSTR("/subtopic")); + // mqtt->subscribe(subuf, 0); + //} + } +#endif + + void addToJsonInfo(JsonObject& root) + { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + String uiDomString = F(""); + infoArr.add(uiDomString); + } + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) + { + } + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) { + if (!initDone) return; // prevent crash on boot applyPreset() + bool en = enabled; + JsonObject um = root[FPSTR(_name)]; + if (!um.isNull()) { + if (um[FPSTR(_enabled)].is()) { + en = um[FPSTR(_enabled)].as(); + } else { + String str = um[FPSTR(_enabled)]; // checkbox -> off or on + en = (bool)(str!="off"); // off is guaranteed to be present + } + if (en != enabled && lights) { + enable(en); + if (!enabled && bob && bob->hasClient()) { + if (bobClient) bobClient.stop(); + bobClient = bob->available(); + BobClear(); + exitRealtime(); + } + } + } + } + + void appendConfigData() { + //oappend(SET_F("dd=addDropdown('usermod','selectfield');")); + //oappend(SET_F("addOption(dd,'1st value',0);")); + //oappend(SET_F("addOption(dd,'2nd value',1);")); + oappend(SET_F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field + } + + void addToConfig(JsonObject& root) { + JsonObject umData = root.createNestedObject(FPSTR(_name)); + umData[FPSTR(_enabled)] = enabled; + umData[F("port")] = bobPort; + umData[F("top")] = top; + umData[F("bottom")] = bottom; + umData[F("left")] = left; + umData[F("right")] = right; + umData[F("pct")] = pct; + } + + bool readFromConfig(JsonObject& root) { + JsonObject umData = root[FPSTR(_name)]; + bool configComplete = !umData.isNull(); + + bool en = enabled; + configComplete &= getJsonValue(umData[FPSTR(_enabled)], en); + enable(en); + + configComplete &= getJsonValue(umData[F("port")], bobPort); + configComplete &= getJsonValue(umData[F("bottom")], bottom, 16); + configComplete &= getJsonValue(umData[F("top")], top, 16); + configComplete &= getJsonValue(umData[F("left")], left, 9); + configComplete &= getJsonValue(umData[F("right")], right, 9); + configComplete &= getJsonValue(umData[F("pct")], pct, 5); // Depth of scan [%] + pct = MIN(50,MAX(1,pct)); + + uint16_t totalLights = bottom + left + top + right; + if (initDone && numLights != totalLights) { + if (lights) delete[] lights; + setup(); + } + return configComplete; + } + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() { + //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + } + + uint16_t getId() { return USERMOD_ID_BOBLIGHT; } + +}; + +// strings to reduce flash memory usage (used more than twice) +const char BobLightUsermod::_name[] PROGMEM = "BobLight"; +const char BobLightUsermod::_enabled[] PROGMEM = "enabled"; + +// main boblight handling (definition here prevents inlining) +void BobLightUsermod::pollBob() { + + //check if there are any new clients + if (bob && bob->hasClient()) { + //find free/disconnected spot + if (!bobClient || !bobClient.connected()) { + if (bobClient) bobClient.stop(); + bobClient = bob->available(); + DEBUG_PRINTLN(F("Boblight: Client connected.")); + } + //no free/disconnected spot so reject + WiFiClient bobClientTmp = bob->available(); + bobClientTmp.stop(); + BobClear(); + exitRealtime(); + } + + //check clients for data + if (bobClient && bobClient.connected()) { + realtimeLock(realtimeTimeoutMs); // lock strip as we have a client connected + + //get data from the client + while (bobClient.available()) { + String input = bobClient.readStringUntil('\n'); + // DEBUG_PRINT("Client: "); DEBUG_PRINTLN(input); // may be to stressful on Serial + if (input.startsWith(F("hello"))) { + DEBUG_PRINTLN(F("hello")); + bobClient.print(F("hello\n")); + } else if (input.startsWith(F("ping"))) { + DEBUG_PRINTLN(F("ping 1")); + bobClient.print(F("ping 1\n")); + } else if (input.startsWith(F("get version"))) { + DEBUG_PRINTLN(F("version 5")); + bobClient.print(F("version 5\n")); + } else if (input.startsWith(F("get lights"))) { + char tmp[64]; + String answer = ""; + sprintf_P(tmp, PSTR("lights %d\n"), numLights); + DEBUG_PRINT(tmp); + answer.concat(tmp); + for (int i=0; i ... + input.remove(0,10); + String tmp = input.substring(0,input.indexOf(' ')); + + int light_id = -1; + for (uint16_t i=0; iavailable(); + BobClear(); + } + } + } +} diff --git a/usermods/boblight/readme.md b/usermods/boblight/readme.md new file mode 100644 index 0000000000..3458300933 --- /dev/null +++ b/usermods/boblight/readme.md @@ -0,0 +1,37 @@ +# BobLight usermod + +This usermod allows displaying BobLight ambilight protocol on WLED device with a limited command set (not a full implementation). +BobLight protocol uses a TCP connection which guarantees packet delivery at the possible expense of latency delays. It is not very efficient (as it uses plaintext comands) so is not suited for large number of LEDs. + +This implementation is intended for TV backlight in combination with XBMC/Kodi BobLight add-on. + +The LEDs can be configured in usermod settings page. The configuration is simple: you enter the number of LED pixels on each side of your TV (top, right, bottom, left). +The LEDs should be wired in a clockwise orientation starting in the middle of bottom side (left half of bottom leds is where the string should start). + +``` ++-------->-------+ +| | +^ v +| | ++---<--+ ---<---+ + ^ + start +``` + +## Installation + +Add `-D USERMOD_BOBLIGHT` to your PlatformIO environment. +If you are not using PlatformIO (which you should) try adding `#define USERMOD_BOBLIGHT` to *my_config.h*. + +## Configuration + +All parameters are runtime configurable though changing port may require reboot. + +If you want to define default port during compile time use the following (default values in parentheses): + +- `BOB_PORT=x` : defines default TCP port for usermod to listen on (19333) + + +## Release notes + +2022-11 Initial implementation by @blazoncek (AKA Blaz Kristan) diff --git a/usermods/mpu6050_imu/readme.md b/usermods/mpu6050_imu/readme.md index adb19ef8eb..4120041517 100644 --- a/usermods/mpu6050_imu/readme.md +++ b/usermods/mpu6050_imu/readme.md @@ -1,13 +1,13 @@ # MPU-6050 Six-Axis (Gyro + Accelerometer) Driver -This usermod-v2 modification allows the connection of a MPU-6050 IMU sensor to -allow for effects that are controlled by the orientation or motion of the WLED Device. +v2 of this usermod enables connection of a MPU-6050 IMU sensor to +work with effects controlled by the orientation or motion of the WLED Device. -The MPU6050 has a built in "Digital Motion Processor" which does a lot of the heavy -lifting in integrating the gyro and accel measurements to get potentially more +The MPU6050 has a built in "Digital Motion Processor" which does the "heavy lifting" +integrating the gyro and accelerometer measurements to get potentially more useful gravity vector and orientation output. -It is pretty straightforward to comment out some of the variables being read off the device if they're not needed to save CPU/Mem/Bandwidth. +It is fairly straightforward to comment out variables being read from the device if they're not needed. Saves CPU/Memory/Bandwidth. _Story:_ @@ -36,7 +36,7 @@ lib_deps = AsyncTCP@1.0.3 Esp Async WebServer@1.2.0 IRremoteESP8266@2.7.3 - I2Cdevlib-MPU6050@fbde122cc5 + jrowberg/I2Cdevlib-MPU6050@^1.0.0 ``` ## Wiring @@ -78,7 +78,7 @@ to the info object ## Usermod installation 1. Copy the file `usermod_mpu6050_imu.h` to the `wled00` directory. -2. Register the usermod by adding `#include "usermod_mpu6050_imu.h.h"` in the top and `registerUsermod(new MPU6050Driver());` in the bottom of `usermods_list.cpp`. +2. Register the usermod by adding `#include "usermod_mpu6050_imu.h"` in the top and `registerUsermod(new MPU6050Driver());` in the bottom of `usermods_list.cpp`. Example **usermods_list.cpp**: diff --git a/usermods/mpu6050_imu/usermod_mpu6050_imu.h b/usermods/mpu6050_imu/usermod_mpu6050_imu.h index 4aa2a128fb..748ddf1a67 100644 --- a/usermods/mpu6050_imu/usermod_mpu6050_imu.h +++ b/usermods/mpu6050_imu/usermod_mpu6050_imu.h @@ -42,14 +42,6 @@ #include "Wire.h" #endif -#ifdef ARDUINO_ARCH_ESP32 - #define HW_PIN_SCL 22 - #define HW_PIN_SDA 21 -#else - #define HW_PIN_SCL 5 - #define HW_PIN_SDA 4 -#endif - // ================================================================ // === INTERRUPT DETECTION ROUTINE === // ================================================================ @@ -93,12 +85,9 @@ class MPU6050Driver : public Usermod { * setup() is called once at boot. WiFi is not yet connected at this point. */ void setup() { - PinManagerPinType pins[2] = { { HW_PIN_SCL, true }, { HW_PIN_SDA, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { enabled = false; return; } - // join I2C bus (I2Cdev library doesn't do this automatically) + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE - Wire.begin(); - Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties + Wire.setClock(400000U); // 400kHz I2C clock. Comment this line if having compilation difficulties #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE Fastwire::setup(400, true); #endif @@ -146,7 +135,7 @@ class MPU6050Driver : public Usermod { // (if it's going to break, usually the code will be 1) DEBUG_PRINT(F("DMP Initialization failed (code ")); DEBUG_PRINT(devStatus); - DEBUG_PRINTLN(F(")")); + DEBUG_PRINTLN(")"); } } @@ -217,7 +206,7 @@ class MPU6050Driver : public Usermod { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); - JsonArray imu_meas = user.createNestedObject("IMU"); + JsonObject imu_meas = user.createNestedObject("IMU"); JsonArray quat_json = imu_meas.createNestedArray("Quat"); quat_json.add(qat.w); quat_json.add(qat.x); @@ -258,20 +247,20 @@ class MPU6050Driver : public Usermod { * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - void addToJsonState(JsonObject& root) - { + //void addToJsonState(JsonObject& root) + //{ //root["user0"] = userVar0; - } + //} /* * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - void readFromJsonState(JsonObject& root) - { + //void readFromJsonState(JsonObject& root) + //{ //if (root["bri"] == 255) DEBUG_PRINTLN(F("Don't burn down your garage!")); - } + //} /* @@ -279,13 +268,13 @@ class MPU6050Driver : public Usermod { * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ - void addToConfig(JsonObject& root) - { - JsonObject top = root.createNestedObject("MPU6050_IMU"); - JsonArray pins = top.createNestedArray("pin"); - pins.add(HW_PIN_SCL); - pins.add(HW_PIN_SDA); - } +// void addToConfig(JsonObject& root) +// { +// JsonObject top = root.createNestedObject("MPU6050_IMU"); +// JsonArray pins = top.createNestedArray("pin"); +// pins.add(HW_PIN_SCL); +// pins.add(HW_PIN_SDA); +// } /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). @@ -295,4 +284,4 @@ class MPU6050Driver : public Usermod { return USERMOD_ID_IMU; } -}; \ No newline at end of file +}; diff --git a/usermods/mqtt_switch_v2/README.md b/usermods/mqtt_switch_v2/README.md index 148e4a5643..744d7fe3c3 100644 --- a/usermods/mqtt_switch_v2/README.md +++ b/usermods/mqtt_switch_v2/README.md @@ -50,5 +50,5 @@ This usermod listens on `[mqttDeviceTopic]/switch/0/set` (where 0 is replaced wi Feedback about the current state is provided at `[mqttDeviceTopic]/switch/0/state`. ### Home Assistant auto-discovery -Auto-discovery information is automatically published and you shoudn't have to do anything to register the switches in Home Assistant. +Auto-discovery information is automatically published and you shouldn't have to do anything to register the switches in Home Assistant. diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md index 2d933cdabe..1f3bec7a63 100644 --- a/usermods/multi_relay/readme.md +++ b/usermods/multi_relay/readme.md @@ -1,37 +1,40 @@ # Multi Relay -This usermod-v2 modification allows the connection of multiple relays each with individual delay and on/off mode. +This usermod-v2 modification allows the connection of multiple relays, each with individual delay and on/off mode. +Usermod supports PCF8574 I2C port expander to reduce GPIO use. +PCF8574 supports 8 outputs and each output corresponds to a relay in WLED (relay 0 = port 0, etc). I you are using more than 8 relays with multiple PCF8574 make sure their addresses are set in sequence (e.g. 0x20 and 0x21). You can set address of first expander in settings. +(**NOTE:** Will require Wire library and global I2C pins defined.) ## HTTP API -All responses are returned as JSON. +All responses are returned in JSON format. * Status Request: `http://[device-ip]/relays` * Switch Command: `http://[device-ip]/relays?switch=1,0,1,1` -The number of numbers behind the switch parameter must correspond to the number of relays. The number 1 switches the relay on. The number 0 switches the relay off. +The number of values behind the switch parameter must correspond to the number of relays. The value 1 switches the relay on, 0 switches it off. * Toggle Command: `http://[device-ip]/relays?toggle=1,0,1,1` -The number of numbers behind the parameter switch must correspond to the number of relays. The number 1 causes a toggling of the relay. The number 0 leaves the state of the device. +The number of values behind the parameter switch must correspond to the number of relays. The value 1 causes the relay to toggle, 0 leaves its state unchanged. -Examples +Examples: 1. total of 4 relays, relay 2 will be toggled: `http://[device-ip]/relays?toggle=0,1,0,0` 2. total of 3 relays, relay 1&3 will be switched on: `http://[device-ip]/relays?switch=1,0,1` ## JSON API -You can switch relay state using the following JSON object transmitted to: `http://[device-ip]/json` - +You can toggle the relay state by sending the following JSON object to: `http://[device-ip]/json` Switch relay 0 on: `{"MultiRelay":{"relay":0,"on":true}}` -Switch relay4 3 & 4 off: `{"MultiRelay":[{"relay":2,"on":false},{"relay":3,"on":false}]}` +Switch relay 3 and 4 off: `{"MultiRelay":[{"relay":2,"on":false},{"relay":3,"on":false}]}` + ## MQTT API * `wled`/_deviceMAC_/`relay`/`0`/`command` `on`|`off`|`toggle` * `wled`/_deviceMAC_/`relay`/`1`/`command` `on`|`off`|`toggle` -When relay is switched it will publish a message: +When a relay is switched, a message is published: * `wled`/_deviceMAC_/`relay`/`0` `on`|`off` @@ -42,7 +45,7 @@ When relay is switched it will publish a message: or 2. Use `#define USERMOD_MULTI_RELAY` in wled.h or `-D USERMOD_MULTI_RELAY` in your platformio.ini -You can override the default maximum number (4) of relays by defining MULTI_RELAY_MAX_RELAYS. +You can override the default maximum number of relays (which is 4) by defining MULTI_RELAY_MAX_RELAYS. Example **usermods_list.cpp**: @@ -78,13 +81,17 @@ void registerUsermods() ## Configuration -Usermod can be configured in Usermods settings page. +Usermod can be configured via the Usermods settings page. * `enabled` - enable/disable usermod -* `pin` - GPIO pin where relay is attached to ESP +* `use-PCF8574` - use PCF8574 port expander instead of GPIO pins +* `first-PCF8574` - I2C address of first expander (WARNING: enter *decimal* value) +* `broadcast`- time in seconds between MQTT relay-state broadcasts +* `HA-discovery`- enable Home Assistant auto discovery +* `pin` - ESP GPIO pin the relay is connected to (can be configured at compile time `-D MULTI_RELAY_PINS=xx,xx,...`) * `delay-s` - delay in seconds after on/off command is received -* `active-high` - toggle high/low activation of relay (can be used to reverse relay states) -* `external` - if enabled WLED does not control relay, it can only be triggered by external command (MQTT, HTTP, JSON or button) +* `active-high` - assign high/low activation of relay (can be used to reverse relay states) +* `external` - if enabled, WLED does not control relay, it can only be triggered by an external command (MQTT, HTTP, JSON or button) * `button` - button (from LED Settings) that controls this relay If there is no MultiRelay section, just save current configuration and re-open Usermods settings page. @@ -97,4 +104,7 @@ Have fun - @blazoncek 2021-11 * Added information about dynamic configuration options -* Added button support. \ No newline at end of file +* Added button support. + +2023-05 +* Added support for PCF8574 I2C port expander (multiple) \ No newline at end of file diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index 6143a6b997..12c0a8da3e 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -4,6 +4,19 @@ #ifndef MULTI_RELAY_MAX_RELAYS #define MULTI_RELAY_MAX_RELAYS 4 +#else + #if MULTI_RELAY_MAX_RELAYS>8 + #undef MULTI_RELAY_MAX_RELAYS + #define MULTI_RELAY_MAX_RELAYS 8 + #warning Maximum relays set to 8 + #endif +#endif + +#ifndef MULTI_RELAY_PINS + #define MULTI_RELAY_PINS -1 + #define MULTI_RELAY_ENABLED false +#else + #define MULTI_RELAY_ENABLED true #endif #define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing) @@ -11,21 +24,37 @@ #define ON true #define OFF false +#ifndef USERMOD_USE_PCF8574 + #undef USE_PCF8574 + #define USE_PCF8574 false +#else + #undef USE_PCF8574 + #define USE_PCF8574 true +#endif + +#ifndef PCF8574_ADDRESS + #define PCF8574_ADDRESS 0x20 // some may start at 0x38 +#endif + /* * This usermod handles multiple relay outputs. * These outputs complement built-in relay output in a way that the activation can be delayed. * They can also activate/deactivate in reverse logic independently. + * + * Written and maintained by @blazoncek */ typedef struct relay_t { int8_t pin; - bool active; - bool mode; - bool state; - bool external; - uint16_t delay; - int8_t button; + struct { // reduces memory footprint + bool active : 1; // is the relay waiting to be switched + bool invert : 1; // does On mean 1 or 0 + bool state : 1; // 1 relay is On, 0 relay is Off + bool external : 1; // is the relay externally controlled + int8_t button : 4; // which button triggers relay + }; + uint16_t delay; // amount of ms to wait after it is activated } Relay; @@ -33,22 +62,17 @@ class MultiRelay : public Usermod { private: // array of relays - Relay _relay[MULTI_RELAY_MAX_RELAYS]; - - // switch timer start time - uint32_t _switchTimerStart = 0; - // old brightness - bool _oldMode; - - // usermod enabled - bool enabled = false; // needs to be configured (no default config) - // status of initialisation - bool initDone = false; - - bool HAautodiscovery = false; - - uint16_t periodicBroadcastSec = 60; - unsigned long lastBroadcast = 0; + Relay _relay[MULTI_RELAY_MAX_RELAYS]; + + uint32_t _switchTimerStart; // switch timer start time + bool _oldMode; // old brightness + bool enabled; // usermod enabled + bool initDone; // status of initialisation + bool usePcf8574; + uint8_t addrPcf8574; + bool HAautodiscovery; + uint16_t periodicBroadcastSec; + unsigned long lastBroadcast; // strings to reduce flash memory usage (used more than twice) static const char _name[]; @@ -60,157 +84,53 @@ class MultiRelay : public Usermod { static const char _button[]; static const char _broadcast[]; static const char _HAautodiscovery[]; + static const char _pcf8574[]; + static const char _pcfAddress[]; - void publishMqtt(int relay) { - //Check if MQTT Connected, otherwise it will crash the 8266 - if (WLED_MQTT_CONNECTED){ - char subuf[64]; - sprintf_P(subuf, PSTR("%s/relay/%d"), mqttDeviceTopic, relay); - mqtt->publish(subuf, 0, false, _relay[relay].state ? "on" : "off"); - } - } - - /** - * switch off the strip if the delay has elapsed - */ - void handleOffTimer() { - unsigned long now = millis(); - bool activeRelays = false; - for (uint8_t i=0; i 0 && now - _switchTimerStart > (_relay[i].delay*1000)) { - if (!_relay[i].external) toggleRelay(i); - _relay[i].active = false; - } else if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) { - if (_relay[i].pin>=0) publishMqtt(i); - } - activeRelays = activeRelays || _relay[i].active; - } - if (!activeRelays) _switchTimerStart = 0; - if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) lastBroadcast = now; - } - - /** - * HTTP API handler - * borrowed from: - * https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h - */ - #define GEOGABVERSION "0.1.3" - void InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer - DEBUG_PRINTLN(F("Relays: Initialize HTML API")); - - server.on("/relays", HTTP_GET, [this](AsyncWebServerRequest *request) { - DEBUG_PRINTLN("Relays: HTML API"); - String janswer; - String error = ""; - //int params = request->params(); - janswer = F("{\"NoOfRelays\":"); - janswer += String(MULTI_RELAY_MAX_RELAYS) + ","; - - if (getActiveRelayCount()) { - // Commands - if(request->hasParam("switch")) { - /**** Switch ****/ - AsyncWebParameter* p = request->getParam("switch"); - // Get Values - for (int i=0; ivalue(), ',', i); - if (value==-1) { - error = F("There must be as many arguments as relays"); - } else { - // Switch - if (_relay[i].external) switchRelay(i, (bool)value); - } - } - } else if(request->hasParam("toggle")) { - /**** Toggle ****/ - AsyncWebParameter* p = request->getParam("toggle"); - // Get Values - for (int i=0;ivalue(), ',', i); - if (value==-1) { - error = F("There must be as many arguments as relays"); - } else { - // Toggle - if (value && _relay[i].external) toggleRelay(i); - } - } - } else { - error = F("No valid command found"); - } - } else { - error = F("No active relays"); - } - - // Status response - char sbuf[16]; - for (int i=0; isend(200, "application/json", janswer); - }); - } + void handleOffTimer(); + void InitHtmlAPIHandle(); + int getValue(String data, char separator, int index); + uint8_t getActiveRelayCount(); - int getValue(String data, char separator, int index) { - int found = 0; - int strIndex[] = {0, -1}; - int maxIndex = data.length()-1; + byte IOexpanderWrite(byte address, byte _data); + byte IOexpanderRead(int address); - for(int i=0; i<=maxIndex && found<=index; i++){ - if(data.charAt(i)==separator || i==maxIndex){ - found++; - strIndex[0] = strIndex[1]+1; - strIndex[1] = (i == maxIndex) ? i+1 : i; - } - } - return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1; - } + void publishMqtt(int relay); +#ifndef WLED_DISABLE_MQTT + void publishHomeAssistantAutodiscovery(); +#endif public: /** * constructor */ - MultiRelay() { - for (uint8_t i=0; i=MULTI_RELAY_MAX_RELAYS || _relay[relay].pin<0) return; - _relay[relay].state = mode; - pinMode(_relay[relay].pin, OUTPUT); - digitalWrite(_relay[relay].pin, mode ? !_relay[relay].mode : _relay[relay].mode); - publishMqtt(relay); - } + void switchRelay(uint8_t relay, bool mode); /** * toggle relay @@ -219,340 +139,58 @@ class MultiRelay : public Usermod { switchRelay(relay, !_relay[relay].state); } - uint8_t getActiveRelayCount() { - uint8_t count = 0; - for (uint8_t i=0; i=0) count++; - return count; - } - - //Functions called by WLED - - /** - * handling of MQTT message - * topic only contains stripped topic (part after /wled/MAC) - * topic should look like: /relay/X/command; where X is relay number, 0 based - */ - bool onMqttMessage(char* topic, char* payload) { - if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, PSTR("/command"), 8) == 0) { - uint8_t relay = strtoul(topic+7, NULL, 10); - if (relaysubscribe(subuf, 0); - if (HAautodiscovery) publishHomeAssistantAutodiscovery(); - for (uint8_t i=0; i= 0 && _relay[i].external) { - StaticJsonDocument<1024> json; - sprintf_P(buf, PSTR("%s Switch %d"), serverDescription, i); //max length: 33 + 8 + 3 = 44 - json[F("name")] = buf; - - sprintf_P(buf, PSTR("%s/relay/%d"), mqttDeviceTopic, i); //max length: 33 + 7 + 3 = 43 - json["~"] = buf; - strcat_P(buf, PSTR("/command")); - mqtt->subscribe(buf, 0); - - json[F("stat_t")] = "~"; - json[F("cmd_t")] = F("~/command"); - json[F("pl_off")] = F("off"); - json[F("pl_on")] = F("on"); - json[F("uniq_id")] = uid; - - strcpy(buf, mqttDeviceTopic); //max length: 33 + 7 = 40 - strcat_P(buf, PSTR("/status")); - json[F("avty_t")] = buf; - json[F("pl_avail")] = F("online"); - json[F("pl_not_avail")] = F("offline"); - //TODO: dev - payload_size = serializeJson(json, json_str); - } else { - //Unpublish disabled or internal relays - json_str[0] = 0; - payload_size = 0; - } - sprintf_P(buf, PSTR("homeassistant/switch/%s/config"), uid); - mqtt->publish(buf, 0, true, json_str, payload_size); - } - } - /** * setup() is called once at boot. WiFi is not yet connected at this point. * You can use it to initialize variables, sensors or similar. */ - void setup() { - // pins retrieved from cfg.json (readFromConfig()) prior to running setup() - for (uint8_t i=0; i=0 && !_relay[i].external) _relay[i].active = true; - } - } + void loop(); - handleOffTimer(); - } +#ifndef WLED_DISABLE_MQTT + bool onMqttMessage(char* topic, char* payload); + void onMqttConnect(bool sessionPresent); +#endif /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ - bool 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) { - return false; - } - - bool handled = false; - for (uint8_t i=0; i WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) - for (uint8_t i=0; i=0 && _relay[i].button == b) { - switchRelay(i, buttonPressedBefore[b]); - buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state - } - } - } - return handled; - } - - //momentary button logic - if (isButtonPressed(b)) { //pressed - - if (!buttonPressedBefore[b]) buttonPressedTime[b] = now; - buttonPressedBefore[b] = true; + bool handleButton(uint8_t b); - if (now - buttonPressedTime[b] > 600) { //long press - //longPressAction(b); //not exposed - //handled = false; //use if you want to pass to default behaviour - buttonLongPressed[b] = true; - } - - } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released - - long dur = now - buttonPressedTime[b]; - if (dur < WLED_DEBOUNCE_THRESHOLD) { - buttonPressedBefore[b] = false; - return handled; - } //too short "press", debounce - bool doublePress = buttonWaitTime[b]; //did we have short press before? - buttonWaitTime[b] = 0; - - if (!buttonLongPressed[b]) { //short press - // if this is second release within 350ms it is a double press (buttonWaitTime!=0) - if (doublePress) { - //doublePressAction(b); //not exposed - //handled = false; //use if you want to pass to default behaviour - } else { - buttonWaitTime[b] = now; - } - } - buttonPressedBefore[b] = false; - buttonLongPressed[b] = 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; - //shortPressAction(b); //not exposed - for (uint8_t i=0; i=0 && _relay[i].button == b) { - toggleRelay(i); - } - } - } - return handled; - } - /** * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. */ - void addToJsonInfo(JsonObject &root) { - if (enabled) { - JsonObject user = root["u"]; - if (user.isNull()) - user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray(F("Number of relays")); //name - infoArr.add(String(getActiveRelayCount())); - - String uiDomString; - for (uint8_t i=0; i"); - uiDomString += F("Relay "); - uiDomString += i; - uiDomString += F(" "); - JsonArray infoArr = user.createNestedArray(uiDomString); // timer value - - infoArr.add(_relay[i].state ? "on" : "off"); - } - } - } + void addToJsonInfo(JsonObject &root); /** * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - void addToJsonState(JsonObject &root) { - if (!initDone || !enabled) return; // prevent crash on boot applyPreset() - JsonObject multiRelay = root[FPSTR(_name)]; - if (multiRelay.isNull()) { - multiRelay = root.createNestedObject(FPSTR(_name)); - } - #if MULTI_RELAY_MAX_RELAYS > 1 - JsonArray rel_arr = multiRelay.createNestedArray(F("relays")); - for (uint8_t i=0; i() && usermod[FPSTR(_relay_str)].is() && usermod[FPSTR(_relay_str)].as()>=0) { - switchRelay(usermod[FPSTR(_relay_str)].as(), usermod["on"].as()); - } - } else if (root[FPSTR(_name)].is()) { - JsonArray relays = root[FPSTR(_name)].as(); - for (JsonVariant r : relays) { - if (r["on"].is() && r[FPSTR(_relay_str)].is() && r[FPSTR(_relay_str)].as()>=0) { - switchRelay(r[FPSTR(_relay_str)].as(), r["on"].as()); - } - } - } - } + void readFromJsonState(JsonObject &root); /** * provide the changeable values */ - void addToConfig(JsonObject &root) { - JsonObject top = root.createNestedObject(FPSTR(_name)); - - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_broadcast)] = periodicBroadcastSec; - for (uint8_t i=0; i=0) { - pinManager.deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); +// class implementation + +void MultiRelay::publishMqtt(int relay) { +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED){ + char subuf[64]; + sprintf_P(subuf, PSTR("%s/relay/%d"), mqttDeviceTopic, relay); + mqtt->publish(subuf, 0, false, _relay[relay].state ? "on" : "off"); + } +#endif +} + +/** + * switch off the strip if the delay has elapsed + */ +void MultiRelay::handleOffTimer() { + unsigned long now = millis(); + bool activeRelays = false; + for (int i=0; i 0 && now - _switchTimerStart > (_relay[i].delay*1000)) { + if (!_relay[i].external) switchRelay(i, !offMode); + _relay[i].active = false; + } else if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) { + if (_relay[i].pin>=0) publishMqtt(i); + } + activeRelays = activeRelays || _relay[i].active; + } + if (!activeRelays) _switchTimerStart = 0; + if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) lastBroadcast = now; +} + +/** + * HTTP API handler + * borrowed from: + * https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h + */ +#define GEOGABVERSION "0.1.3" +void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer + DEBUG_PRINTLN(F("Relays: Initialize HTML API")); + + server.on("/relays", HTTP_GET, [this](AsyncWebServerRequest *request) { + DEBUG_PRINTLN("Relays: HTML API"); + String janswer; + String error = ""; + //int params = request->params(); + janswer = F("{\"NoOfRelays\":"); + janswer += String(MULTI_RELAY_MAX_RELAYS) + ","; + + if (getActiveRelayCount()) { + // Commands + if(request->hasParam("switch")) { + /**** Switch ****/ + AsyncWebParameter* p = request->getParam("switch"); + // Get Values + for (int i=0; ivalue(), ',', i); + if (value==-1) { + error = F("There must be as many arguments as relays"); + } else { + // Switch + if (_relay[i].external) switchRelay(i, (bool)value); } - // allocate new pins - for (uint8_t i=0; i=0 && pinManager.allocatePin(_relay[i].pin, true, PinOwner::UM_MultiRelay)) { - if (!_relay[i].external) { - _relay[i].state = !offMode; - switchRelay(i, _relay[i].state); - _oldMode = offMode; - } + } + } else if(request->hasParam("toggle")) { + /**** Toggle ****/ + AsyncWebParameter* p = request->getParam("toggle"); + // Get Values + for (int i=0;ivalue(), ',', i); + if (value==-1) { + error = F("There must be as many arguments as relays"); } else { - _relay[i].pin = -1; + // Toggle + if (value && _relay[i].external) toggleRelay(i); } - _relay[i].active = false; } - DEBUG_PRINTLN(F(" config (re)loaded.")); + } else { + error = F("No valid command found"); } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_broadcast)].isNull(); + } else { + error = F("No active relays"); } - /** - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_MULTI_RELAY; + // Status response + char sbuf[16]; + for (int i=0; isend(200, "application/json", janswer); + }); +} + +int MultiRelay::getValue(String data, char separator, int index) { + int found = 0; + int strIndex[] = {0, -1}; + int maxIndex = data.length()-1; + + for(int i=0; i<=maxIndex && found<=index; i++){ + if(data.charAt(i)==separator || i==maxIndex){ + found++; + strIndex[0] = strIndex[1]+1; + strIndex[1] = (i == maxIndex) ? i+1 : i; + } + } + return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1; +} + +//Write a byte to the IO expander +byte MultiRelay::IOexpanderWrite(byte address, byte _data ) { + Wire.beginTransmission(address); + Wire.write(_data); + return Wire.endTransmission(); +} + +//Read a byte from the IO expander +byte MultiRelay::IOexpanderRead(int address) { + byte _data = 0; + Wire.requestFrom(address, 1); + if (Wire.available()) { + _data = Wire.read(); + } + return _data; +} + + +// public methods + +MultiRelay::MultiRelay() + : _switchTimerStart(0) + , enabled(MULTI_RELAY_ENABLED) + , initDone(false) + , usePcf8574(USE_PCF8574) + , addrPcf8574(PCF8574_ADDRESS) + , HAautodiscovery(false) + , periodicBroadcastSec(60) + , lastBroadcast(0) +{ + const int8_t defPins[] = {MULTI_RELAY_PINS}; + for (size_t i=0; i=MULTI_RELAY_MAX_RELAYS || _relay[relay].pin<0) return; + _relay[relay].state = mode; + if (usePcf8574 && _relay[relay].pin >= 100) { + // we need to send all outputs at the same time + uint8_t state = 0; + for (int i=0; i=0) count++; + return count; +} + + +//Functions called by WLED + +#ifndef WLED_DISABLE_MQTT +/** + * handling of MQTT message + * topic only contains stripped topic (part after /wled/MAC) + * topic should look like: /relay/X/command; where X is relay number, 0 based + */ +bool MultiRelay::onMqttMessage(char* topic, char* payload) { + if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, PSTR("/command"), 8) == 0) { + uint8_t relay = strtoul(topic+7, NULL, 10); + if (relaysubscribe(subuf, 0); + if (HAautodiscovery) publishHomeAssistantAutodiscovery(); + for (int i=0; i= 0 && _relay[i].external) { + StaticJsonDocument<1024> json; + sprintf_P(buf, PSTR("%s Switch %d"), serverDescription, i); //max length: 33 + 8 + 3 = 44 + json[F("name")] = buf; + + sprintf_P(buf, PSTR("%s/relay/%d"), mqttDeviceTopic, i); //max length: 33 + 7 + 3 = 43 + json["~"] = buf; + strcat_P(buf, PSTR("/command")); + mqtt->subscribe(buf, 0); + + json[F("stat_t")] = "~"; + json[F("cmd_t")] = F("~/command"); + json[F("pl_off")] = "off"; + json[F("pl_on")] = "on"; + json[F("uniq_id")] = uid; + + strcpy(buf, mqttDeviceTopic); //max length: 33 + 7 = 40 + strcat_P(buf, PSTR("/status")); + json[F("avty_t")] = buf; + json[F("pl_avail")] = F("online"); + json[F("pl_not_avail")] = F("offline"); + //TODO: dev + payload_size = serializeJson(json, json_str); + } else { + //Unpublish disabled or internal relays + json_str[0] = 0; + payload_size = 0; + } + sprintf_P(buf, PSTR("homeassistant/switch/%s/config"), uid); + mqtt->publish(buf, 0, true, json_str, payload_size); + } +} +#endif + +/** + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ +void MultiRelay::setup() { + // pins retrieved from cfg.json (readFromConfig()) prior to running setup() + // if we want PCF8574 expander I2C pins need to be valid + if (i2c_sda<0 || i2c_scl<0) usePcf8574 = false; + + uint8_t state = 0; + for (int i=0; i= 100) { + uint8_t pin = _relay[i].pin - 100; + if (!_relay[i].external) _relay[i].state = !offMode; + state |= (uint8_t)(_relay[i].invert ? !_relay[i].state : _relay[i].state) << pin; + } else if (_relay[i].pin<100 && _relay[i].pin>=0) { + if (pinManager.allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { + if (!_relay[i].external) _relay[i].state = !offMode; + switchRelay(i, _relay[i].state); + _relay[i].active = false; + } else { + _relay[i].pin = -1; // allocation failed + } + } + } + if (usePcf8574) { + IOexpanderWrite(addrPcf8574, state); // init expander (set all outputs) + DEBUG_PRINTLN(F("PCF8574(s) inited.")); + } + _oldMode = offMode; + initDone = true; +} + +/** + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ +void MultiRelay::loop() { + static unsigned long lastUpdate = 0; + yield(); + if (!enabled || (strip.isUpdating() && millis() - lastUpdate < 100)) return; + + if (millis() - lastUpdate < 100) return; // update only 10 times/s + lastUpdate = millis(); + + //set relay when LEDs turn on + if (_oldMode != offMode) { + _oldMode = offMode; + _switchTimerStart = millis(); + for (int i=0; i=0) && !_relay[i].external) _relay[i].active = true; + } + } + + handleOffTimer(); +} + +/** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ +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) { + return false; + } + + bool handled = false; + for (int i=0; i WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) + for (int i=0; i 600) { //long press + //longPressAction(b); //not exposed + //handled = false; //use if you want to pass to default behaviour + buttonLongPressed[b] = true; + } + + } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + + long dur = now - buttonPressedTime[b]; + if (dur < WLED_DEBOUNCE_THRESHOLD) { + buttonPressedBefore[b] = false; + return handled; + } //too short "press", debounce + bool doublePress = buttonWaitTime[b]; //did we have short press before? + buttonWaitTime[b] = 0; + + if (!buttonLongPressed[b]) { //short press + // if this is second release within 350ms it is a double press (buttonWaitTime!=0) + if (doublePress) { + //doublePressAction(b); //not exposed + //handled = false; //use if you want to pass to default behaviour + } else { + buttonWaitTime[b] = now; + } + } + buttonPressedBefore[b] = false; + buttonLongPressed[b] = 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; + //shortPressAction(b); //not exposed + for (int i=0; i"); + uiDomString += F(""); + infoArr.add(uiDomString); + } + } +} + +/** + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ +void MultiRelay::addToJsonState(JsonObject &root) { + if (!initDone || !enabled) return; // prevent crash on boot applyPreset() + JsonObject multiRelay = root[FPSTR(_name)]; + if (multiRelay.isNull()) { + multiRelay = root.createNestedObject(FPSTR(_name)); + } + #if MULTI_RELAY_MAX_RELAYS > 1 + JsonArray rel_arr = multiRelay.createNestedArray(F("relays")); + for (int i=0; i() && usermod[FPSTR(_relay_str)].as()>=0) { + int rly = usermod[FPSTR(_relay_str)].as(); + if (usermod["on"].is()) { + switchRelay(rly, usermod["on"].as()); + } else if (usermod["on"].is() && usermod["on"].as()[0] == 't') { + toggleRelay(rly); + } + } + } else if (root[FPSTR(_name)].is()) { + JsonArray relays = root[FPSTR(_name)].as(); + for (JsonVariant r : relays) { + if (r[FPSTR(_relay_str)].is() && r[FPSTR(_relay_str)].as()>=0) { + int rly = r[FPSTR(_relay_str)].as(); + if (r["on"].is()) { + switchRelay(rly, r["on"].as()); + } else if (r["on"].is() && r["on"].as()[0] == 't') { + toggleRelay(rly); + } + } + } + } +} + +/** + * provide the changeable values + */ +void MultiRelay::addToConfig(JsonObject &root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_pcf8574)] = usePcf8574; + top[FPSTR(_pcfAddress)] = addrPcf8574; + top[FPSTR(_broadcast)] = periodicBroadcastSec; + top[FPSTR(_HAautodiscovery)] = HAautodiscovery; + for (int i=0; i(not hex!)');")); + oappend(SET_F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); + //oappend(SET_F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); + oappend(SET_F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); +} + +/** + * restore the changeable values + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ +bool MultiRelay::readFromConfig(JsonObject &root) { + int8_t oldPin[MULTI_RELAY_MAX_RELAYS]; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + //bool configComplete = !top.isNull(); + //configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + enabled = top[FPSTR(_enabled)] | enabled; + usePcf8574 = top[FPSTR(_pcf8574)] | usePcf8574; + addrPcf8574 = top[FPSTR(_pcfAddress)] | addrPcf8574; + // if I2C is not globally initialised just ignore + if (i2c_sda<0 || i2c_scl<0) usePcf8574 = false; + periodicBroadcastSec = top[FPSTR(_broadcast)] | periodicBroadcastSec; + periodicBroadcastSec = min(900,max(0,(int)periodicBroadcastSec)); + HAautodiscovery = top[FPSTR(_HAautodiscovery)] | HAautodiscovery; + + for (int i=0; i=0 && oldPin[i]<100) { + pinManager.deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); + } + // allocate new pins + setup(); + DEBUG_PRINTLN(F(" config (re)loaded.")); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_pcf8574)].isNull(); +} // strings to reduce flash memory usage (used more than twice) -const char MultiRelay::_name[] PROGMEM = "MultiRelay"; -const char MultiRelay::_enabled[] PROGMEM = "enabled"; -const char MultiRelay::_relay_str[] PROGMEM = "relay"; -const char MultiRelay::_delay_str[] PROGMEM = "delay-s"; -const char MultiRelay::_activeHigh[] PROGMEM = "active-high"; -const char MultiRelay::_external[] PROGMEM = "external"; -const char MultiRelay::_button[] PROGMEM = "button"; -const char MultiRelay::_broadcast[] PROGMEM = "broadcast-sec"; +const char MultiRelay::_name[] PROGMEM = "MultiRelay"; +const char MultiRelay::_enabled[] PROGMEM = "enabled"; +const char MultiRelay::_relay_str[] PROGMEM = "relay"; +const char MultiRelay::_delay_str[] PROGMEM = "delay-s"; +const char MultiRelay::_activeHigh[] PROGMEM = "active-high"; +const char MultiRelay::_external[] PROGMEM = "external"; +const char MultiRelay::_button[] PROGMEM = "button"; +const char MultiRelay::_broadcast[] PROGMEM = "broadcast-sec"; const char MultiRelay::_HAautodiscovery[] PROGMEM = "HA-autodiscovery"; +const char MultiRelay::_pcf8574[] PROGMEM = "use-PCF8574"; +const char MultiRelay::_pcfAddress[] PROGMEM = "PCF8574-address"; diff --git a/usermods/photoresistor_sensor_mqtt_v1/README.md b/usermods/photoresistor_sensor_mqtt_v1/README.md index 33d96bc743..f83bb01a22 100644 --- a/usermods/photoresistor_sensor_mqtt_v1/README.md +++ b/usermods/photoresistor_sensor_mqtt_v1/README.md @@ -1,10 +1,12 @@ # Photoresister sensor with MQTT -This simple usermod allows attaching a photoresistor sensor like the KY-018 and publish the readings in percentage over MQTT. The frequency of MQTT messages can be modified, and there is a threshold value that can be set so that significant changes in the readings can be published immediately instead of waiting for the next update. This was found to be a good compromise between spamming MQTT messages and delayed updates. +Enables attaching a photoresistor sensor like the KY-018 and publishing the readings as a percentage, via MQTT. The frequency of MQTT messages is user definable. +A threshold value can be set so significant changes in the readings are published immediately vice waiting for the next update. This was found to be a good compromise between excessive MQTT traffic and delayed updates. -I also found it useful to limit the frequency of analog pin reads because otherwise the board hangs. +I also found it useful to limit the frequency of analog pin reads, otherwise the board hangs. -This usermod has only been tested with the KY-018 sensor though should work for any other analog pin sensor. Note that this does not control the LED strip directly, it only publishes MQTT readings for use with other integrations like Home Assistant. +This usermod has only been tested with the KY-018 sensor though it should work for any other analog pin sensor. +Note: this does not control the LED strip directly, it only publishes MQTT readings for use with other integrations like Home Assistant. ## Installation diff --git a/usermods/project_cars_shiftlight/readme.md b/usermods/project_cars_shiftlight/readme.md index 4490a2ba10..433da43006 100644 --- a/usermods/project_cars_shiftlight/readme.md +++ b/usermods/project_cars_shiftlight/readme.md @@ -1,12 +1,11 @@ ### Shift Light for Project Cars Turn your WLED lights into a rev light and shift indicator for Project Cars. +It's easy to use. -It is pretty straight forward to use. +1. Make sure your WLED device and your PC/console are on the same network and can talk to each other -1. Make sure, your WLED device and your PC/console are on the same network and can talk to each other - -2. Go to the gameplay settings menu in PCARS and enable UDP. There are 9 numbers you can choose from. This is the refresh rate. The lower the number, the better. But you might run into problems at faster rates. +2. Go to the gameplay settings menu in PCARS and enable UDP. There are 9 numbers you can choose from. This is the refresh rate. The lower the number, the better. However, you might run into problems at faster rates. | Number | Updates/Second | | ------ | -------------- | @@ -20,4 +19,5 @@ It is pretty straight forward to use. | 8 | 05 | | 9 | 1 | -3. once you enter a race, WLED should automatically shift to PCARS mode. Done. +3. Once you enter a race, WLED should automatically shift to PCARS mode. +4. Done. diff --git a/usermods/pwm_outputs/readme.md b/usermods/pwm_outputs/readme.md new file mode 100644 index 0000000000..0309ad3612 --- /dev/null +++ b/usermods/pwm_outputs/readme.md @@ -0,0 +1,27 @@ +# PWM outputs + +v2 Usermod to add generic PWM outputs to WLED. Usermode could be used to control servo motors, LED brightness or any other device controlled by PWM signal. + +## Installation + +Add the compile-time option `-D USERMOD_PWM_OUTPUTS` to your `platformio.ini` (or `platformio_override.ini`). By default upt to 3 PWM outputs could be configured, to increase that limit add build argument `-D USERMOD_PWM_OUTPUT_PINS=10` (replace 10 by desired amount). + +Currently only ESP32 is supported. + +## Configuration + +By default PWM outputs are disabled, navigate to Usermods settings and configure desired PWM pins and frequencies. + +## Usage + +If PWM output is configured, it starts to publish its duty cycle value (0-1) both to state JSON and to info JSON (visible in UI info panel). To set PWM duty cycle, use JSON api (over HTTP or over Serial) + +```json +{ + "pwm": { + "0": {"duty": 0.1}, + "1": {"duty": 0.2}, + ... + } +} +``` diff --git a/usermods/pwm_outputs/usermod_pwm_outputs.h b/usermods/pwm_outputs/usermod_pwm_outputs.h new file mode 100644 index 0000000000..1880308c45 --- /dev/null +++ b/usermods/pwm_outputs/usermod_pwm_outputs.h @@ -0,0 +1,221 @@ +#pragma once +#include "wled.h" + +#ifndef ESP32 + #error This usermod does not support the ESP8266. +#endif + +#ifndef USERMOD_PWM_OUTPUT_PINS + #define USERMOD_PWM_OUTPUT_PINS 3 +#endif + + +class PwmOutput { + public: + + void open(int8_t pin, uint32_t freq) { + + if (enabled_) { + if (pin == pin_ && freq == freq_) { + return; // PWM output is already open + } else { + close(); // Config has changed, close and reopen + } + } + + pin_ = pin; + freq_ = freq; + if (pin_ < 0) + return; + + DEBUG_PRINTF("pwm_output[%d]: setup to freq %d\n", pin_, freq_); + if (!pinManager.allocatePin(pin_, true, PinOwner::UM_PWM_OUTPUTS)) + return; + + channel_ = pinManager.allocateLedc(1); + if (channel_ == 255) { + DEBUG_PRINTF("pwm_output[%d]: failed to quire ledc\n", pin_); + pinManager.deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); + return; + } + + ledcSetup(channel_, freq_, bit_depth_); + ledcAttachPin(pin_, channel_); + DEBUG_PRINTF("pwm_output[%d]: init successful\n", pin_); + enabled_ = true; + } + + void close() { + DEBUG_PRINTF("pwm_output[%d]: close\n", pin_); + if (!enabled_) + return; + pinManager.deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); + if (channel_ != 255) + pinManager.deallocateLedc(channel_, 1); + channel_ = 255; + duty_ = 0.0f; + enabled_ = false; + } + + void setDuty(const float duty) { + DEBUG_PRINTF("pwm_output[%d]: set duty %f\n", pin_, duty); + if (!enabled_) + return; + duty_ = min(1.0f, max(0.0f, duty)); + const uint32_t value = static_cast((1 << bit_depth_) * duty_); + ledcWrite(channel_, value); + } + + void setDuty(const uint16_t duty) { + setDuty(static_cast(duty) / 65535.0f); + } + + bool isEnabled() const { + return enabled_; + } + + void addToJsonState(JsonObject& pwmState) const { + pwmState[F("duty")] = duty_; + } + + void readFromJsonState(JsonObject& pwmState) { + if (pwmState.isNull()) { + return; + } + float duty; + if (getJsonValue(pwmState[F("duty")], duty)) { + setDuty(duty); + } + } + + void addToJsonInfo(JsonObject& user) const { + if (!enabled_) + return; + char buffer[12]; + sprintf_P(buffer, PSTR("PWM pin %d"), pin_); + JsonArray data = user.createNestedArray(buffer); + data.add(1e2f * duty_); + data.add(F("%")); + } + + void addToConfig(JsonObject& pwmConfig) const { + pwmConfig[F("pin")] = pin_; + pwmConfig[F("freq")] = freq_; + } + + bool readFromConfig(JsonObject& pwmConfig) { + if (pwmConfig.isNull()) + return false; + + bool configComplete = true; + int8_t newPin = pin_; + uint32_t newFreq = freq_; + configComplete &= getJsonValue(pwmConfig[F("pin")], newPin); + configComplete &= getJsonValue(pwmConfig[F("freq")], newFreq); + + open(newPin, newFreq); + + return configComplete; + } + + private: + int8_t pin_ {-1}; + uint32_t freq_ {50}; + static const uint8_t bit_depth_ {12}; + uint8_t channel_ {255}; + float duty_ {0.0f}; + bool enabled_ {false}; +}; + + +class PwmOutputsUsermod : public Usermod { + public: + + static const char USERMOD_NAME[]; + static const char PWM_STATE_NAME[]; + + void setup() { + // By default all PWM outputs are disabled, no setup do be done + } + + void loop() { + } + + void addToJsonState(JsonObject& root) { + JsonObject pwmStates = root.createNestedObject(PWM_STATE_NAME); + for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { + const PwmOutput& pwm = pwms_[i]; + if (!pwm.isEnabled()) + continue; + char buffer[4]; + sprintf_P(buffer, PSTR("%d"), i); + JsonObject pwmState = pwmStates.createNestedObject(buffer); + pwm.addToJsonState(pwmState); + } + } + + void readFromJsonState(JsonObject& root) { + JsonObject pwmStates = root[PWM_STATE_NAME]; + if (pwmStates.isNull()) + return; + + for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { + PwmOutput& pwm = pwms_[i]; + if (!pwm.isEnabled()) + continue; + char buffer[4]; + sprintf_P(buffer, PSTR("%d"), i); + JsonObject pwmState = pwmStates[buffer]; + pwm.readFromJsonState(pwmState); + } + } + + void addToJsonInfo(JsonObject& root) { + JsonObject user = root[F("u")]; + if (user.isNull()) + user = root.createNestedObject(F("u")); + + for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { + const PwmOutput& pwm = pwms_[i]; + pwm.addToJsonInfo(user); + } + } + + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(USERMOD_NAME); + for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { + const PwmOutput& pwm = pwms_[i]; + char buffer[8]; + sprintf_P(buffer, PSTR("PWM %d"), i); + JsonObject pwmConfig = top.createNestedObject(buffer); + pwm.addToConfig(pwmConfig); + } + } + + bool readFromConfig(JsonObject& root) { + JsonObject top = root[USERMOD_NAME]; + if (top.isNull()) + return false; + + bool configComplete = true; + for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { + PwmOutput& pwm = pwms_[i]; + char buffer[8]; + sprintf_P(buffer, PSTR("PWM %d"), i); + JsonObject pwmConfig = top[buffer]; + configComplete &= pwm.readFromConfig(pwmConfig); + } + return configComplete; + } + + uint16_t getId() { + return USERMOD_ID_PWM_OUTPUTS; + } + + private: + PwmOutput pwms_[USERMOD_PWM_OUTPUT_PINS]; + +}; + +const char PwmOutputsUsermod::USERMOD_NAME[] PROGMEM = "PwmOutputs"; +const char PwmOutputsUsermod::PWM_STATE_NAME[] PROGMEM = "pwm"; diff --git a/usermods/quinled-an-penta/readme.md b/usermods/quinled-an-penta/readme.md index 4a4c0290ab..c1260d9134 100644 --- a/usermods/quinled-an-penta/readme.md +++ b/usermods/quinled-an-penta/readme.md @@ -1,8 +1,8 @@ # QuinLED-An-Penta -The (un)official usermod to get the best out of the QuinLED-An-Penta (https://quinled.info/quinled-an-penta/), like using the OLED and the SHT30 temperature/humidity sensor. +The (un)official usermod to get the best out of the QuinLED-An-Penta (https://quinled.info/quinled-an-penta/), e.g. using the OLED and the SHT30 temperature/humidity sensor. ## Requirements -* "u8gs" by olikraus, v2.28 or higher: https://github.com/olikraus/u8g2 +* "u8g2" by olikraus, v2.28 or higher: https://github.com/olikraus/u8g2 * "SHT85" by Rob Tillaart, v0.2 or higher: https://github.com/RobTillaart/SHT85 ## Usermod installation @@ -31,15 +31,15 @@ lib_deps = ${esp32.lib_deps} ## Some words about the (optional) OLED This mod has been optimized for an SSD1306 driven 128x64 OLED. Using a smaller OLED or an OLED using a different driver will result in unexpected results. I highly recommend using these "two color monochromatic OLEDs", which have the first 16 pixels in a different color than the other 48, e.g. a yellow/blue OLED. -Also note, you need to have an **SPI** driven OLED, **not i2c**! +Note: you _must_ use an **SPI** driven OLED, **not an i2c one**! ### Limitations combined with Ethernet -The initial development of this mod had been done with a beta version of the QuinLED-An-Penta, which had a different IO layout for the OLED: The CS pin used to be IO_0, but has been changed to IO27 with the first v1 public release. Unfortunately, IO27 is used by the Ethernet boards, so WLED will not let you enable the OLED screen, if you're using it with Ethernet. This unfortunately makes the development I've done to support/show Ethernet information void, as it cannot be used. -However (and I've not tried this, as I don't own a v1 board): You can try to modify this mod and try to use IO27 for the OLED and share it with the Ethernet board. It is "just" the chip select pin, so there is a chance that both can coexist and use the same IO. You need to skip WLEDs PinManager for the CS pin, so WLED will not block using it. If you don't know how this works: Leave it. If you know what I'm talking about: Try it and please let me know on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG +The initial development of this mod was done with a beta version of the QuinLED-An-Penta, which had a different IO layout for the OLED: The CS pin _was_ IO_0, but has been changed to IO27 with the first v1 public release. Unfortunately, IO27 is used by Ethernet boards, so WLED will not let you enable the OLED screen, if you're using it with Ethernet. Unfortunately, that makes the development I've done to support/show Ethernet information invalid, as it cannot be used. +However, (and I've not tried this, as I don't own a v1 board) you can modify this usermod and try to use IO27 for the OLED and share it with the Ethernet board. It is "just" the chip select pin, so there is a chance that both can coexist and use the same IO. You need to skip WLEDs PinManager for the CS pin, so WLED will not block using it. If you don't know how this works, don't change it. If you know what I'm talking about, try it and please let me know on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG ### My OLED flickers after some time, what should I do? -That's a tricky one: During development I saw that the OLED sometimes starts to "bug out" / flicker and won't work anymore. This seems to be caused by the high PWM interference the board produces. It seems to loose its settings and then doesn't know how to draw anymore. Turns out the only way to fix this is to call the libraries `begin()` method again which will re-initialize the display. -If you're facing this issue, you can enable a setting I've added which will call the `begin()` roughly every 60 seconds between a page change. This will make the page change take ~500ms, but will fix the display. +That's a tricky one. During development I saw that the OLED sometimes starts to "drop out" / flicker and won't work anymore. This seems to be caused by the high PWM interference the board produces. It seems to lose its settings then doesn't know how to draw anymore. Turns out the only way to fix this is to call the libraries `begin()` method again which re-initializes the display. +If you're facing this issue, you can enable a setting which will call the `begin()` roughly every 60 seconds between page changes. This will make the page change take ~500ms, but will fix the display. ## Configuration @@ -53,11 +53,11 @@ Navigate to the "Config" and then to the "Usermods" section. If you compiled WLE * Possible values: Enabled/Disabled * Default: Disabled * OLED-Flip-Screen-180: - * What it does: Flips the screen 180° / upside-down + * What it does: Flips the screen 180° * Possible values: Enabled/Disabled * Default: Disabled * OLED-Seconds-Per-Page: - * What it does: Defines how long the OLED should stay on one page in seconds before changing to the next + * What it does: Number of seconds the OLED should stay on one page before changing pages * Possible values: Enabled/Disabled * Default: 10 * OLED-Fix-Bugged-Screen: @@ -76,4 +76,4 @@ Navigate to the "Config" and then to the "Usermods" section. If you compiled WLE * First implementation. ## Credits -ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG \ No newline at end of file +ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG diff --git a/usermods/readme.md b/usermods/readme.md index 0c56efaed2..8aa8d6abcf 100644 --- a/usermods/readme.md +++ b/usermods/readme.md @@ -2,17 +2,17 @@ This folder serves as a repository for usermods (custom `usermod.cpp` files)! -If you have created an usermod that you believe is useful (for example to support a particular sensor, display, feature...), feel free to contribute by opening a pull request! +If you have created a usermod you believe is useful (for example to support a particular sensor, display, feature...), feel free to contribute by opening a pull request! In order for other people to be able to have fun with your usermod, please keep these points in mind: - Create a folder in this folder with a descriptive name (for example `usermod_ds18b20_temp_sensor_mqtt`) - Include your custom files -- If your usermod requires changes to other WLED files, please write a `readme.md` outlining the steps one has to take to use the usermod +- If your usermod requires changes to other WLED files, please write a `readme.md` outlining the steps one needs to take - Create a pull request! - If your feature is useful for the majority of WLED users, I will consider adding it to the base code! -While I do my best to not break too much, keep in mind that as WLED is being updated, usermods might break. +While I do my best to not break too much, keep in mind that as WLED is updated, usermods might break. I am not actively maintaining any usermod in this directory, that is your responsibility as the creator of the usermod. For new usermods, I would recommend trying out the new v2 usermod API, which allows installing multiple usermods at once and new functions! diff --git a/usermods/rgb-rotary-encoder/readme.md b/usermods/rgb-rotary-encoder/readme.md index 2bf0ecb72a..ba5aad4df7 100644 --- a/usermods/rgb-rotary-encoder/readme.md +++ b/usermods/rgb-rotary-encoder/readme.md @@ -5,8 +5,8 @@ This usermod-v2 adds support for the awesome RGB Rotary Encoder Board by Adam Ze https://user-images.githubusercontent.com/3090131/124680599-0180ab80-dec7-11eb-9065-a6d08ebe0287.mp4 ## Credits -The actual / original code that does the different LED modes is from Adam Zeloof. So I don't take credit for these. But I ported it to WLED, which involved replacing the LED library he used (because, guess what, WLED already has one; so no need to add another one, but use whatever WLED uses), plus the rotary encoder library, because that one was not compatible with ESP, only Arduino. -So it was quite more work than I hoped, but I got there eventually :) +The actual / original code that controls the LED modes is from Adam Zeloof. I take no credit for it. I ported it to WLED, which involved replacing the LED library he used, (because WLED already has one, so no need to add another one) plus the rotary encoder library because it was not compatible with ESP, only Arduino. +It was quite a bit more work than I hoped, but I got there eventually :) ## Requirements * "ESP Rotary" by Lennart Hennigs, v1.5.0 or higher: https://github.com/LennartHennigs/ESPRotary @@ -33,25 +33,25 @@ lib_deps = ${esp8266.lib_deps} ``` ## How to connect the board to your ESP -We gonna need (minimum) three or (maximum) four GPIOs for the board: -* "ea": Basically tells if the encoder goes into one or the other direction -* "eb": Same thing, but the other direction -* "di": LED data in. To actually control the LEDs -* *(optional)* "sw": The integrated switch in the rotary encoder. Can be omitted for the bare functionality of just controlling the brightness +We'll need (minimum) three or (maximum) four GPIOs for the board: +* "ea": reports the encoder direction +* "eb": Same thing, opposite direction +* "di": LED data in. +* *(optional)* "sw": The integrated switch in the rotary encoder. Can be omitted for the bare functionality of controlling only the brightness -We also gonna need some power, so: +We'll also need power: * "vdd": Needs to be connected to **+5V**. -* "gnd": Well, it's GND. +* "gnd": Ground. -You can freely pick the GPIOs, it doesn't matter. Those will be configured in the "Usermods" section in the WLED web panel: +You can freely pick the GPIOs, it doesn't matter. Those will be configured in the "Usermods" section of the WLED web panel: ## Configuration -Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D RGB_ROTARY_ENCODER`, you will see the config for it there. The settings there are the GPIOs we mentioned before (*Note: The switch pin is not there, as this can just be configured the "normal" button on the "LED Preferences" page*), plus a few more: +Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D RGB_ROTARY_ENCODER`, you will see the config for it there. The settings there are the aforementioned GPIOs, (*Note: The switch pin is not there, as this can just be configured the "normal" button on the "LED Preferences" page*) plus a few more: * LED pin: * Possible values: Any valid and available GPIO * Default: 3 - * What it does: Pin to control the LED ring + * What it does: controls the LED ring * ea pin: * Possible values: Any valid and available GPIO * Default: 15 @@ -63,7 +63,7 @@ Navigate to the "Config" and then to the "Usermods" section. If you compiled WLE * LED Mode: * Possible values: 1-3 * Default: 3 - * What it does: The usermod provides three different modes of how the LEDs can look like. Here's an example: https://github.com/isotope-engineering/RGB-Encoder-Board/blob/master/images/rgb-encoder-animations.gif + * What it does: The usermod provides three different modes of how the LEDs can appear. Here's an example: https://github.com/isotope-engineering/RGB-Encoder-Board/blob/master/images/rgb-encoder-animations.gif * Up left is "1" * Up right is not supported / doesn't make sense for brightness control * Bottom left is "2" @@ -71,15 +71,15 @@ Navigate to the "Config" and then to the "Usermods" section. If you compiled WLE * LED Brightness: * Possible values: 1-255 * Default: 64 - * What it does: Brightness of the LED ring + * What it does: sets LED ring Brightness * Steps per click: * Possible values: Any positive number * Default: 4 - * What it does: With each "click", a rotary encoder actually increments it's "steps". Most rotary encoder do four "steps" per "click". I know this sounds super weird, so just leave this the default value, unless your rotary encoder behaves weirdly, like with one click, it makes two LEDs light up, or you sometimes need two click for one LED. Then you should play around with this value or write a small sketch using the same "ESP Rotary" library and read out the steps it does. + * What it does: With each "click", a rotary encoder actually increments its "steps". Most rotary encoders produce four "steps" per "click". Leave this at the default value unless your rotary encoder behaves strangely. e.g. with one click, it makes two LEDs light up, or you need two clicks for one LED. If that's the case, adjust this value or write a small sketch using the same "ESP Rotary" library and read out the steps it produce. * Increment per click: * Possible values: Any positive number * Default: 5 - * What it does: Most rotary encoder have 20 "clicks", so basically 20 positions. This value should be set to 100 / `number of clicks` + * What it does: Most rotary encoders have 20 "clicks" or positions. This value should be set to 100/`number of clicks` ## Change log 2021-07 diff --git a/usermods/sd_card/readme.md b/usermods/sd_card/readme.md new file mode 100644 index 0000000000..96390c05ac --- /dev/null +++ b/usermods/sd_card/readme.md @@ -0,0 +1,34 @@ +# SD-card mod + +## Build +- modify `platformio.ini` and add to the `build_flags` of your configuration the following +- choose the way your SD is connected + 1. via `-D WLED_USE_SD_MMC` when connected via MMC + 2. via `-D WLED_USE_SD_SPI` when connected via SPI (use usermod page to setup SPI pins) + +### Test +- enable `-D SD_PRINT_HOME_DIR` and `-D WLED_DEBUG` +- this will print all files in `/` on boot via serial + +## Configuration +### MMC +- The MMC port / pins needs no configuration as they are specified by Espressif +### SPI +- The SPI port / pins can be modified via the WLED web-UI: `Config → Usermod → SD Card` + | option | effect | default | + | ----------------- | ------------------------------------------------------------------------------------------------ | ------- | + | `pinSourceSelect` | GPIO that is connected to SD's `SS`(source select) / `CS`(chip select) | 16 | + | `pinSourceClock` | GPIO that is connected to SD's `SCLK` (source clock) / `CLK`(clock) | 14 | + | `pinPoci` | GPIO that is connected to SD's `POCI` (Peripheral-Out-Ctrl-In) / `MISO` (deprecated) | 36 | + | `pinPico` | GPIO that is connected to SD's `PICO` (Peripheral-In-Ctrl-Out) / `MOSI` (deprecated) | 15 | + | `sdEnable` | Enable to read data from the SD-card | true | + + Following new naming convention of [OSHWA](https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/) + +## Usage in other mods +- creates a macro `SD_ADAPTER` which is either mapped to `SD` or `SD_MMC` (see `SD_Test.ino` how to use SD / SD_MMC functions) + +- checks if the specified file is available on the SD card + ```cpp + bool file_onSD(const char *filepath) {...} + ``` diff --git a/usermods/sd_card/usermod_sd_card.h b/usermods/sd_card/usermod_sd_card.h new file mode 100644 index 0000000000..5dac79159c --- /dev/null +++ b/usermods/sd_card/usermod_sd_card.h @@ -0,0 +1,243 @@ +#pragma once + +#include "wled.h" + +// SD connected via MMC / SPI +#if defined(WLED_USE_SD_MMC) + #define USED_STORAGE_FILESYSTEMS "SD MMC, LittleFS" + #define SD_ADAPTER SD_MMC + #include "SD_MMC.h" +// SD connected via SPI (adjustable via usermod config) +#elif defined(WLED_USE_SD_SPI) + #define SD_ADAPTER SD + #define USED_STORAGE_FILESYSTEMS "SD SPI, LittleFS" + #include "SD.h" + #include "SPI.h" +#endif + +#ifdef WLED_USE_SD_MMC +#elif defined(WLED_USE_SD_SPI) + SPIClass spiPort = SPIClass(VSPI); +#endif + +void listDir( const char * dirname, uint8_t levels); + +class UsermodSdCard : public Usermod { + private: + bool sdInitDone = false; + + #ifdef WLED_USE_SD_SPI + int8_t configPinSourceSelect = 16; + int8_t configPinSourceClock = 14; + int8_t configPinPoci = 36; // confusing names? Then have a look :) + int8_t configPinPico = 15; // https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/ + + //acquired and initialize the SPI port + void init_SD_SPI() + { + if(!configSdEnabled) return; + if(sdInitDone) return; + + PinManagerPinType pins[5] = { + { configPinSourceSelect, true }, + { configPinSourceClock, true }, + { configPinPoci, false }, + { configPinPico, true } + }; + + if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) { + DEBUG_PRINTF("[%s] SD (SPI) pin allocation failed!\n", _name); + sdInitDone = false; + return; + } + + bool returnOfInitSD = false; + + #if defined(WLED_USE_SD_SPI) + spiPort.begin(configPinSourceClock, configPinPoci, configPinPico, configPinSourceSelect); + returnOfInitSD = SD_ADAPTER.begin(configPinSourceSelect, spiPort); + #endif + + if(!returnOfInitSD) { + DEBUG_PRINTF("[%s] SPI begin failed!\n", _name); + sdInitDone = false; + return; + } + + sdInitDone = true; + } + + //deinitialize the acquired SPI port + void deinit_SD_SPI() + { + if(!sdInitDone) return; + + SD_ADAPTER.end(); + + DEBUG_PRINTF("[%s] deallocate pins!\n", _name); + pinManager.deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard); + pinManager.deallocatePin(configPinSourceClock, PinOwner::UM_SdCard); + pinManager.deallocatePin(configPinPoci, PinOwner::UM_SdCard); + pinManager.deallocatePin(configPinPico, PinOwner::UM_SdCard); + + sdInitDone = false; + } + + // some SPI pin was changed, while SPI was initialized, reinit to new port + void reinit_SD_SPI() + { + deinit_SD_SPI(); + init_SD_SPI(); + } + #endif + + #ifdef WLED_USE_SD_MMC + void init_SD_MMC() { + if(sdInitDone) return; + bool returnOfInitSD = false; + returnOfInitSD = SD_ADAPTER.begin(); + DEBUG_PRINTF("[%s] MMC begin\n", _name); + + if(!returnOfInitSD) { + DEBUG_PRINTF("[%s] MMC begin failed!\n", _name); + sdInitDone = false; + return; + } + + sdInitDone = true; + } + #endif + + public: + static bool configSdEnabled; + static const char _name[]; + + void setup() { + DEBUG_PRINTF("[%s] usermod loaded \n", _name); + #if defined(WLED_USE_SD_SPI) + init_SD_SPI(); + #elif defined(WLED_USE_SD_MMC) + init_SD_MMC(); + #endif + + #if defined(SD_ADAPTER) && defined(SD_PRINT_HOME_DIR) + listDir("/", 0); + #endif + } + + void loop(){ + + } + + uint16_t getId() + { + return USERMOD_ID_SD_CARD; + } + + void addToConfig(JsonObject& root) + { + #ifdef WLED_USE_SD_SPI + JsonObject top = root.createNestedObject(FPSTR(_name)); + top["pinSourceSelect"] = configPinSourceSelect; + top["pinSourceClock"] = configPinSourceClock; + top["pinPoci"] = configPinPoci; + top["pinPico"] = configPinPico; + top["sdEnabled"] = configSdEnabled; + #endif + } + + bool readFromConfig(JsonObject &root) + { + #ifdef WLED_USE_SD_SPI + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); + return false; + } + + uint8_t oldPinSourceSelect = configPinSourceSelect; + uint8_t oldPinSourceClock = configPinSourceClock; + uint8_t oldPinPoci = configPinPoci; + uint8_t oldPinPico = configPinPico; + bool oldSdEnabled = configSdEnabled; + + getJsonValue(top["pinSourceSelect"], configPinSourceSelect); + getJsonValue(top["pinSourceClock"], configPinSourceClock); + getJsonValue(top["pinPoci"], configPinPoci); + getJsonValue(top["pinPico"], configPinPico); + getJsonValue(top["sdEnabled"], configSdEnabled); + + if(configSdEnabled != oldSdEnabled) { + configSdEnabled ? init_SD_SPI() : deinit_SD_SPI(); + DEBUG_PRINTF("[%s] SD card %s\n", _name, configSdEnabled ? "enabled" : "disabled"); + } + + if( configSdEnabled && ( + oldPinSourceSelect != configPinSourceSelect || + oldPinSourceClock != configPinSourceClock || + oldPinPoci != configPinPoci || + oldPinPico != configPinPico) + ) + { + DEBUG_PRINTF("[%s] Init SD card based of config\n", _name); + DEBUG_PRINTF("[%s] Config changes \n - SS: %d -> %d\n - MI: %d -> %d\n - MO: %d -> %d\n - En: %d -> %d\n", _name, oldPinSourceSelect, configPinSourceSelect, oldPinSourceClock, configPinSourceClock, oldPinPoci, configPinPoci, oldPinPico, configPinPico); + reinit_SD_SPI(); + } + #endif + + return true; + } +}; + +const char UsermodSdCard::_name[] PROGMEM = "SD Card"; +bool UsermodSdCard::configSdEnabled = true; + +#ifdef SD_ADAPTER +//checks if the file is available on SD card +bool file_onSD(const char *filepath) +{ + #ifdef WLED_USE_SD_SPI + if(!UsermodSdCard::configSdEnabled) return false; + #endif + + uint8_t cardType = SD_ADAPTER.cardType(); + if(cardType == CARD_NONE) { + DEBUG_PRINTF("[%s] not attached / cardType none\n", UsermodSdCard::_name); + return false; // no SD card attached + } + if(cardType == CARD_MMC || cardType == CARD_SD || cardType == CARD_SDHC) + { + return SD_ADAPTER.exists(filepath); + } + + return false; // unknown card type +} + +void listDir( const char * dirname, uint8_t levels){ + DEBUG_PRINTF("Listing directory: %s\n", dirname); + + File root = SD_ADAPTER.open(dirname); + if(!root){ + DEBUG_PRINTF("Failed to open directory\n"); + return; + } + if(!root.isDirectory()){ + DEBUG_PRINTF("Not a directory\n"); + return; + } + + File file = root.openNextFile(); + while(file){ + if(file.isDirectory()){ + DEBUG_PRINTF(" DIR : %s\n",file.name()); + if(levels){ + listDir(file.name(), levels -1); + } + } else { + DEBUG_PRINTF(" FILE: %s SIZE: %d\n",file.name(), file.size()); + } + file = root.openNextFile(); + } +} + +#endif \ No newline at end of file diff --git a/usermods/sensors_to_mqtt/readme.md b/usermods/sensors_to_mqtt/readme.md index 7f2d64071a..d427d3e144 100644 --- a/usermods/sensors_to_mqtt/readme.md +++ b/usermods/sensors_to_mqtt/readme.md @@ -1,12 +1,12 @@ -# Sensors To Home Assistant (or mqtt) +# Send sensor data To Home Assistant -This usermod will publish values of the BMP280, CCS811 and Si7021 sensors to Home Assistant via MQTT. +Publishes BMP280, CCS811 and Si7021 measurements to Home Assistant via MQTT. -Its using home assistant automatic device discovery feature. +Uses Home Assistant Automatic Device Discovery. -The use of Home Assistant is not mandatory; it will publish the sensor values via MQTT just fine without it. +The use of Home Assistant is not mandatory. The mod will publish sensor values via MQTT just fine without it. -Its resusing the mqtt connection set in the WLED web user interface. +Uses the MQTT connection set in the WLED web user interface. ## Maintainer @@ -15,12 +15,12 @@ twitter.com/mpronk89 ## Features - Reads BMP280, CCS811 and Si7021 senors -- Publishes via MQTT, configured via webui of wled +- Publishes via MQTT, configured via WLED webUI - Announces device in Home Assistant for easy setup - Efficient energy usage - Updates every 60 seconds -## Example mqtt topics: +## Example MQTT topics: `$mqttDeviceTopic` is set in webui of WLED! @@ -40,7 +40,7 @@ IAQ: $mqttDeviceTopic/iaq ### Requirements 1. BMP280/CCS811/Si7021 sensor. E.g. https://aliexpress.com/item/32979998543.html -2. A microcontroller which can talk i2c, e.g. esp32 +2. A microcontroller that supports i2c. e.g. esp32 ### installation @@ -77,7 +77,7 @@ SDA_PIN = 4; adafruit/Adafruit Si7021 Library @ 1.4.0 ``` -The #ifdefs in `usermods_list.cpp` should do the rest :) +The #ifdefs in `usermods_list.cpp` should do the rest # Credits diff --git a/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h b/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h index dd7aedc1f0..4f51750ac7 100644 --- a/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h +++ b/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h @@ -1,8 +1,11 @@ +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + #pragma once #include "wled.h" #include -#include #include #include #include @@ -12,14 +15,6 @@ Adafruit_BMP280 bmp; Adafruit_Si7021 si7021; Adafruit_CCS811 ccs811; -#ifdef ARDUINO_ARCH_ESP32 //ESP32 boards -uint8_t SCL_PIN = 22; -uint8_t SDA_PIN = 21; -#else //ESP8266 boards -uint8_t SCL_PIN = 5; -uint8_t SDA_PIN = 4; -#endif - class UserMod_SensorsToMQTT : public Usermod { private: @@ -227,7 +222,6 @@ class UserMod_SensorsToMQTT : public Usermod void setup() { Serial.println("Starting!"); - Wire.begin(SDA_PIN, SCL_PIN); Serial.println("Initializing sensors.. "); _initialize(); } diff --git a/usermods/seven_segment_display/readme.md b/usermods/seven_segment_display/readme.md index ca52383d69..792393a831 100644 --- a/usermods/seven_segment_display/readme.md +++ b/usermods/seven_segment_display/readme.md @@ -1,25 +1,25 @@ # Seven Segment Display -Usermod that uses the overlay feature to create a configurable seven segment display. -This has only been tested on a single configuration. Colon support is entirely untested. +Uses the overlay feature to create a configurable seven segment display. +This has only been tested on a single configuration. Colon support has _not_ been tested. ## Installation Add the compile-time option `-D USERMOD_SEVEN_SEGMENT` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_SEVEN_SEGMENT` in `my_config.h`. ## Settings -Settings can be controlled through both the usermod setting page and through MQTT with a raw payload. +Settings can be controlled via both the usermod setting page and through MQTT with a raw payload. ##### Example Topic ```/sevenSeg/perSegment/set``` Payload ```3``` #### perSegment -- ssLEDPerSegment -The number of individual LEDs per segment. There are 7 segments per digit. +The number of individual LEDs per segment. 7 segments per digit. #### perPeriod -- ssLEDPerPeriod -The number of individual LEDs per period. A ':' has 2x periods. +The number of individual LEDs per period. A ':' (colon) has two periods. #### startIdx -- ssStartLED -Index of the LED that the display starts at. Allows a seven segment display to be in the middle of a string. +Index of the LED the display starts at. Enables a seven segment display to be in the middle of a string. #### timeEnable -- ssTimeEnabled -When true, when displayMask is configured for a time output and no message is set the time will be displayed. +When true, when displayMask is configured for a time output and no message is set, the time will be displayed. #### scrollSpd -- ssScrollSpeed Time, in milliseconds, between message shifts when the length of displayMsg exceeds the length of the displayMask. #### displayMask -- ssDisplayMask @@ -35,9 +35,9 @@ All others for alpha numeric, (will be blank when displaying time) ```HHMMSS ``` ```hh:MM:SS ``` #### displayMsg -- ssDisplayMessage -Message to be displayed across the display. If the length exceeds the length of the displayMask the message will scroll at scrollSpd. To 'remove' a message or revert back to time, if timeEnabled is true, set the message to '~'. +Message to be displayed. If the message length exceeds the length of displayMask, the message will scroll at scrollSpd. To 'remove' a message or revert back to time, if timeEnabled is true, set the message to '~'. #### displayCfg -- ssDisplayConfig -The order that your LEDs are configured. All seven segments in the display need to be wired the same way. +The order your LEDs are configured in. All segments in the display need to be wired the same way.
            -------
          /   A   /          0 - EDCGFAB
diff --git a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h b/usermods/seven_segment_display/usermod_v2_seven_segment_display.h
index 5c0022e027..20fef15df5 100644
--- a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h
+++ b/usermods/seven_segment_display/usermod_v2_seven_segment_display.h
@@ -1,3 +1,7 @@
+#ifndef WLED_ENABLE_MQTT
+#error "This user mod requires MQTT to be enabled."
+#endif
+
 #pragma once
 
 #include "wled.h"
@@ -405,7 +409,7 @@ class SevenSegmentDisplay : public Usermod
 
     if (mqttGroupTopic[0] != 0)
     {
-      //subcribe for sevenseg messages on the group topic
+      //subscribe for sevenseg messages on the group topic
       sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttGroupTopic, _str_sevenSeg);
       mqtt->subscribe(subBuffer, 2);
     }
@@ -413,7 +417,7 @@ class SevenSegmentDisplay : public Usermod
 
   bool onMqttMessage(char *topic, char *payload)
   {
-    //If topic beings iwth sevenSeg cut it off, otherwise not our message.
+    //If topic beings with sevenSeg cut it off, otherwise not our message.
     size_t topicPrefixLen = strlen_P(PSTR("/sevenSeg/"));
     if (strncmp_P(topic, PSTR("/sevenSeg/"), topicPrefixLen) == 0)
       topic += topicPrefixLen;
diff --git a/usermods/seven_segment_display_reloaded/readme.md b/usermods/seven_segment_display_reloaded/readme.md
index 09479754c1..a3398c3e59 100644
--- a/usermods/seven_segment_display_reloaded/readme.md
+++ b/usermods/seven_segment_display_reloaded/readme.md
@@ -1,6 +1,6 @@
 # Seven Segment Display Reloaded
 
-Usermod that uses the overlay feature to create a configurable seven segment display.
+Uses the overlay feature to create a configurable seven segment display.
 Optimized for maximum configurability and use with seven segment clocks by parallyze (https://www.instructables.com/member/parallyze/instructables/)
 Very loosely based on the existing usermod "seven segment display".
 
@@ -12,26 +12,29 @@ Add the compile-time option `-D USERMOD_SSDR` to your `platformio.ini` (or `plat
 For the auto brightness option, the usermod SN_Photoresistor has to be installed as well. See SN_Photoresistor/readme.md for instructions.
 
 ## Settings
-All settings can be controlled the usermod setting page.
+All settings can be controlled via the usermod settings page.
 Part of the settings can be controlled through MQTT with a raw payload or through a json request to /json/state.
 
 ### enabled
-Enables/disables this overlay usermod
+Enables/disables this usermod
 
 ### inverted
-Enables the inverted mode in which the background should be enabled and the digits should be black (leds off)
+Enables the inverted mode in which the background should be enabled and the digits should be black (LEDs off)
 
 ### Colon-blinking
 Enables the blinking colon(s) if they are defined
 
+### Leading-Zero
+Shows the leading zero of the hour if it exists (i.e. shows `07` instead of `7`)
+
 ### enable-auto-brightness
-Enables the auto brightness feature. Can be only used with the usermod SN_Photoresistor installed.
+Enables the auto brightness feature. Can be used only when the usermod SN_Photoresistor is installed.
 
 ### auto-brightness-min / auto-brightness-max
 The lux value calculated from usermod SN_Photoresistor will be mapped to the values defined here.
-The mapping is 0 - 1000 lux will be mapped to auto-brightness-min - auto-brightness-max
+The mapping, 0 - 1000 lux, will be mapped to auto-brightness-min and auto-brightness-max
 
-The mA current protection of WLED will override the calculated value if it is too high.
+WLED current protection will override the calculated value if it is too high.
 
 ### Display-Mask
 Defines the type of the time/date display. 
@@ -61,7 +64,7 @@ See following example for usage.
 
 ## Example
 
-Example for Leds definition
+Example of an LED definition:
 ```
   <  A  >
 /\       /\
@@ -74,15 +77,15 @@ E        C
   <  D  >
 ```
 
-Leds or Range of Leds are seperated by a comma ","
+LEDs or Range of LEDs are separated by a comma ","
 
-Segments are seperated by a semicolon ";" and are read as A;B;C;D;E;F;G
+Segments are separated by a semicolon ";" and are read as A;B;C;D;E;F;G
 
-Digits are seperated by colon ":" -> A;B;C;D;E;F;G:A;B;C;D;E;F;G
+Digits are separated by colon ":" -> A;B;C;D;E;F;G:A;B;C;D;E;F;G
 
 Ranges are defined as lower to higher (lower first)
 
-For example, an clock definition for the following clock (https://www.instructables.com/Lazy-7-Quick-Build-Edition/) is
+For example, a clock definition for the following clock (https://www.instructables.com/Lazy-7-Quick-Build-Edition/) is
 
 - hour "59,46;47-48;50-51;52-53;54-55;57-58;49,56:0,13;1-2;4-5;6-7;8-9;11-12;3,10"
 
@@ -96,18 +99,18 @@ or
 
 depending on the orientation.
 
-# The example detailed:
+# Example details:
 hour "59,46;47-48;50-51;52-53;54-55;57-58;49,56:0,13;1-2;4-5;6-7;8-9;11-12;3,10"
 
-there are two digits seperated by ":"
+there are two digits separated by ":"
 
 - 59,46;47-48;50-51;52-53;54-55;57-58;49,56
 - 0,13;1-2;4-5;6-7;8-9;11-12;3,10
 
 In the first digit, 
-the **segment A** consists of the leds number **59 and 46**., **segment B** consists of the leds number **47, 48** and so on
+the **segment A** consists of the LEDs number **59 and 46**., **segment B** consists of the LEDs number **47, 48** and so on
 
-The second digit starts again with **segment A** and leds **0 and 13**, **segment B** consists of the leds number **1 and 2** and so on
+The second digit starts again with **segment A** and LEDs **0 and 13**, **segment B** consists of the LEDs number **1 and 2** and so on
 
 ### first digit of the hour
 - Segment A: 59, 46
@@ -126,4 +129,4 @@ The second digit starts again with **segment A** and leds **0 and 13**, **segmen
 - Segment D: 6, 7
 - Segment E: 8, 9
 - Segment F: 11, 12
-- Segment G: 3, 10
\ No newline at end of file
+- Segment G: 3, 10
diff --git a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h
index 6274abceec..5c2fac0d4d 100644
--- a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h
+++ b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h
@@ -1,3 +1,7 @@
+#ifndef WLED_ENABLE_MQTT
+#error "This user mod requires MQTT to be enabled."
+#endif
+
 #pragma once
 
 #include "wled.h"
@@ -13,6 +17,7 @@ class UsermodSSDR : public Usermod {
   bool umSSDRDisplayTime = false;
   bool umSSDRInverted = false;
   bool umSSDRColonblink = true;
+  bool umSSDRLeadingZero = false;
   bool umSSDREnableLDR = false;
   String umSSDRHours = "";
   String umSSDRMinutes = "";
@@ -75,6 +80,7 @@ class UsermodSSDR : public Usermod {
   static const char _str_timeEnabled[];
   static const char _str_inverted[];
   static const char _str_colonblink[];
+  static const char _str_leadingZero[];
   static const char _str_displayMask[];
   static const char _str_hours[];
   static const char _str_minutes[];
@@ -101,15 +107,15 @@ class UsermodSSDR : public Usermod {
       switch (umSSDRDisplayMask[index]) {
         case 'h':
           timeVar = hourFormat12(localTime);
-          _showElements(&umSSDRHours, timeVar, 0, 1);
+          _showElements(&umSSDRHours, timeVar, 0, !umSSDRLeadingZero);
           break;
         case 'H':
           timeVar = hour(localTime);
-          _showElements(&umSSDRHours, timeVar, 0, 1);
+          _showElements(&umSSDRHours, timeVar, 0, !umSSDRLeadingZero);
           break;
         case 'k':
           timeVar = hour(localTime) + 1;
-          _showElements(&umSSDRHours, timeVar, 0, 0);
+          _showElements(&umSSDRHours, timeVar, 0, !umSSDRLeadingZero);
           break;
         case 'm':
           timeVar = minute(localTime);
@@ -305,6 +311,9 @@ class UsermodSSDR : public Usermod {
     if (_cmpIntSetting_P(topic, payload, _str_colonblink, &umSSDRColonblink)) {
       return true;
     }
+    if (_cmpIntSetting_P(topic, payload, _str_leadingZero, &umSSDRLeadingZero)) {
+      return true;
+    }
     if (strcmp_P(topic, _str_displayMask) == 0) {
       umSSDRDisplayMask = String(payload);
       _publishMQTTstr_P(_str_displayMask, umSSDRDisplayMask);
@@ -319,6 +328,7 @@ class UsermodSSDR : public Usermod {
     _publishMQTTint_P(_str_ldrEnabled, umSSDREnableLDR);
     _publishMQTTint_P(_str_inverted, umSSDRInverted);
     _publishMQTTint_P(_str_colonblink, umSSDRColonblink);
+    _publishMQTTint_P(_str_leadingZero, umSSDRLeadingZero);
 
     _publishMQTTstr_P(_str_hours, umSSDRHours);
     _publishMQTTstr_P(_str_minutes, umSSDRMinutes);
@@ -343,6 +353,7 @@ class UsermodSSDR : public Usermod {
     ssdrObj[FPSTR(_str_ldrEnabled)] = umSSDREnableLDR;
     ssdrObj[FPSTR(_str_inverted)] = umSSDRInverted;
     ssdrObj[FPSTR(_str_colonblink)] = umSSDRColonblink;
+    ssdrObj[FPSTR(_str_leadingZero)] = umSSDRLeadingZero;
     ssdrObj[FPSTR(_str_displayMask)] = umSSDRDisplayMask;
     ssdrObj[FPSTR(_str_hours)] = umSSDRHours;
     ssdrObj[FPSTR(_str_minutes)] = umSSDRMinutes;
@@ -384,7 +395,7 @@ class UsermodSSDR : public Usermod {
     if (!umSSDRDisplayTime || strip.isUpdating()) {
       return;
     }
-    #ifdef USERMOD_ID_SN_PHOTORESISTOR
+    #ifdef USERMOD_SN_PHOTORESISTOR
       if(bri != 0 && umSSDREnableLDR && (millis() - umSSDRLastRefresh > umSSDRResfreshTime)) {
         if (ptr != nullptr) {
           uint16_t lux = ptr->getLastLDRValue();
@@ -421,6 +432,8 @@ class UsermodSSDR : public Usermod {
     invert.add(umSSDRInverted);
     JsonArray blink = user.createNestedArray("Blinking colon");
     blink.add(umSSDRColonblink);
+    JsonArray zero = user.createNestedArray("Show the hour leading zero");
+    zero.add(umSSDRLeadingZero);
     JsonArray ldrEnable = user.createNestedArray("Auto Brightness enabled");
     ldrEnable.add(umSSDREnableLDR);
 
@@ -450,6 +463,7 @@ class UsermodSSDR : public Usermod {
       umSSDREnableLDR = ssdrObj[FPSTR(_str_ldrEnabled)] | umSSDREnableLDR;
       umSSDRInverted = ssdrObj[FPSTR(_str_inverted)] | umSSDRInverted;
       umSSDRColonblink = ssdrObj[FPSTR(_str_colonblink)] | umSSDRColonblink;
+      umSSDRLeadingZero = ssdrObj[FPSTR(_str_leadingZero)] | umSSDRLeadingZero;
       umSSDRDisplayMask = ssdrObj[FPSTR(_str_displayMask)] | umSSDRDisplayMask;
     }
   }
@@ -466,14 +480,14 @@ class UsermodSSDR : public Usermod {
 
     if (mqttGroupTopic[0] != 0)
     {
-      //subcribe for sevenseg messages on the group topic
+      //subscribe for sevenseg messages on the group topic
       sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttGroupTopic, _str_name);
       mqtt->subscribe(subBuffer, 2);
     }
   }
 
   bool onMqttMessage(char *topic, char *payload) {
-    //If topic beings iwth sevenSeg cut it off, otherwise not our message.
+    //If topic begins with sevenSeg cut it off, otherwise not our message.
     size_t topicPrefixLen = strlen_P(PSTR("/wledSS/"));
     if (strncmp_P(topic, PSTR("/wledSS/"), topicPrefixLen) == 0) {
       topic += topicPrefixLen;
@@ -512,6 +526,7 @@ class UsermodSSDR : public Usermod {
     umSSDREnableLDR        = (top[FPSTR(_str_ldrEnabled)] | umSSDREnableLDR);
     umSSDRInverted         = (top[FPSTR(_str_inverted)] | umSSDRInverted);
     umSSDRColonblink       = (top[FPSTR(_str_colonblink)] | umSSDRColonblink);
+    umSSDRLeadingZero      = (top[FPSTR(_str_leadingZero)] | umSSDRLeadingZero);
 
     umSSDRDisplayMask      = top[FPSTR(_str_displayMask)] | umSSDRDisplayMask;
     umSSDRHours            = top[FPSTR(_str_hours)] | umSSDRHours;
@@ -542,6 +557,7 @@ const char UsermodSSDR::_str_name[]        PROGMEM = "UsermodSSDR";
 const char UsermodSSDR::_str_timeEnabled[] PROGMEM = "enabled";
 const char UsermodSSDR::_str_inverted[]    PROGMEM = "inverted";
 const char UsermodSSDR::_str_colonblink[]  PROGMEM = "Colon-blinking";
+const char UsermodSSDR::_str_leadingZero[] PROGMEM = "Leading-Zero";
 const char UsermodSSDR::_str_displayMask[] PROGMEM = "Display-Mask";
 const char UsermodSSDR::_str_hours[]       PROGMEM = "LED-Numbers-Hours";
 const char UsermodSSDR::_str_minutes[]     PROGMEM = "LED-Numbers-Minutes";
diff --git a/usermods/sht/readme.md b/usermods/sht/readme.md
new file mode 100644
index 0000000000..0337805b3a
--- /dev/null
+++ b/usermods/sht/readme.md
@@ -0,0 +1,56 @@
+# SHT
+Usermod to support various SHT i2c sensors like the SHT30, SHT31, SHT35 and SHT85
+
+## Requirements
+* "SHT85" by Rob Tillaart, v0.2 or higher: https://github.com/RobTillaart/SHT85
+
+## Usermod installation
+Simply copy the below block (build task) to your `platformio_override.ini` and compile WLED using this new build task. Or use an existing one, add the buildflag `-D USERMOD_SHT` and the below library dependencies.
+
+ESP32:
+```
+[env:custom_esp32dev_usermod_sht]
+extends = env:esp32dev
+build_flags = ${common.build_flags_esp32}
+  -D USERMOD_SHT
+lib_deps = ${esp32.lib_deps}
+    robtillaart/SHT85@~0.3.3
+```
+
+ESP8266:
+```
+[env:custom_d1_mini_usermod_sht]
+extends = env:d1_mini
+build_flags = ${common.build_flags_esp8266}
+  -D USERMOD_SHT
+lib_deps = ${esp8266.lib_deps}
+    robtillaart/SHT85@~0.3.3
+```
+
+## MQTT Discovery for Home Assistant
+If you're using Home Assistant and want to have the temperature and humidity available as entities in HA, you can tick the "Add-To-Home-Assistant-MQTT-Discovery" option in the usermod settings. If you have an MQTT broker configured under "Sync Settings" and it is connected, the mod will publish the auto discovery message to your broker and HA will instantly find it and create an entity each for the temperature and humidity.
+
+### Publishing readings via MQTT
+Regardless of having MQTT discovery ticked or not, the mod will always report temperature and humidity to the WLED MQTT topic of that instance, if you have a broker configured and it's connected.
+
+## Configuration
+Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D USERMOD_SHT`, you will see the config for it there:
+* SHT-Type:
+  * What it does: Select the SHT sensor type you want to use
+  * Possible values: SHT30, SHT31, SHT35, SHT85
+  * Default: SHT30
+* Unit:
+  * What it does: Select which unit should be used to display the temperature in the info section. Also used when sending via MQTT discovery, see below.
+  * Possible values: Celsius, Fahrenheit
+  * Default: Celsius
+* Add-To-HA-MQTT-Discovery:
+  * What it does: Makes the temperature and humidity available via MQTT discovery, so they're automatically added to Home Assistant, because that way it's typesafe.
+  * Possible values: Enabled/Disabled
+  * Default: Disabled
+
+## Change log
+2022-12
+* First implementation.
+
+## Credits
+ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG
diff --git a/usermods/sht/usermod_sht.h b/usermods/sht/usermod_sht.h
new file mode 100644
index 0000000000..c6e17221be
--- /dev/null
+++ b/usermods/sht/usermod_sht.h
@@ -0,0 +1,480 @@
+#ifndef WLED_ENABLE_MQTT
+#error "This user mod requires MQTT to be enabled."
+#endif
+
+#pragma once
+
+#include "SHT85.h"
+
+#define USERMOD_SHT_TYPE_SHT30 0
+#define USERMOD_SHT_TYPE_SHT31 1
+#define USERMOD_SHT_TYPE_SHT35 2
+#define USERMOD_SHT_TYPE_SHT85 3
+
+class ShtUsermod : public Usermod
+{
+  private:
+    bool enabled = false; // Is usermod enabled or not
+    bool firstRunDone = false; // Remembers if the first config load run had been done
+    bool initDone = false; // Remembers if the mod has been completely initialised
+    bool haMqttDiscovery = false; // Is MQTT discovery enabled or not
+    bool haMqttDiscoveryDone = false; // Remembers if we already published the HA discovery topics
+
+    // SHT vars
+    SHT *shtTempHumidSensor = nullptr; // Instance of SHT lib
+    byte shtType = 0; // SHT sensor type to be used. Default: SHT30
+    byte unitOfTemp = 0; // Temperature unit to be used. Default: Celsius (0 = Celsius, 1 = Fahrenheit)
+    bool shtInitDone = false; // Remembers if SHT sensor has been initialised
+    bool shtReadDataSuccess = false; // Did we have a successful data read and is a valid temperature and humidity available?
+    const byte shtI2cAddress = 0x44; // i2c address of the sensor. 0x44 is the default for all SHT sensors. Change this, if needed
+    unsigned long shtLastTimeUpdated = 0; // Remembers when we read data the last time
+    bool shtDataRequested = false; // Reading data is done async. This remembers if we asked the sensor to read data
+    float shtCurrentTempC = 0.0f; // Last read temperature in Celsius
+    float shtCurrentHumidity = 0.0f; // Last read humidity in RH%
+
+
+    void initShtTempHumiditySensor();
+    void cleanupShtTempHumiditySensor();
+    void cleanup();
+    inline bool isShtReady() { return shtInitDone; } // Checks if the SHT sensor has been initialised.
+
+    void publishTemperatureAndHumidityViaMqtt();
+    void publishHomeAssistantAutodiscovery();
+    void appendDeviceToMqttDiscoveryMessage(JsonDocument& root);
+
+  public:
+    // Strings to reduce flash memory usage (used more than twice)
+    static const char _name[];
+    static const char _enabled[];
+    static const char _shtType[];
+    static const char _unitOfTemp[];
+    static const char _haMqttDiscovery[];
+
+    void setup();
+    void loop();
+    void onMqttConnect(bool sessionPresent);
+    void appendConfigData();
+    void addToConfig(JsonObject &root);
+    bool readFromConfig(JsonObject &root);
+    void addToJsonInfo(JsonObject& root);
+
+    bool isEnabled() { return enabled; }
+
+    float getTemperature();
+    float getTemperatureC() { return roundf(shtCurrentTempC * 10.0f) / 10.0f; }
+    float getTemperatureF() { return (getTemperatureC() * 1.8f) + 32.0f; }
+    float getHumidity() { return roundf(shtCurrentHumidity * 10.0f) / 10.0f; }
+    const char* getUnitString();
+
+    uint16_t getId() { return USERMOD_ID_SHT; }
+};
+
+// Strings to reduce flash memory usage (used more than twice)
+const char ShtUsermod::_name[]            PROGMEM = "SHT-Sensor";
+const char ShtUsermod::_enabled[]         PROGMEM = "Enabled";
+const char ShtUsermod::_shtType[]         PROGMEM = "SHT-Type";
+const char ShtUsermod::_unitOfTemp[]      PROGMEM = "Unit";
+const char ShtUsermod::_haMqttDiscovery[] PROGMEM = "Add-To-HA-MQTT-Discovery";
+
+/**
+ * Initialise SHT sensor.
+ *
+ * Using the correct constructor according to config and initialises it using the
+ * global i2c pins.
+ *
+ * @return void
+ */
+void ShtUsermod::initShtTempHumiditySensor()
+{
+  switch (shtType) {
+    case USERMOD_SHT_TYPE_SHT30: shtTempHumidSensor = (SHT *) new SHT30(); break;
+    case USERMOD_SHT_TYPE_SHT31: shtTempHumidSensor = (SHT *) new SHT31(); break;
+    case USERMOD_SHT_TYPE_SHT35: shtTempHumidSensor = (SHT *) new SHT35(); break;
+    case USERMOD_SHT_TYPE_SHT85: shtTempHumidSensor = (SHT *) new SHT85(); break;
+  }
+
+  shtTempHumidSensor->begin(shtI2cAddress); // uses &Wire
+  if (shtTempHumidSensor->readStatus() == 0xFFFF) {
+    DEBUG_PRINTF("[%s] SHT init failed!\n", _name);
+    cleanup();
+    return;
+  }
+
+  shtInitDone = true;
+}
+
+/**
+ * Cleanup the SHT sensor.
+ *
+ * Properly calls "reset" for the sensor then releases it from memory.
+ *
+ * @return void
+ */
+void ShtUsermod::cleanupShtTempHumiditySensor()
+{
+  if (isShtReady()) {
+    shtTempHumidSensor->reset();
+    delete shtTempHumidSensor;
+    shtTempHumidSensor = nullptr;
+  }
+  shtInitDone = false;
+}
+
+/**
+ * Cleanup the mod completely.
+ *
+ * Calls ::cleanupShtTempHumiditySensor() to cleanup the SHT sensor and
+ * deallocates pins.
+ *
+ * @return void
+ */
+void ShtUsermod::cleanup()
+{
+  cleanupShtTempHumiditySensor();
+  enabled = false;
+}
+
+/**
+ * Publish temperature and humidity to WLED device topic.
+ *
+ * Will add a "/temperature" and "/humidity" topic to the WLED device topic.
+ * Temperature will be written in configured unit.
+ *
+ * @return void
+ */
+void ShtUsermod::publishTemperatureAndHumidityViaMqtt() {
+  if (!WLED_MQTT_CONNECTED) return;
+  char buf[128];
+
+  snprintf_P(buf, 127, PSTR("%s/temperature"), mqttDeviceTopic);
+  mqtt->publish(buf, 0, false, String(getTemperature()).c_str());
+  snprintf_P(buf, 127, PSTR("%s/humidity"), mqttDeviceTopic);
+  mqtt->publish(buf, 0, false, String(getHumidity()).c_str());
+}
+
+/**
+ * If enabled, publishes HA MQTT device discovery topics.
+ *
+ * Will make Home Assistant add temperature and humidity as entities automatically.
+ *
+ * Note: Whenever usermods are part of the WLED integration in HA, this can be dropped.
+ *
+ * @return void
+ */
+void ShtUsermod::publishHomeAssistantAutodiscovery() {
+  if (!WLED_MQTT_CONNECTED) return;
+
+  char json_str[1024], buf[128];
+  size_t payload_size;
+  StaticJsonDocument<1024> json;
+
+  snprintf_P(buf, 127, PSTR("%s Temperature"), serverDescription);
+  json[F("name")] = buf;
+  snprintf_P(buf, 127, PSTR("%s/temperature"), mqttDeviceTopic);
+  json[F("stat_t")] = buf;
+  json[F("dev_cla")] = F("temperature");
+  json[F("stat_cla")] = F("measurement");
+  snprintf_P(buf, 127, PSTR("%s-temperature"), escapedMac.c_str());
+  json[F("uniq_id")] = buf;
+  json[F("unit_of_meas")] = unitOfTemp ? F("°F") : F("°C");
+  appendDeviceToMqttDiscoveryMessage(json);
+  payload_size = serializeJson(json, json_str);
+  snprintf_P(buf, 127, PSTR("homeassistant/sensor/%s/%s-temperature/config"), escapedMac.c_str(), escapedMac.c_str());
+  mqtt->publish(buf, 0, true, json_str, payload_size);
+
+  json.clear();
+
+  snprintf_P(buf, 127, PSTR("%s Humidity"), serverDescription);
+  json[F("name")] = buf;
+  snprintf_P(buf, 127, PSTR("%s/humidity"), mqttDeviceTopic);
+  json[F("stat_t")] = buf;
+  json[F("dev_cla")] = F("humidity");
+  json[F("stat_cla")] = F("measurement");
+  snprintf_P(buf, 127, PSTR("%s-humidity"), escapedMac.c_str());
+  json[F("uniq_id")] = buf;
+  json[F("unit_of_meas")] = F("%");
+  appendDeviceToMqttDiscoveryMessage(json);
+  payload_size = serializeJson(json, json_str);
+  snprintf_P(buf, 127, PSTR("homeassistant/sensor/%s/%s-humidity/config"), escapedMac.c_str(), escapedMac.c_str());
+  mqtt->publish(buf, 0, true, json_str, payload_size);
+
+  haMqttDiscoveryDone = true;
+}
+
+/**
+ * Helper to add device information to MQTT discovery topic.
+ *
+ * @return void
+ */
+void ShtUsermod::appendDeviceToMqttDiscoveryMessage(JsonDocument& root) {
+  JsonObject device = root.createNestedObject(F("dev"));
+  device[F("ids")] = escapedMac.c_str();
+  device[F("name")] = serverDescription;
+  device[F("sw")] = versionString;
+  device[F("mdl")] = ESP.getChipModel();
+  device[F("mf")] = F("espressif");
+}
+
+/**
+ * Setup the mod.
+ *
+ * Allocates i2c pins as PinOwner::HW_I2C, so they can be allocated multiple times.
+ * And calls ::initShtTempHumiditySensor() to initialise the sensor.
+ *
+ * @see Usermod::setup()
+ * @see UsermodManager::setup()
+ *
+ * @return void
+ */
+void ShtUsermod::setup()
+{
+  if (enabled) {
+    // GPIOs can be set to -1 , so check they're gt zero
+    if (i2c_sda < 0 || i2c_scl < 0) {
+      DEBUG_PRINTF("[%s] I2C bus not initialised!\n", _name);
+      cleanup();
+      return;
+    }
+
+    initShtTempHumiditySensor();
+
+    initDone = true;
+  }
+
+  firstRunDone = true;
+}
+
+/**
+ * Actually reading data (async) from the sensor every 30 seconds.
+ *
+ * If last reading is at least 30 seconds, it will trigger a reading using
+ * SHT::requestData(). We will then continiously check SHT::dataReady() if
+ * data is ready to be read. If so, it's read, stored locally and published
+ * via MQTT.
+ *
+ * @see Usermod::loop()
+ * @see UsermodManager::loop()
+ *
+ * @return void
+ */
+void ShtUsermod::loop()
+{
+  if (!enabled || !initDone || strip.isUpdating()) return;
+
+  if (isShtReady()) {
+    if (millis() - shtLastTimeUpdated > 30000 && !shtDataRequested) {
+      shtTempHumidSensor->requestData();
+      shtDataRequested = true;
+
+      shtLastTimeUpdated = millis();
+    }
+
+    if (shtDataRequested) {
+      if (shtTempHumidSensor->dataReady()) {
+        if (shtTempHumidSensor->readData(false)) {
+          shtCurrentTempC = shtTempHumidSensor->getTemperature();
+          shtCurrentHumidity = shtTempHumidSensor->getHumidity();
+
+          publishTemperatureAndHumidityViaMqtt();
+          shtReadDataSuccess = true;
+        } else {
+          shtReadDataSuccess = false;
+        }
+
+        shtDataRequested = false;
+      }
+    }
+  }
+}
+
+/**
+ * Whenever MQTT is connected, publish HA autodiscovery topics.
+ *
+ * Is only done once.
+ *
+ * @see Usermod::onMqttConnect()
+ * @see UsermodManager::onMqttConnect()
+ *
+ * @return void
+ */
+void ShtUsermod::onMqttConnect(bool sessionPresent) {
+  if (haMqttDiscovery && !haMqttDiscoveryDone) publishHomeAssistantAutodiscovery();
+}
+
+/**
+ * Add dropdown for sensor type and unit to UM config page.
+ *
+ * @see Usermod::appendConfigData()
+ * @see UsermodManager::appendConfigData()
+ *
+ * @return void
+ */
+void ShtUsermod::appendConfigData() {
+  oappend(SET_F("dd=addDropdown('"));
+  oappend(_name);
+  oappend(SET_F("','"));
+  oappend(_shtType);
+  oappend(SET_F("');"));
+  oappend(SET_F("addOption(dd,'SHT30',0);"));
+  oappend(SET_F("addOption(dd,'SHT31',1);"));
+  oappend(SET_F("addOption(dd,'SHT35',2);"));
+  oappend(SET_F("addOption(dd,'SHT85',3);"));
+  oappend(SET_F("dd=addDropdown('"));
+  oappend(_name);
+  oappend(SET_F("','"));
+  oappend(_unitOfTemp);
+  oappend(SET_F("');"));
+  oappend(SET_F("addOption(dd,'Celsius',0);"));
+  oappend(SET_F("addOption(dd,'Fahrenheit',1);"));
+}
+
+/**
+ * Add config data to be stored in cfg.json.
+ *
+ * @see Usermod::addToConfig()
+ * @see UsermodManager::addToConfig()
+ *
+ * @return void
+ */
+void ShtUsermod::addToConfig(JsonObject &root)
+{
+  JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
+
+  top[FPSTR(_enabled)] = enabled;
+  top[FPSTR(_shtType)] = shtType;
+  top[FPSTR(_unitOfTemp)] = unitOfTemp;
+  top[FPSTR(_haMqttDiscovery)] = haMqttDiscovery;
+}
+
+/**
+ * Apply config on boot or save of UM config page.
+ *
+ * This is called whenever WLED boots and loads cfg.json, or when the UM config
+ * page is saved. Will properly re-instantiate the SHT class upon type change and
+ * publish HA discovery after enabling.
+ *
+ * @see Usermod::readFromConfig()
+ * @see UsermodManager::readFromConfig()
+ *
+ * @return bool
+ */
+bool ShtUsermod::readFromConfig(JsonObject &root)
+{
+  JsonObject top = root[FPSTR(_name)];
+  if (top.isNull()) {
+    DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name);
+    return false;
+  }
+
+  bool oldEnabled = enabled;
+  byte oldShtType = shtType;
+  byte oldUnitOfTemp = unitOfTemp;
+  bool oldHaMqttDiscovery = haMqttDiscovery;
+
+  getJsonValue(top[FPSTR(_enabled)], enabled);
+  getJsonValue(top[FPSTR(_shtType)], shtType);
+  getJsonValue(top[FPSTR(_unitOfTemp)], unitOfTemp);
+  getJsonValue(top[FPSTR(_haMqttDiscovery)], haMqttDiscovery);
+
+  // First run: reading from cfg.json, nothing to do here, will be all done in setup()
+  if (!firstRunDone) {
+    DEBUG_PRINTF("[%s] First run, nothing to do\n", _name);
+  }
+  // Check if mod has been en-/disabled
+  else if (enabled != oldEnabled) {
+    enabled ? setup() : cleanup();
+    DEBUG_PRINTF("[%s] Usermod has been en-/disabled\n", _name);
+  }
+  // Config has been changed, so adopt to changes
+  else if (enabled) {
+    if (oldShtType != shtType) {
+      cleanupShtTempHumiditySensor();
+      initShtTempHumiditySensor();
+    }
+
+    if (oldUnitOfTemp != unitOfTemp) {
+      publishTemperatureAndHumidityViaMqtt();
+      publishHomeAssistantAutodiscovery();
+    }
+
+    if (oldHaMqttDiscovery != haMqttDiscovery && haMqttDiscovery) {
+      publishHomeAssistantAutodiscovery();
+    }
+
+    DEBUG_PRINTF("[%s] Config (re)loaded\n", _name);
+  }
+
+  return true;
+}
+
+/**
+ * Adds the temperature and humidity actually to the info section and /json info.
+ *
+ * This is called every time the info section is opened ot /json is called.
+ *
+ * @see Usermod::addToJsonInfo()
+ * @see UsermodManager::addToJsonInfo()
+ *
+ * @return void
+ */
+void ShtUsermod::addToJsonInfo(JsonObject& root)
+{
+  if (!enabled && !isShtReady()) {
+    return;
+  }
+
+  JsonObject user = root["u"];
+  if (user.isNull()) user = root.createNestedObject("u");
+
+  JsonArray jsonTemp = user.createNestedArray(F("Temperature"));
+  JsonArray jsonHumidity = user.createNestedArray(F("Humidity"));
+
+  if (shtLastTimeUpdated == 0 || !shtReadDataSuccess) {
+    jsonTemp.add(0);
+    jsonHumidity.add(0);
+    if (shtLastTimeUpdated == 0) {
+      jsonTemp.add(F(" Not read yet"));
+      jsonHumidity.add(F(" Not read yet"));
+    } else {
+      jsonTemp.add(F(" Error"));
+      jsonHumidity.add(F(" Error"));
+    }
+    return;
+  }
+
+  jsonHumidity.add(getHumidity());
+  jsonHumidity.add(F(" RH"));
+
+  jsonTemp.add(getTemperature());
+  jsonTemp.add(getUnitString());
+
+  // sensor object
+  JsonObject sensor = root[F("sensor")];
+  if (sensor.isNull()) sensor = root.createNestedObject(F("sensor"));
+
+  jsonTemp = sensor.createNestedArray(F("temp"));
+  jsonTemp.add(getTemperature());
+  jsonTemp.add(getUnitString());
+
+  jsonHumidity = sensor.createNestedArray(F("humidity"));
+  jsonHumidity.add(getHumidity());
+  jsonHumidity.add(F(" RH"));
+}
+
+/**
+ * Getter for last read temperature for configured unit.
+ *
+ * @return float
+ */
+float ShtUsermod::getTemperature() {
+  return unitOfTemp ? getTemperatureF() : getTemperatureC();
+}
+
+/**
+ * Returns the current configured unit as human readable string.
+ *
+ * @return const char*
+ */
+const char* ShtUsermod::getUnitString() {
+  return unitOfTemp ? "°F" : "°C";
+}
\ No newline at end of file
diff --git a/usermods/smartnest/readme.md b/usermods/smartnest/readme.md
new file mode 100644
index 0000000000..5c3ef8072e
--- /dev/null
+++ b/usermods/smartnest/readme.md
@@ -0,0 +1,61 @@
+# Smartnest
+
+Enables integration with `smartnest.cz` service which provides MQTT integration with voice assistants.
+In order to setup Smartnest follow the [documentation](https://www.docu.smartnest.cz/).
+
+## MQTT API
+
+The API is described in the Smartnest [Github repo](https://github.com/aososam/Smartnest/blob/master/Devices/lightRgb/lightRgb.ino).
+
+
+## Usermod installation
+
+1. Register the usermod by adding `#include "../usermods/smartnest/usermod_smartnest.h"` at the top and `usermods.add(new Smartnest());` at the bottom of `usermods_list.cpp`.
+or
+2. Use `#define USERMOD_SMARTNEST` in wled.h or `-D USERMOD_SMARTNEST` in your platformio.ini
+
+
+Example **usermods_list.cpp**:
+
+```cpp
+#include "wled.h"
+/*
+ * Register your v2 usermods here!
+ *   (for v1 usermods using just usermod.cpp, you can ignore this file)
+ */
+
+/*
+ * Add/uncomment your usermod filename here (and once more below)
+ * || || ||
+ * \/ \/ \/
+ */
+//#include "usermod_v2_example.h"
+//#include "usermod_temperature.h"
+#include "../usermods/usermod_smartnest.h"
+
+void registerUsermods()
+{
+  /*
+   * Add your usermod class name here
+   * || || ||
+   * \/ \/ \/
+   */
+  //usermods.add(new MyExampleUsermod());
+  //usermods.add(new UsermodTemperature());
+  usermods.add(new Smartnest());
+
+}
+```
+
+## Configuration
+
+Usermod has no configuration, but it relies on the MQTT configuration.\
+Under Config > Sync Interfaces > MQTT:
+* Enable MQTT check box
+* Set the `Broker` field to: `smartnest.cz`
+* The `Username` and `Password` fields are the login information from the `smartnest.cz` website.
+* `Client ID` field is obtained from the device configuration panel in `smartnest.cz`.
+
+## Change log
+2022-09
+* First implementation.
diff --git a/usermods/smartnest/usermod_smartnest.h b/usermods/smartnest/usermod_smartnest.h
new file mode 100644
index 0000000000..8d2b04ff96
--- /dev/null
+++ b/usermods/smartnest/usermod_smartnest.h
@@ -0,0 +1,171 @@
+#ifndef WLED_ENABLE_MQTT
+#error "This user mod requires MQTT to be enabled."
+#endif
+
+#pragma once
+
+#include "wled.h"
+
+class Smartnest : public Usermod
+{
+private:
+  void sendToBroker(const char *const topic, const char *const message)
+  {
+    if (!WLED_MQTT_CONNECTED)
+    {
+      return;
+    }
+
+    String topic_ = String(mqttClientID) + "/" + String(topic);
+    mqtt->publish(topic_.c_str(), 0, true, message);
+  }
+
+  void turnOff()
+  {
+    setBrightness(0);
+    turnOnAtBoot = false;
+    offMode = true;
+    sendToBroker("report/powerState", "OFF");
+  }
+
+  void turnOn()
+  {
+    setBrightness(briLast);
+    turnOnAtBoot = true;
+    offMode = false;
+    sendToBroker("report/powerState", "ON");
+  }
+
+  void setBrightness(int value)
+  {
+    if (value == 0 && bri > 0) briLast = bri;
+    bri = value;
+    stateUpdated(CALL_MODE_DIRECT_CHANGE);
+  }
+
+  void setColor(int r, int g, int b)
+  {
+    strip.setColor(0, r, g, b);
+    stateUpdated(CALL_MODE_DIRECT_CHANGE);
+    char msg[18] {};
+    sprintf(msg, "rgb(%d,%d,%d)", r, g, b);
+    sendToBroker("report/color", msg);
+  }
+
+  int splitColor(const char *const color, int * const rgb)
+  {
+    char *color_ = NULL;
+    const char delim[] = ",";
+    char *cxt = NULL;
+    char *token = NULL;
+    int position = 0;
+
+    // We need to copy the string in order to keep it read only as strtok_r function requires mutable string
+    color_ = (char *)malloc(strlen(color));
+    if (NULL == color_) {
+      return -1;
+    }
+
+    strcpy(color_, color);
+    token = strtok_r(color_, delim, &cxt);
+
+    while (token != NULL)
+    {
+      rgb[position++] = (int)strtoul(token, NULL, 10);
+      token = strtok_r(NULL, delim, &cxt);
+    }
+    free(color_);
+
+    return position;
+  }
+
+public:
+  // Functions called by WLED
+
+  /**
+   * handling of MQTT message
+   * topic should look like: ///
+   */
+  bool onMqttMessage(char *topic, char *message)
+  {
+    String topic_{topic};
+    String topic_prefix{mqttClientID + String("/directive/")};
+
+    if (!topic_.startsWith(topic_prefix))
+    {
+      return false;
+    }
+
+    String subtopic = topic_.substring(topic_prefix.length());
+    String message_(message);
+
+    if (subtopic == "powerState")
+    {
+      if (strcmp(message, "ON") == 0)
+      {
+        turnOn();
+      }
+      else if (strcmp(message, "OFF") == 0)
+      {
+        turnOff();
+      }
+      return true;
+    }
+
+    if (subtopic == "percentage")
+    {
+      int val = (int)strtoul(message, NULL, 10);
+      if (val >= 0 && val <= 100)
+      {
+        setBrightness(map(val, 0, 100, 0, 255));
+      }
+      return true;
+    }
+
+    if (subtopic == "color")
+    {
+      // Parse the message which is in the format "rgb(<0-255>,<0-255>,<0-255>)"
+      int rgb[3] = {};
+      String colors = message_.substring(String("rgb(").length(), message_.lastIndexOf(')'));
+      if (3 != splitColor(colors.c_str(), rgb))
+      {
+        return false;
+      }
+      setColor(rgb[0], rgb[1], rgb[2]);
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * subscribe to MQTT topic and send publish current status.
+   */
+  void onMqttConnect(bool sessionPresent)
+  {
+    String topic = String(mqttClientID) + "/#";
+
+    mqtt->subscribe(topic.c_str(), 0);
+    sendToBroker("report/online", (bri ? "true" : "false")); // Reports that the device is online
+    delay(100);
+    sendToBroker("report/firmware", versionString); // Reports the firmware version
+    delay(100);
+    sendToBroker("report/ip", (char *)WiFi.localIP().toString().c_str()); // Reports the ip
+    delay(100);
+    sendToBroker("report/network", (char *)WiFi.SSID().c_str()); // Reports the network name
+    delay(100);
+
+    String signal(WiFi.RSSI(), 10);
+    sendToBroker("report/signal", signal.c_str()); // Reports the signal strength
+    delay(100);
+  }
+
+  /**
+   * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).
+   * This could be used in the future for the system to determine whether your usermod is installed.
+   */
+  uint16_t getId()
+  {
+    return USERMOD_ID_SMARTNEST;
+  }
+};
diff --git a/usermods/stairway_wipe_basic/readme.md b/usermods/stairway_wipe_basic/readme.md
index 632b7d85d8..353856b3c1 100644
--- a/usermods/stairway_wipe_basic/readme.md
+++ b/usermods/stairway_wipe_basic/readme.md
@@ -1,14 +1,22 @@
-### Stairway lighting
+# Stairway lighting
 
+## Install
+Add the buildflag `-D USERMOD_STAIRCASE_WIPE` to your enviroment to activate it.
+
+### Configuration
+`-D STAIRCASE_WIPE_OFF` 
+
Have the LEDs wipe off instead of fading out + +## Description Quick usermod to accomplish something similar to [this video](https://www.youtube.com/watch?v=NHkju5ncC4A). -This usermod allows you to add a lightstrip alongside or on the steps of a staircase. +This usermod enables you to add a lightstrip alongside or on the steps of a staircase. When the `userVar0` variable is set, the LEDs will gradually turn on in a Wipe effect. Both directions are supported by setting userVar0 to 1 and 2, respectively (HTTP API commands `U0=1` and `U0=2`). -After the Wipe is complete, the light will either stay on (Solid effect) indefinitely or after `userVar1` seconds have elapsed. -If userVar0 is updated (e.g. by triggering a second sensor) the light will slowly fade off. -This could be extended to also run a Wipe effect in reverse order to turn the LEDs back off. +After the Wipe is complete, the light will either stay on (Solid effect) indefinitely or extinguish after `userVar1` seconds have elapsed. +If userVar0 is updated (e.g. by triggering a second sensor) the light will fade slowly until it's off. +This could be extended to also run a Wipe effect in reverse order to turn the LEDs off. This is just a basic version to accomplish this using HTTP API calls `U0` and `U1` and/or macros. -It should be easy to adapt this code however to interface with motion sensors or other input devices. \ No newline at end of file +It should be easy to adapt this code to interface with motion sensors or other input devices. diff --git a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h b/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h index 08d551be02..f712316b86 100644 --- a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h +++ b/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h @@ -19,10 +19,12 @@ class StairwayWipeUsermod : public Usermod { unsigned long timeStaticStart = 0; uint16_t previousUserVar0 = 0; +//moved to buildflag //comment this out if you want the turn off effect to be just fading out instead of reverse wipe -#define STAIRCASE_WIPE_OFF +//#define STAIRCASE_WIPE_OFF public: - +void setup() { + } 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 @@ -84,19 +86,20 @@ class StairwayWipeUsermod : public Usermod { uint16_t getId() { - return USERMOD_ID_EXAMPLE; + return USERMOD_ID_STAIRWAY_WIPE; } void startWipe() { bri = briLast; //turn on - transitionDelayTemp = 0; //no transition + jsonTransitionOnce = true; + strip.setTransition(0); //no transition effectCurrent = FX_MODE_COLOR_WIPE; resetTimebase(); //make sure wipe starts from beginning //set wipe direction - WS2812FX::Segment& seg = strip.getSegment(0); + Segment& seg = strip.getSegment(0); bool doReverse = (userVar0 == 2); seg.setOption(1, doReverse); @@ -105,10 +108,11 @@ class StairwayWipeUsermod : public Usermod { void turnOff() { + jsonTransitionOnce = true; #ifdef STAIRCASE_WIPE_OFF - transitionDelayTemp = 0; //turn off immediately after wipe completed + strip.setTransition(0); //turn off immediately after wipe completed #else - transitionDelayTemp = 4000; //fade out slowly + strip.setTransition(4000); //fade out slowly #endif bri = 0; stateUpdated(CALL_MODE_NOTIFICATION); diff --git a/usermods/stairway_wipe_basic/wled06_usermod.ino b/usermods/stairway_wipe_basic/wled06_usermod.ino index eeece4438a..c1264ebfb2 100644 --- a/usermods/stairway_wipe_basic/wled06_usermod.ino +++ b/usermods/stairway_wipe_basic/wled06_usermod.ino @@ -89,7 +89,7 @@ void startWipe() resetTimebase(); //make sure wipe starts from beginning //set wipe direction - WS2812FX::Segment& seg = strip.getSegment(0); + Segment& seg = strip.getSegment(0); bool doReverse = (userVar0 == 2); seg.setOption(1, doReverse); diff --git a/usermods/usermod_rotary_brightness_color/README.md b/usermods/usermod_rotary_brightness_color/README.md index 9d59a0ba9f..06a3a0f82a 100644 --- a/usermods/usermod_rotary_brightness_color/README.md +++ b/usermods/usermod_rotary_brightness_color/README.md @@ -1,18 +1,18 @@ # Rotary Encoder (Brightness and Color) -V2 usermod that allows changing brightness and color using a rotary encoder, +V2 usermod that enables changing brightness and color using a rotary encoder change between modes by pressing a button (many encoders have one included) -but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle" +it will wait for AUTOSAVE_SETTLE_MS milliseconds. a "settle" period in case there are other changes (any change will -extend the "settle" window). +extend the "settle" period). It will additionally load preset AUTOSAVE_PRESET_NUM at startup. during the first `loop()`. Reasoning below. AutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes. -Note: I don't love that WLED doesn't respect the brightness of the preset being auto loaded, so the AutoSaveUsermod will set the AUTOSAVE_PRESET_NUM preset in the first loop, so brightness IS honored. This means WLED will effectively ignore Default brightness and Apply N preset at boot when the AutoSaveUsermod is installed. +Note: WLED doesn't respect the brightness of the preset being auto loaded, so the AutoSaveUsermod will set the AUTOSAVE_PRESET_NUM preset in the first loop, so brightness IS honored. This means WLED will effectively ignore Default brightness and Apply N preset at boot when the AutoSaveUsermod is installed. ## Installation diff --git a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h index 61b76ba194..85a9a16054 100644 --- a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h +++ b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h @@ -23,7 +23,7 @@ class RotaryEncoderBrightnessColor : public Usermod unsigned char Enc_B; unsigned char Enc_A_prev = 0; - // private class memebers configurable by Usermod Settings (defaults set inside readFromConfig()) + // private class members configurable by Usermod Settings (defaults set inside readFromConfig()) int8_t pins[3]; // pins[0] = DT from encoder, pins[1] = CLK from encoder, pins[2] = CLK from encoder (optional) int fadeAmount; // how many points to fade the Neopixel with each step @@ -162,7 +162,7 @@ class RotaryEncoderBrightnessColor : public Usermod * - configComplete is used to return false if any value is missing, not just if the main object is missing * - The defaults are loaded every time readFromConfig() is run, not just once after boot * - * This ensures that missing values are added to the config, with their default values, in the rare but plauible cases of: + * This ensures that missing values are added to the config, with their default values, in the rare but plausible cases of: * - a single value being missing at boot, e.g. if the Usermod was upgraded and a new setting was added * - a single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) * diff --git a/usermods/usermod_v2_auto_save/readme.md b/usermods/usermod_v2_auto_save/readme.md index 7cf81085e7..f54d87a769 100644 --- a/usermods/usermod_v2_auto_save/readme.md +++ b/usermods/usermod_v2_auto_save/readme.md @@ -1,24 +1,21 @@ # Auto Save -v2 Usermod to automatically save settings -to preset number AUTOSAVE_PRESET_NUM after a change to any of - +v2 Usermod to automatically save settings +to preset number AUTOSAVE_PRESET_NUM after a change to any of: * brightness * effect speed * effect intensity * mode (effect) * palette -but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle" -period in case there are other changes (any change will -extend the "settle" window). +but it will wait for AUTOSAVE_AFTER_SEC seconds, +a "settle" period in case there are other changes (any change will extend the "settle" period). -It will additionally load preset AUTOSAVE_PRESET_NUM at startup. -during the first `loop()`. Reasoning below. +It will additionally load preset AUTOSAVE_PRESET_NUM at startup during the first `loop()`. AutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes. -Note: I don't love that WLED doesn't respect the brightness of the preset being auto loaded, so the AutoSaveUsermod will set the AUTOSAVE_PRESET_NUM preset in the first loop, so brightness IS honored. This means WLED will effectively ignore Default brightness and Apply N preset at boot when the AutoSaveUsermod is installed. +Note: WLED doesn't respect the brightness of the preset being auto loaded, so the AutoSaveUsermod will set the AUTOSAVE_PRESET_NUM preset in the first loop, so brightness IS honored. This means WLED will effectively ignore Default brightness and Apply N preset at boot when the AutoSaveUsermod is installed. ## Installation @@ -28,10 +25,21 @@ This file should be placed in the same directory as `platformio.ini`. ### Define Your Options -* `USERMOD_AUTO_SAVE` - define this to have this the Auto Save usermod included wled00\usermods_list.cpp -* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) +* `USERMOD_AUTO_SAVE` - define this to have this usermod included wled00\usermods_list.cpp +* `AUTOSAVE_AFTER_SEC` - define the delay time after the settings auto-saving routine should be executed +* `AUTOSAVE_PRESET_NUM` - define the preset number used by autosave usermod +* `USERMOD_AUTO_SAVE_ON_BOOT` - define if autosave should be enabled on boot +* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp + also tells this usermod that the display is available + (see the Four Line Display usermod `readme.md` for more details) + +Example to add in platformio_override: + -D USERMOD_AUTO_SAVE + -D AUTOSAVE_AFTER_SEC=10 + -D AUTOSAVE_PRESET_NUM=100 + -D USERMOD_AUTO_SAVE_ON_BOOT=true -You can configure auto-save parameters using Usermods settings page. +You can also configure auto-save parameters using Usermods settings page. ### PlatformIO requirements @@ -44,4 +52,4 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. 2021-02 * First public release 2021-04 -* Adaptation for runtime configuration. \ No newline at end of file +* Adaptation for runtime configuration. diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h index c289cc32ad..2a63dd4d88 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -33,9 +33,23 @@ class AutoSaveUsermod : public Usermod { bool enabled = true; // configurable parameters + #ifdef AUTOSAVE_AFTER_SEC + uint16_t autoSaveAfterSec = AUTOSAVE_AFTER_SEC; + #else uint16_t autoSaveAfterSec = 15; // 15s by default + #endif + + #ifdef AUTOSAVE_PRESET_NUM + uint8_t autoSavePreset = AUTOSAVE_PRESET_NUM; + #else uint8_t autoSavePreset = 250; // last possible preset + #endif + + #ifdef USERMOD_AUTO_SAVE_ON_BOOT + bool applyAutoSaveOnBoot = USERMOD_AUTO_SAVE_ON_BOOT; + #else bool applyAutoSaveOnBoot = false; // do we load auto-saved preset on boot? + #endif // If we've detected the need to auto save, this will be non zero. unsigned long autoSaveAfter = 0; @@ -64,6 +78,7 @@ class AutoSaveUsermod : public Usermod { PSTR("~ %02d-%02d %02d:%02d:%02d ~"), month(localTime), day(localTime), hour(localTime), minute(localTime), second(localTime)); + cacheInvalidate++; // force reload of presets savePreset(autoSavePreset, presetNameBuffer); } @@ -76,13 +91,17 @@ class AutoSaveUsermod : public Usermod { #endif } + void enable(bool enable) { + enabled = enable; + } + public: // gets called once at boot. Do all initialization that doesn't depend on // network here void setup() { #ifdef USERMOD_FOUR_LINE_DISPLAY - // This Usermod has enhanced funcionality if + // This Usermod has enhanced functionality if // FourLineDisplayUsermod is available. display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); #endif @@ -129,7 +148,7 @@ class AutoSaveUsermod : public Usermod { if (autoSaveAfter && now > autoSaveAfter) { autoSaveAfter = 0; - // Time to auto save. You may have some flickry? + // Time to auto save. You may have some flickery? saveSettings(); displayOverlay(); } @@ -140,12 +159,24 @@ class AutoSaveUsermod : public Usermod { * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. * Below it is shown how this could be used for e.g. a light sensor */ - //void addToJsonInfo(JsonObject& root) { - //JsonObject user = root["u"]; - //if (user.isNull()) user = root.createNestedObject("u"); - //JsonArray data = user.createNestedArray(F("Autosave")); - //data.add(F("Loaded.")); - //} + void addToJsonInfo(JsonObject& root) { + JsonObject user = root["u"]; + if (user.isNull()) { + user = root.createNestedObject("u"); + } + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); // name + + String uiDomString = F(""); + infoArr.add(uiDomString); + } /* * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). @@ -158,9 +189,20 @@ class AutoSaveUsermod : public Usermod { * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - //void readFromJsonState(JsonObject& root) { - // if (!initDone) return; // prevent crash on boot applyPreset() - //} + void readFromJsonState(JsonObject& root) { + if (!initDone) return; // prevent crash on boot applyPreset() + bool en = enabled; + JsonObject um = root[FPSTR(_name)]; + if (!um.isNull()) { + if (um[FPSTR(_autoSaveEnabled)].is()) { + en = um[FPSTR(_autoSaveEnabled)].as(); + } else { + String str = um[FPSTR(_autoSaveEnabled)]; // checkbox -> off or on + en = (bool)(str!="off"); // off is guaranteed to be present + } + if (en != enabled) enable(en); + } + } /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. diff --git a/usermods/usermod_v2_four_line_display/readme.md b/usermods/usermod_v2_four_line_display/readme.md index 47518be905..a0ed44d71f 100644 --- a/usermods/usermod_v2_four_line_display/readme.md +++ b/usermods/usermod_v2_four_line_display/readme.md @@ -2,9 +2,9 @@ First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod. -This usermod provides a four line display using either +Provides a four line display using either 128x32 or 128x64 OLED displays. -It's can operate independently, but starts to provide +It can operate independently, but starts to provide a relatively complete on-device UI when paired with the Rotary Encoder UI usermod. I strongly encourage you to use them together. @@ -19,11 +19,11 @@ This file should be placed in the same directory as `platformio.ini`. ### Define Your Options -* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, that the display is available +* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, the display is available * `FLD_PIN_SCL` - The display SCL pin, defaults to 5 * `FLD_PIN_SDA` - The display SDA pin, defaults to 4 -All of the parameters can be configured using Usermods settings page, inluding GPIO pins. +All of the parameters can be configured via the Usermods settings page, including GPIO pins. ### PlatformIO requirements @@ -44,7 +44,7 @@ UI usermod folder for how to include these using `platformio_override.ini`. * 6 = SPI SSD1306 128x32 * 7 = SPI SSD1306 128x64 (4 double-height lines) * `contrast` - set display contrast (higher contrast may reduce display lifetime) -* `refreshRateSec` - time in seconds for display refresh +* `refreshRateSec` - display refresh time in seconds * `screenTimeOutSec` - screen saver time-out in seconds * `flip` - flip/rotate display 180° * `sleepMode` - enable/disable screen saver @@ -60,4 +60,4 @@ UI usermod folder for how to include these using `platformio_override.ini`. * Adaptation for runtime configuration. 2021-11 -* Added configuration option description. \ No newline at end of file +* Added configuration option description. diff --git a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h index a49961dd57..2190c23939 100644 --- a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h +++ b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h @@ -11,7 +11,7 @@ // for WLED. // // Dependencies -// * This usermod REQURES the ModeSortUsermod +// * This usermod REQUIRES the ModeSortUsermod // * This Usermod works best, by far, when coupled // with RotaryEncoderUIUsermod. // @@ -24,54 +24,31 @@ // //The SCL and SDA pins are defined here. +#ifndef FLD_PIN_SCL + #define FLD_PIN_SCL i2c_scl +#endif +#ifndef FLD_PIN_SDA + #define FLD_PIN_SDA i2c_sda +#endif +#ifndef FLD_PIN_CLOCKSPI + #define FLD_PIN_CLOCKSPI spi_sclk +#endif + #ifndef FLD_PIN_DATASPI + #define FLD_PIN_DATASPI spi_mosi +#endif +#ifndef FLD_PIN_CS + #define FLD_PIN_CS spi_cs +#endif #ifdef ARDUINO_ARCH_ESP32 - #define HW_PIN_SCL 22 - #define HW_PIN_SDA 21 - #define HW_PIN_CLOCKSPI 18 - #define HW_PIN_DATASPI 23 - #ifndef FLD_PIN_SCL - #define FLD_PIN_SCL 22 - #endif - #ifndef FLD_PIN_SDA - #define FLD_PIN_SDA 21 - #endif - #ifndef FLD_PIN_CLOCKSPI - #define FLD_PIN_CLOCKSPI 18 - #endif - #ifndef FLD_PIN_DATASPI - #define FLD_PIN_DATASPI 23 - #endif #ifndef FLD_PIN_DC #define FLD_PIN_DC 19 #endif - #ifndef FLD_PIN_CS - #define FLD_PIN_CS 5 - #endif #ifndef FLD_PIN_RESET #define FLD_PIN_RESET 26 #endif #else - #define HW_PIN_SCL 5 - #define HW_PIN_SDA 4 - #define HW_PIN_CLOCKSPI 14 - #define HW_PIN_DATASPI 13 - #ifndef FLD_PIN_SCL - #define FLD_PIN_SCL 5 - #endif - #ifndef FLD_PIN_SDA - #define FLD_PIN_SDA 4 - #endif - #ifndef FLD_PIN_CLOCKSPI - #define FLD_PIN_CLOCKSPI 14 - #endif - #ifndef FLD_PIN_DATASPI - #define FLD_PIN_DATASPI 13 - #endif #ifndef FLD_PIN_DC #define FLD_PIN_DC 12 - #endif - #ifndef FLD_PIN_CS - #define FLD_PIN_CS 15 #endif #ifndef FLD_PIN_RESET #define FLD_PIN_RESET 16 @@ -192,13 +169,14 @@ class FourLineDisplayUsermod : public Usermod { bool isHW; PinOwner po = PinOwner::UM_FourLineDisplay; if (type == SSD1306_SPI || type == SSD1306_SPI64) { - isHW = (ioPin[0]==HW_PIN_CLOCKSPI && ioPin[1]==HW_PIN_DATASPI); + isHW = (ioPin[0]==spi_sclk && ioPin[1]==spi_mosi); + if (isHW) po = PinOwner::HW_SPI; // allow multiple allocations of HW I2C bus pins PinManagerPinType pins[5] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true }, { ioPin[3], true }, { ioPin[4], true }}; if (!pinManager.allocateMultiplePins(pins, 5, po)) { type=NONE; return; } } else { - isHW = (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA); + isHW = (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda); + if (isHW) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; - if (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins if (!pinManager.allocateMultiplePins(pins, 2, po)) { type=NONE; return; } } @@ -415,7 +393,7 @@ class FourLineDisplayUsermod : public Usermod { drawString(getCols() - 1, 0, "~"); } - // Second row with IP or Psssword + // Second row with IP or Password drawGlyph(0, lineHeight, 68, u8x8_font_open_iconic_embedded_1x1); // wifi icon // Print password in AP mode and if led is OFF. if (apActive && bri == 0) { @@ -718,8 +696,14 @@ class FourLineDisplayUsermod : public Usermod { if (pinsChanged || type!=newType) { if (type != NONE) delete u8x8; PinOwner po = PinOwner::UM_FourLineDisplay; - if (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - pinManager.deallocateMultiplePins((const uint8_t *)ioPin, (type == SSD1306_SPI || type == SSD1306_SPI64) ? 5 : 2, po); + bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64); + if (isSPI) { + if (ioPin[0]==spi_sclk && ioPin[1]==spi_mosi) po = PinOwner::HW_SPI; // allow multiple allocations of HW SPI bus pins + pinManager.deallocateMultiplePins((const uint8_t *)ioPin, 5, po); + } else { + if (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins + pinManager.deallocateMultiplePins((const uint8_t *)ioPin, 2, po); + } for (byte i=0; i<5; i++) ioPin[i] = newPin[i]; if (ioPin[0]<0 || ioPin[1]<0) { // data & clock must be > -1 type = NONE; diff --git a/usermods/usermod_v2_four_line_display_ALT/readme.md b/usermods/usermod_v2_four_line_display_ALT/readme.md index 67cde35326..ea9f436109 100644 --- a/usermods/usermod_v2_four_line_display_ALT/readme.md +++ b/usermods/usermod_v2_four_line_display_ALT/readme.md @@ -4,12 +4,12 @@ Thank you to the authors of the original version of these usermods. It would not "usermod_v2_four_line_display" "usermod_v2_rotary_encoder_ui" -The core of these usermods are a copy of the originals. The main changes are done to the FourLineDisplay usermod. +The core of these usermods are a copy of the originals. The main changes are to the FourLineDisplay usermod. The display usermod UI has been completely changed. The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. -Without the display it functions identical to the original. +Without the display it, functions identical to the original. The original "usermod_v2_auto_save" will not work with the display just yet. Press the encoder to cycle through the options: @@ -22,7 +22,7 @@ Press the encoder to cycle through the options: *Saturation (only if display is used) Press and hold the encoder to display Network Info - if AP is active then it will display AP ssid and Password + if AP is active, it will display AP, SSID and password Also shows if the timer is enabled @@ -42,4 +42,4 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. ## Change Log 2021-10 -* First public release \ No newline at end of file +* First public release diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index 383accc52c..cd4201fecf 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -1,20 +1,24 @@ #pragma once #include "wled.h" +#undef U8X8_NO_HW_I2C // borrowed from WLEDMM: we do want I2C hardware drivers - if possible #include // from https://github.com/olikraus/u8g2/ #include "4LD_wled_fonts.c" +#ifndef FLD_ESP32_NO_THREADS + #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! +#endif + // -// Insired by the usermod_v2_four_line_display +// Inspired by the usermod_v2_four_line_display // // v2 usermod for using 128x32 or 128x64 i2c // OLED displays to provide a four line display // for WLED. // // Dependencies -// * This usermod REQURES the ModeSortUsermod // * This Usermod works best, by far, when coupled -// with RotaryEncoderUIUsermod. +// with RotaryEncoderUI ALT Usermod. // // Make sure to enable NTP and set your time zone in WLED Config | Time. // @@ -23,56 +27,26 @@ // REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) // REQUIREMENT: * Wire // +// If display does not work or looks corrupted check the +// constructor reference: +// https://github.com/olikraus/u8g2/wiki/u8x8setupcpp +// or check the gallery: +// https://github.com/olikraus/u8g2/wiki/gallery + +#ifndef FLD_PIN_CS + #define FLD_PIN_CS 15 +#endif -//The SCL and SDA pins are defined here. #ifdef ARDUINO_ARCH_ESP32 - #define HW_PIN_SCL 22 - #define HW_PIN_SDA 21 - #define HW_PIN_CLOCKSPI 18 - #define HW_PIN_DATASPI 23 - #ifndef FLD_PIN_SCL - #define FLD_PIN_SCL 22 - #endif - #ifndef FLD_PIN_SDA - #define FLD_PIN_SDA 21 - #endif - #ifndef FLD_PIN_CLOCKSPI - #define FLD_PIN_CLOCKSPI 18 - #endif - #ifndef FLD_PIN_DATASPI - #define FLD_PIN_DATASPI 23 - #endif #ifndef FLD_PIN_DC #define FLD_PIN_DC 19 #endif - #ifndef FLD_PIN_CS - #define FLD_PIN_CS 5 - #endif #ifndef FLD_PIN_RESET #define FLD_PIN_RESET 26 #endif #else - #define HW_PIN_SCL 5 - #define HW_PIN_SDA 4 - #define HW_PIN_CLOCKSPI 14 - #define HW_PIN_DATASPI 13 - #ifndef FLD_PIN_SCL - #define FLD_PIN_SCL 5 - #endif - #ifndef FLD_PIN_SDA - #define FLD_PIN_SDA 4 - #endif - #ifndef FLD_PIN_CLOCKSPI - #define FLD_PIN_CLOCKSPI 14 - #endif - #ifndef FLD_PIN_DATASPI - #define FLD_PIN_DATASPI 13 - #endif #ifndef FLD_PIN_DC #define FLD_PIN_DC 12 - #endif - #ifndef FLD_PIN_CS - #define FLD_PIN_CS 15 #endif #ifndef FLD_PIN_RESET #define FLD_PIN_RESET 16 @@ -92,39 +66,55 @@ #define SCREEN_TIMEOUT_MS 60*1000 // 1 min // Minimum time between redrawing screen in ms -#define USER_LOOP_REFRESH_RATE_MS 1000 +#define REFRESH_RATE_MS 1000 // Extra char (+1) for null #define LINE_BUFFER_SIZE 16+1 #define MAX_JSON_CHARS 19+1 #define MAX_MODE_LINE_SPACE 13+1 + +#ifdef ARDUINO_ARCH_ESP32 +static TaskHandle_t Display_Task = nullptr; +void DisplayTaskCode(void * parameter); +#endif + + typedef enum { NONE = 0, - SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C - SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C - SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C - SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C - SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C - SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI - SSD1306_SPI64 // U8X8_SSD1306_128X64_NONAME_HW_SPI + SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C + SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C + SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C + SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C + SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C + SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI + SSD1306_SPI64, // U8X8_SSD1306_128X64_NONAME_HW_SPI + SSD1309_SPI64 // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI } DisplayType; class FourLineDisplayUsermod : public Usermod { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + public: + FourLineDisplayUsermod() { if (!instance) instance = this; } + static FourLineDisplayUsermod* getInstance(void) { return instance; } +#endif private: + static FourLineDisplayUsermod *instance; bool initDone = false; + volatile bool drawing = false; + volatile bool lockRedraw = false; // HW interface & configuration U8X8 *u8x8 = nullptr; // pointer to U8X8 display object #ifndef FLD_SPI_DEFAULT - int8_t ioPin[5] = {FLD_PIN_SCL, FLD_PIN_SDA, -1, -1, -1}; // I2C pins: SCL, SDA + int8_t ioPin[3] = {-1, -1, -1}; // I2C pins: SCL, SDA uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) #else - int8_t ioPin[5] = {FLD_PIN_CLOCKSPI, FLD_PIN_DATASPI, FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // SPI pins: CLK, MOSI, CS, DC, RST + int8_t ioPin[3] = {FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // custom SPI pins: CS, DC, RST uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) #endif @@ -132,8 +122,8 @@ class FourLineDisplayUsermod : public Usermod { bool flip = false; // flip display 180° uint8_t contrast = 10; // screen contrast uint8_t lineHeight = 1; // 1 row or 2 rows - uint16_t refreshRate = USER_LOOP_REFRESH_RATE_MS; // in ms - uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms + uint16_t refreshRate = REFRESH_RATE_MS; // in ms + uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms bool sleepMode = true; // allow screen sleep? bool clockMode = false; // display clock bool showSeconds = true; // display clock with seconds @@ -187,444 +177,78 @@ class FourLineDisplayUsermod : public Usermod { // https://github.com/olikraus/u8g2/wiki/gallery // some displays need this to properly apply contrast - void setVcomh(bool highContrast) { - u8x8_t *u8x8_struct = u8x8->getU8x8(); - u8x8_cad_StartTransfer(u8x8_struct); - u8x8_cad_SendCmd(u8x8_struct, 0x0db); //address of value - u8x8_cad_SendArg(u8x8_struct, highContrast ? 0x000 : 0x040); //value 0 for fix, reboot resets default back to 64 - u8x8_cad_EndTransfer(u8x8_struct); - } - - public: + void setVcomh(bool highContrast); + void startDisplay(); - // gets called once at boot. Do all initialization that doesn't depend on - // network here - void setup() { - if (type == NONE || !enabled) return; + /** + * Wrappers for screen drawing + */ + void setFlipMode(uint8_t mode); + void setContrast(uint8_t contrast); + void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false); + void draw2x2String(uint8_t col, uint8_t row, const char *string); + void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false); + void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font); + void draw2x2GlyphIcons(); + uint8_t getCols(); + void clear(); + void setPowerSave(uint8_t save); + void center(String &line, uint8_t width); - bool isHW, isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64); - PinOwner po = PinOwner::UM_FourLineDisplay; - if (isSPI) { - isHW = (ioPin[0]==HW_PIN_CLOCKSPI && ioPin[1]==HW_PIN_DATASPI); - PinManagerPinType pins[5] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true }, { ioPin[3], true }, { ioPin[4], true }}; - if (!pinManager.allocateMultiplePins(pins, 5, po)) { type=NONE; return; } - } else { - isHW = (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA); - PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; - if (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - if (!pinManager.allocateMultiplePins(pins, 2, po)) { type=NONE; return; } - } + /** + * Display the current date and time in large characters + * on the middle rows. Based 24 or 12 hour depending on + * the useAMPM configuration. + */ + void showTime(); - DEBUG_PRINTLN(F("Allocating display.")); -/* -// At some point it may be good to not new/delete U8X8 object but use this instead -// (does not currently work) -//------------------------------------------------------------------------------- - switch (type) { - case SSD1306: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SH1106: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_sh1106_128x64_winstar, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1306_64: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1305: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1305_128x32_adafruit, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1305_64: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1306_SPI: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_4wire_sw_spi, u8x8_gpio_and_delay_arduino); - break; - case SSD1306_SPI64: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_001, u8x8_byte_arduino_4wire_sw_spi, u8x8_gpio_and_delay_arduino); - break; - default: - type = NONE; - return; - } - if (isSPI) { - if (!isHW) u8x8_SetPin_4Wire_SW_SPI(u8x8.getU8x8(), ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8_SetPin_4Wire_HW_SPI(u8x8.getU8x8(), ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - } else { - if (!isHW) u8x8_SetPin_SW_I2C(u8x8.getU8x8(), ioPin[0], ioPin[1], U8X8_PIN_NONE); // SCL, SDA, reset - else u8x8_SetPin_HW_I2C(u8x8.getU8x8(), U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - } -*/ - switch (type) { - case SSD1306: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SH1106: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1306_64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1305: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1305_64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1306_SPI: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - break; - case SSD1306_SPI64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - break; - default: - u8x8 = nullptr; - } + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool enabled); - if (nullptr == u8x8) { - DEBUG_PRINTLN(F("Display init failed.")); - pinManager.deallocateMultiplePins((const uint8_t*)ioPin, isSPI ? 5 : 2, po); - type = NONE; - return; - } + public: - lineHeight = u8x8->getRows() > 4 ? 2 : 1; - DEBUG_PRINTLN(F("Starting display.")); - u8x8->setBusClock(ioFrequency); // can be used for SPI too - u8x8->begin(); - setFlipMode(flip); - setVcomh(contrastFix); - setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 - setPowerSave(0); - //drawString(0, 0, "Loading..."); - overlayLogo(3500); - initDone = true; - } + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup(); // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here - void connected() { - knownSsid = WiFi.SSID(); //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : - knownIp = Network.localIP(); //apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); - networkOverlay(PSTR("NETWORK INFO"),7000); - } + void connected(); /** * Da loop. */ - void loop() { - if (!enabled || strip.isUpdating()) return; - unsigned long now = millis(); - if (now < nextUpdate) return; - nextUpdate = now + ((displayTurnedOff && clockMode && showSeconds) ? 1000 : refreshRate); - redraw(false); - } - - /** - * Wrappers for screen drawing - */ - void setFlipMode(uint8_t mode) { - if (type == NONE || !enabled) return; - u8x8->setFlipMode(mode); - } - void setContrast(uint8_t contrast) { - if (type == NONE || !enabled) return; - u8x8->setContrast(contrast); - } - void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) { - if (type == NONE || !enabled) return; - u8x8->setFont(u8x8_font_chroma48medium8_r); - if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string); - else u8x8->drawString(col, row, string); - } - void draw2x2String(uint8_t col, uint8_t row, const char *string) { - if (type == NONE || !enabled) return; - u8x8->setFont(u8x8_font_chroma48medium8_r); - u8x8->draw2x2String(col, row, string); - } - void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false) { - if (type == NONE || !enabled) return; - u8x8->setFont(font); - if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph); - else u8x8->drawGlyph(col, row, glyph); - } - void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font) { - if (type == NONE || !enabled) return; - u8x8->setFont(font); - u8x8->draw2x2Glyph(col, row, glyph); - } - uint8_t getCols() { - if (type==NONE || !enabled) return 0; - return u8x8->getCols(); - } - void clear() { - if (type == NONE || !enabled) return; - u8x8->clear(); - } - void setPowerSave(uint8_t save) { - if (type == NONE || !enabled) return; - u8x8->setPowerSave(save); - } - - void center(String &line, uint8_t width) { - int len = line.length(); - if (len0; i--) line = ' ' + line; - for (byte i=line.length(); i 0) { - if (now >= overlayUntil) { - // Time to display the overlay has elapsed. - overlayUntil = 0; - forceRedraw = true; - } else { - // We are still displaying the overlay - // Don't redraw. - return; - } - } + void redraw(bool forceRedraw); - if (apActive && WLED_WIFI_CONFIGURED && now<15000) { - knownSsid = apSSID; - networkOverlay(PSTR("NETWORK INFO"),30000); - return; - } - - // Check if values which are shown on display changed from the last time. - if (forceRedraw) { - needRedraw = true; - clear(); - } else if ((bri == 0 && powerON) || (bri > 0 && !powerON)) { //trigger power icon - powerON = !powerON; - drawStatusIcons(); - return; - } else if (knownnightlight != nightlightActive) { //trigger moon icon - knownnightlight = nightlightActive; - drawStatusIcons(); - if (knownnightlight) { - String timer = PSTR("Timer On"); - center(timer,LINE_BUFFER_SIZE-1); - overlay(timer.c_str(), 2500, 6); - } - return; - } else if (wificonnected != interfacesInited) { //trigger wifi icon - wificonnected = interfacesInited; - drawStatusIcons(); - return; - } else if (knownMode != effectCurrent || knownPalette != effectPalette) { - if (displayTurnedOff) needRedraw = true; - else { - if (knownPalette != effectPalette) { showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); knownPalette = effectPalette; } - if (knownMode != effectCurrent) { showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); knownMode = effectCurrent; } - lastRedraw = now; - return; - } - } else if (knownBrightness != bri) { - if (displayTurnedOff && nightlightActive) { knownBrightness = bri; } - else if (!displayTurnedOff) { updateBrightness(); lastRedraw = now; return; } - } else if (knownEffectSpeed != effectSpeed) { - if (displayTurnedOff) needRedraw = true; - else { updateSpeed(); lastRedraw = now; return; } - } else if (knownEffectIntensity != effectIntensity) { - if (displayTurnedOff) needRedraw = true; - else { updateIntensity(); lastRedraw = now; return; } - } - - if (!needRedraw) { - // Nothing to change. - // Turn off display after 1 minutes with no change. - if (sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { - // We will still check if there is a change in redraw() - // and turn it back on if it changed. - clear(); - sleepOrClock(true); - } else if (displayTurnedOff && ntpEnabled) { - showTime(); - } - return; - } - - lastRedraw = now; - - // Turn the display back on - wakeDisplay(); - - // Update last known values. - knownBrightness = bri; - knownMode = effectCurrent; - knownPalette = effectPalette; - knownEffectSpeed = effectSpeed; - knownEffectIntensity = effectIntensity; - knownnightlight = nightlightActive; - wificonnected = interfacesInited; - - // Do the actual drawing - // First row: Icons - draw2x2GlyphIcons(); - drawArrow(); - drawStatusIcons(); - - // Second row - updateBrightness(); - updateSpeed(); - updateIntensity(); - - // Third row - showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); //Palette info - - // Fourth row - showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); //Effect Mode info - } - - void updateBrightness() { - knownBrightness = bri; - if (overlayUntil == 0) { - brightness100 = ((uint16_t)bri*100)/255; - char lineBuffer[4]; - sprintf_P(lineBuffer, PSTR("%-3d"), brightness100); - drawString(1, lineHeight, lineBuffer); - //lastRedraw = millis(); - } - } - - void updateSpeed() { - knownEffectSpeed = effectSpeed; - if (overlayUntil == 0) { - fxspeed100 = ((uint16_t)effectSpeed*100)/255; - char lineBuffer[4]; - sprintf_P(lineBuffer, PSTR("%-3d"), fxspeed100); - drawString(5, lineHeight, lineBuffer); - //lastRedraw = millis(); - } - } - - void updateIntensity() { - knownEffectIntensity = effectIntensity; - if (overlayUntil == 0) { - fxintensity100 = ((uint16_t)effectIntensity*100)/255; - char lineBuffer[4]; - sprintf_P(lineBuffer, PSTR("%-3d"), fxintensity100); - drawString(9, lineHeight, lineBuffer); - //lastRedraw = millis(); - } - } - - void draw2x2GlyphIcons() { - if (lineHeight == 2) { - drawGlyph( 1, 0, 1, u8x8_4LineDisplay_WLED_icons_2x2, true); //brightness icon - drawGlyph( 5, 0, 2, u8x8_4LineDisplay_WLED_icons_2x2, true); //speed icon - drawGlyph( 9, 0, 3, u8x8_4LineDisplay_WLED_icons_2x2, true); //intensity icon - drawGlyph(14, 2*lineHeight, 4, u8x8_4LineDisplay_WLED_icons_2x2, true); //palette icon - drawGlyph(14, 3*lineHeight, 5, u8x8_4LineDisplay_WLED_icons_2x2, true); //effect icon - } else { - drawGlyph( 1, 0, 1, u8x8_4LineDisplay_WLED_icons_2x1); //brightness icon - drawGlyph( 5, 0, 2, u8x8_4LineDisplay_WLED_icons_2x1); //speed icon - drawGlyph( 9, 0, 3, u8x8_4LineDisplay_WLED_icons_2x1); //intensity icon - drawGlyph(15, 2, 4, u8x8_4LineDisplay_WLED_icons_1x1); //palette icon - drawGlyph(15, 3, 5, u8x8_4LineDisplay_WLED_icons_1x1); //effect icon - } - } - - void drawStatusIcons() { - uint8_t col = 15; - uint8_t row = 0; - drawGlyph(col, row, (wificonnected ? 20 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // wifi icon - if (lineHeight==2) { col--; } else { row++; } - drawGlyph(col, row, (bri > 0 ? 9 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // power icon - if (lineHeight==2) { col--; } else { col = row = 0; } - drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nighlight mode - } + void updateBrightness(); + void updateSpeed(); + void updateIntensity(); + void drawStatusIcons(); /** * marks the position of the arrow showing * the current setting being changed * pass line and colum info */ - void setMarkLine(byte newMarkLineNum, byte newMarkColNum) { - markLineNum = newMarkLineNum; - markColNum = newMarkColNum; - } + void setMarkLine(byte newMarkLineNum, byte newMarkColNum); - //Draw the arrow for the current setting beiong changed - void drawArrow() { - if (markColNum != 255 && markLineNum !=255) drawGlyph(markColNum, markLineNum*lineHeight, 21, u8x8_4LineDisplay_WLED_icons_1x1); - } + //Draw the arrow for the current setting being changed + void drawArrow(); - //Display the current effect or palette (desiredEntry) - // on the appropriate line (row). - void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) { - char lineBuffer[MAX_JSON_CHARS]; - if (overlayUntil == 0) { - // Find the mode name in JSON - uint8_t printedChars = extractModeName(inputEffPal, qstring, lineBuffer, MAX_JSON_CHARS-1); - if (lineBuffer[0]=='*' && lineBuffer[1]==' ') { - // remove "* " from dynamic palettes - for (byte i=2; i<=printedChars; i++) lineBuffer[i-2] = lineBuffer[i]; //include '\0' - printedChars -= 2; - } - if (lineHeight == 2) { // use this code for 8 line display - char smallBuffer1[MAX_MODE_LINE_SPACE]; - char smallBuffer2[MAX_MODE_LINE_SPACE]; - uint8_t smallChars1 = 0; - uint8_t smallChars2 = 0; - if (printedChars < MAX_MODE_LINE_SPACE) { // use big font if the text fits - while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' '; - lineBuffer[printedChars] = 0; - drawString(1, row*lineHeight, lineBuffer); - } else { // for long names divide the text into 2 lines and print them small - bool spaceHit = false; - for (uint8_t i = 0; i < printedChars; i++) { - switch (lineBuffer[i]) { - case ' ': - if (i > 4 && !spaceHit) { - spaceHit = true; - break; - } - if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; - else smallBuffer1[smallChars1++] = lineBuffer[i]; - break; - default: - if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; - else smallBuffer1[smallChars1++] = lineBuffer[i]; - break; - } - } - while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' '; - smallBuffer1[smallChars1] = 0; - drawString(1, row*lineHeight, smallBuffer1, true); - while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; - smallBuffer2[smallChars2] = 0; - drawString(1, row*lineHeight+1, smallBuffer2, true); - } - } else { // use this code for 4 ling displays - char smallBuffer3[MAX_MODE_LINE_SPACE+1]; // uses 1x1 icon for mode/palette - uint8_t smallChars3 = 0; - for (uint8_t i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; - smallBuffer3[smallChars3] = 0; - drawString(1, row*lineHeight, smallBuffer3, true); - } - } - } + //Display the current effect or palette (desiredEntry) + // on the appropriate line (row). + void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); /** * If there screen is off or in clock is displayed, @@ -632,314 +256,59 @@ class FourLineDisplayUsermod : public Usermod { * the first input from the rotary encoder but * to wake up the screen. */ - bool wakeDisplay() { - if (type == NONE || !enabled) return false; - if (displayTurnedOff) { - clear(); - // Turn the display back on - sleepOrClock(false); - //lastRedraw = millis(); - return true; - } - return false; - } + bool wakeDisplay(); /** * Allows you to show one line and a glyph as overlay for a period of time. * Clears the screen and prints. * Used in Rotary Encoder usermod. */ - void overlay(const char* line1, long showHowLong, byte glyphType) { - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (glyphType>0 && glyphType<255) { - if (lineHeight == 2) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font - else drawGlyph(6, 0, glyphType, u8x8_4LineDisplay_WLED_icons_3x3, true); - } - if (line1) { - String buf = line1; - center(buf, getCols()); - drawString(0, (glyphType<255?3:0)*lineHeight, buf.c_str()); - } - overlayUntil = millis() + showHowLong; - } + void overlay(const char* line1, long showHowLong, byte glyphType); /** * Allows you to show Akemi WLED logo overlay for a period of time. * Clears the screen and prints. */ - void overlayLogo(long showHowLong) { - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (lineHeight == 2) { - //add a bit of randomness - switch (millis()%3) { - case 0: - //WLED - draw2x2Glyph( 0, 2, 1, u8x8_wled_logo_2x2); - draw2x2Glyph( 4, 2, 2, u8x8_wled_logo_2x2); - draw2x2Glyph( 8, 2, 3, u8x8_wled_logo_2x2); - draw2x2Glyph(12, 2, 4, u8x8_wled_logo_2x2); - break; - case 1: - //WLED Akemi - drawGlyph( 2, 2, 1, u8x8_wled_logo_akemi_4x4, true); - drawGlyph( 6, 2, 2, u8x8_wled_logo_akemi_4x4, true); - drawGlyph(10, 2, 3, u8x8_wled_logo_akemi_4x4, true); - break; - case 2: - //Akemi - //draw2x2Glyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_3x3); // use this if flash runs short and comment out 6x6 font - drawGlyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_6x6, true); - drawString(6, 6, "WLED"); - break; - } - } else { - switch (millis()%3) { - case 0: - //WLED - draw2x2Glyph( 0, 0, 1, u8x8_wled_logo_2x2); - draw2x2Glyph( 4, 0, 2, u8x8_wled_logo_2x2); - draw2x2Glyph( 8, 0, 3, u8x8_wled_logo_2x2); - draw2x2Glyph(12, 0, 4, u8x8_wled_logo_2x2); - break; - case 1: - //WLED Akemi - drawGlyph( 2, 0, 1, u8x8_wled_logo_akemi_4x4); - drawGlyph( 6, 0, 2, u8x8_wled_logo_akemi_4x4); - drawGlyph(10, 0, 3, u8x8_wled_logo_akemi_4x4); - break; - case 2: - //Akemi - //drawGlyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_4x4); // a bit nicer, but uses extra 1.5k flash - draw2x2Glyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_2x2); - break; - } - } - overlayUntil = millis() + showHowLong; - } + void overlayLogo(long showHowLong); /** * Allows you to show two lines as overlay for a period of time. * Clears the screen and prints. * Used in Auto Save usermod */ - void overlay(const char* line1, const char* line2, long showHowLong) { - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (line1) { - String buf = line1; - center(buf, getCols()); - drawString(0, 1*lineHeight, buf.c_str()); - } - if (line2) { - String buf = line2; - center(buf, getCols()); - drawString(0, 2*lineHeight, buf.c_str()); - } - overlayUntil = millis() + showHowLong; - } - - void networkOverlay(const char* line1, long showHowLong) { - String line; - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (line1) { - line = line1; - center(line, getCols()); - drawString(0, 0, line.c_str()); - } - // Second row with Wifi name - line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); - if (line.length() < getCols()) center(line, getCols()); - drawString(0, lineHeight, line.c_str()); - // Print `~` char to indicate that SSID is longer, than our display - if (knownSsid.length() > getCols()) { - drawString(getCols() - 1, 0, "~"); - } - // Third row with IP and Password in AP Mode - line = knownIp.toString(); - center(line, getCols()); - drawString(0, lineHeight*2, line.c_str()); - line = ""; - if (apActive) { - line = apPass; - } else if (strcmp(serverDescription, "WLED") != 0) { - line = serverDescription; - } - center(line, getCols()); - drawString(0, lineHeight*3, line.c_str()); - overlayUntil = millis() + showHowLong; - } + void overlay(const char* line1, const char* line2, long showHowLong); - - /** - * Enable sleep (turn the display off) or clock mode. - */ - void sleepOrClock(bool enabled) { - if (enabled) { - displayTurnedOff = true; - if (clockMode && ntpEnabled) { - knownMinute = knownHour = 99; - showTime(); - } else - setPowerSave(1); - } else { - displayTurnedOff = false; - setPowerSave(0); - } - } - - /** - * Display the current date and time in large characters - * on the middle rows. Based 24 or 12 hour depending on - * the useAMPM configuration. - */ - void showTime() { - if (type == NONE || !enabled || !displayTurnedOff) return; - - char lineBuffer[LINE_BUFFER_SIZE]; - static byte lastSecond; - byte secondCurrent = second(localTime); - byte minuteCurrent = minute(localTime); - byte hourCurrent = hour(localTime); - - if (knownMinute != minuteCurrent) { //only redraw clock if it has changed - //updateLocalTime(); - byte AmPmHour = hourCurrent; - boolean isitAM = true; - if (useAMPM) { - if (AmPmHour > 11) { AmPmHour -= 12; isitAM = false; } - if (AmPmHour == 0) { AmPmHour = 12; } - } - if (knownHour != hourCurrent) { - // only update date when hour changes - sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); - draw2x2String(2, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day - } - sprintf_P(lineBuffer,PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent); - draw2x2String(2, lineHeight*2, lineBuffer); //draw hour, min. blink ":" depending on odd/even seconds - if (useAMPM) drawString(12, lineHeight*2, (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time - - drawStatusIcons(); //icons power, wifi, timer, etc - - knownMinute = minuteCurrent; - knownHour = hourCurrent; - } else { - if (secondCurrent == lastSecond) return; - } - if (showSeconds) { - lastSecond = secondCurrent; - draw2x2String(6, lineHeight*2, secondCurrent%2 ? " " : ":"); - sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); - drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line - } - } + void networkOverlay(const char* line1, long showHowLong); /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ - bool handleButton(uint8_t b) { - yield(); - if (!enabled - || b // butto 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) { - return false; - } + bool handleButton(uint8_t b); - unsigned long now = millis(); - static bool buttonPressedBefore = false; - static bool buttonLongPressed = false; - static unsigned long buttonPressedTime = 0; - static unsigned long buttonWaitTime = 0; - bool handled = true; - - //momentary button logic - if (isButtonPressed(b)) { //pressed - - if (!buttonPressedBefore) buttonPressedTime = now; - buttonPressedBefore = true; - - if (now - buttonPressedTime > 600) { //long press - buttonLongPressed = true; - //TODO: handleButton() handles button 0 without preset in a different way for double click - //so we need to override with same behaviour - longPressAction(0); - //handled = false; - } + void onUpdateBegin(bool init); - } else if (!isButtonPressed(b) && buttonPressedBefore) { //released - - long dur = now - buttonPressedTime; - if (dur < 50) { - buttonPressedBefore = false; - return true; - } //too short "press", debounce - - bool doublePress = buttonWaitTime; //did we have short press before? - buttonWaitTime = 0; - - if (!buttonLongPressed) { //short press - // if this is second release within 350ms it is a double press (buttonWaitTime!=0) - //TODO: handleButton() handles button 0 without preset in a different way for double click - if (doublePress) { - networkOverlay(PSTR("NETWORK INFO"),7000); - handled = true; - } else { - buttonWaitTime = now; - } - } - buttonPressedBefore = false; - buttonLongPressed = false; - } - // if 350ms elapsed since last press/release it is a short press - if (buttonWaitTime && now - buttonWaitTime > 350 && !buttonPressedBefore) { - buttonWaitTime = 0; - //TODO: handleButton() handles button 0 without preset in a different way for double click - //so we need to override with same behaviour - shortPressAction(0); - //handled = false; - } - return handled; - } - /* * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. * Below it is shown how this could be used for e.g. a light sensor */ - //void addToJsonInfo(JsonObject& root) { - //JsonObject user = root["u"]; - //if (user.isNull()) user = root.createNestedObject("u"); - //JsonArray data = user.createNestedArray(F("4LineDisplay")); - //data.add(F("Loaded.")); - //} + //void addToJsonInfo(JsonObject& root); /* * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - //void addToJsonState(JsonObject& root) { - //} + //void addToJsonState(JsonObject& root); /* * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - //void readFromJsonState(JsonObject& root) { - // if (!initDone) return; // prevent crash on boot applyPreset() - //} + //void readFromJsonState(JsonObject& root); + + void appendConfigData(); /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. @@ -955,25 +324,7 @@ class FourLineDisplayUsermod : public Usermod { * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ - void addToConfig(JsonObject& root) { - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; - JsonArray io_pin = top.createNestedArray("pin"); - for (byte i=0; i<5; i++) io_pin.add(ioPin[i]); - top["help4Pins"] = F("Clk,Data,CS,DC,RST"); // help for Settings page - top["type"] = type; - top["help4Type"] = F("1=SSD1306,2=SH1106,3=SSD1306_128x64,4=SSD1305,5=SSD1305_128x64,6=SSD1306_SPI,7=SSD1306_SPI_128x64"); // help for Settings page - top[FPSTR(_flip)] = (bool) flip; - top[FPSTR(_contrast)] = contrast; - top[FPSTR(_contrastFix)] = (bool) contrastFix; - top[FPSTR(_refreshRate)] = refreshRate; - top[FPSTR(_screenTimeOut)] = screenTimeout/1000; - top[FPSTR(_sleepMode)] = (bool) sleepMode; - top[FPSTR(_clockMode)] = (bool) clockMode; - top[FPSTR(_showSeconds)] = (bool) showSeconds; - top[FPSTR(_busClkFrequency)] = ioFrequency/1000; - DEBUG_PRINTLN(F("4 Line Display config saved.")); - } + void addToConfig(JsonObject& root); /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). @@ -983,71 +334,7 @@ class FourLineDisplayUsermod : public Usermod { * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) */ - bool readFromConfig(JsonObject& root) { - bool needsRedraw = false; - DisplayType newType = type; - int8_t newPin[5]; for (byte i=0; i<5; i++) newPin[i] = ioPin[i]; - - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - - enabled = top[FPSTR(_enabled)] | enabled; - newType = top["type"] | newType; - for (byte i=0; i<5; i++) newPin[i] = top["pin"][i] | ioPin[i]; - flip = top[FPSTR(_flip)] | flip; - contrast = top[FPSTR(_contrast)] | contrast; - refreshRate = top[FPSTR(_refreshRate)] | refreshRate; - refreshRate = min(5000, max(250, (int)refreshRate)); - screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; - sleepMode = top[FPSTR(_sleepMode)] | sleepMode; - clockMode = top[FPSTR(_clockMode)] | clockMode; - showSeconds = top[FPSTR(_showSeconds)] | showSeconds; - contrastFix = top[FPSTR(_contrastFix)] | contrastFix; - if (newType == SSD1306_SPI || newType == SSD1306_SPI64) - ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency - else - ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency - - DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - // first run: reading from cfg.json - for (byte i=0; i<5; i++) ioPin[i] = newPin[i]; - type = newType; - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing parameters from settings page - bool pinsChanged = false; - for (byte i=0; i<5; i++) if (ioPin[i] != newPin[i]) { pinsChanged = true; break; } - if (pinsChanged || type!=newType) { - if (type != NONE) delete u8x8; - PinOwner po = PinOwner::UM_FourLineDisplay; - if (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - pinManager.deallocateMultiplePins((const uint8_t *)ioPin, (type == SSD1306_SPI || type == SSD1306_SPI64) ? 5 : 2, po); - for (byte i=0; i<5; i++) ioPin[i] = newPin[i]; - if (ioPin[0]<0 || ioPin[1]<0) { // data & clock must be > -1 - type = NONE; - return true; - } else type = newType; - setup(); - needsRedraw |= true; - } else { - u8x8->setBusClock(ioFrequency); // can be used for SPI too - setVcomh(contrastFix); - setContrast(contrast); - setFlipMode(flip); - } - knownHour = 99; - if (needsRedraw && !wakeDisplay()) redraw(true); - else overlayLogo(3500); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_contrastFix)].isNull(); - } + bool readFromConfig(JsonObject& root); /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). @@ -1070,3 +357,1023 @@ const char FourLineDisplayUsermod::_clockMode[] PROGMEM = "clockMode"; const char FourLineDisplayUsermod::_showSeconds[] PROGMEM = "showSeconds"; const char FourLineDisplayUsermod::_busClkFrequency[] PROGMEM = "i2c-freq-kHz"; const char FourLineDisplayUsermod::_contrastFix[] PROGMEM = "contrastFix"; + +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) +FourLineDisplayUsermod *FourLineDisplayUsermod::instance = nullptr; +#endif + +// some displays need this to properly apply contrast +void FourLineDisplayUsermod::setVcomh(bool highContrast) { + if (type == NONE || !enabled) return; + u8x8_t *u8x8_struct = u8x8->getU8x8(); + u8x8_cad_StartTransfer(u8x8_struct); + u8x8_cad_SendCmd(u8x8_struct, 0x0db); //address of value + u8x8_cad_SendArg(u8x8_struct, highContrast ? 0x000 : 0x040); //value 0 for fix, reboot resets default back to 64 + u8x8_cad_EndTransfer(u8x8_struct); +} + +void FourLineDisplayUsermod::startDisplay() { + if (type == NONE || !enabled) return; + lineHeight = u8x8->getRows() > 4 ? 2 : 1; + DEBUG_PRINTLN(F("Starting display.")); + u8x8->setBusClock(ioFrequency); // can be used for SPI too + u8x8->begin(); + setFlipMode(flip); + setVcomh(contrastFix); + setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 + setPowerSave(0); + //drawString(0, 0, "Loading..."); + overlayLogo(3500); +} + +/** + * Wrappers for screen drawing + */ +void FourLineDisplayUsermod::setFlipMode(uint8_t mode) { + if (type == NONE || !enabled) return; + u8x8->setFlipMode(mode); +} +void FourLineDisplayUsermod::setContrast(uint8_t contrast) { + if (type == NONE || !enabled) return; + u8x8->setContrast(contrast); +} +void FourLineDisplayUsermod::drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH) { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->setFont(u8x8_font_chroma48medium8_r); + if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string); + else u8x8->drawString(col, row, string); + drawing = false; +} +void FourLineDisplayUsermod::draw2x2String(uint8_t col, uint8_t row, const char *string) { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->setFont(u8x8_font_chroma48medium8_r); + u8x8->draw2x2String(col, row, string); + drawing = false; +} +void FourLineDisplayUsermod::drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH) { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->setFont(font); + if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph); + else u8x8->drawGlyph(col, row, glyph); + drawing = false; +} +void FourLineDisplayUsermod::draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font) { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->setFont(font); + u8x8->draw2x2Glyph(col, row, glyph); + drawing = false; +} +uint8_t FourLineDisplayUsermod::getCols() { + if (type==NONE || !enabled) return 0; + return u8x8->getCols(); +} +void FourLineDisplayUsermod::clear() { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->clear(); + drawing = false; +} +void FourLineDisplayUsermod::setPowerSave(uint8_t save) { + if (type == NONE || !enabled) return; + u8x8->setPowerSave(save); +} + +void FourLineDisplayUsermod::center(String &line, uint8_t width) { + int len = line.length(); + if (len0; i--) line = ' ' + line; + for (byte i=line.length(); i 11) { AmPmHour -= 12; isitAM = false; } + if (AmPmHour == 0) { AmPmHour = 12; } + } + if (knownHour != hourCurrent) { + // only update date when hour changes + sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); + draw2x2String(2, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day + } + sprintf_P(lineBuffer,PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent); + draw2x2String(2, lineHeight*2, lineBuffer); //draw hour, min. blink ":" depending on odd/even seconds + if (useAMPM) drawString(12, lineHeight*2, (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time + + drawStatusIcons(); //icons power, wifi, timer, etc + + knownMinute = minuteCurrent; + knownHour = hourCurrent; + } + if (showSeconds && secondCurrent != lastSecond) { + lastSecond = secondCurrent; + draw2x2String(6, lineHeight*2, secondCurrent%2 ? " " : ":"); + sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); + drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line + } +} + +/** + * Enable sleep (turn the display off) or clock mode. + */ +void FourLineDisplayUsermod::sleepOrClock(bool enabled) { + if (enabled) { + displayTurnedOff = true; + if (clockMode && ntpEnabled) { + knownMinute = knownHour = 99; + showTime(); + } else + setPowerSave(1); + } else { + displayTurnedOff = false; + setPowerSave(0); + } +} + +// gets called once at boot. Do all initialization that doesn't depend on +// network here +void FourLineDisplayUsermod::setup() { + bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); + + // check if pins are -1 and disable usermod as PinManager::allocateMultiplePins() will accept -1 as a valid pin + if (isSPI) { + if (spi_sclk<0 || spi_mosi<0 || ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { + type = NONE; + } else { + PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; + if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type = NONE; } + } + } else { + if (i2c_scl<0 || i2c_sda<0) { type=NONE; } + } + + DEBUG_PRINTLN(F("Allocating display.")); + switch (type) { + // U8X8 uses Wire (or Wire1 with 2ND constructor) and will use existing Wire properties (calls Wire.begin() though) + case SSD1306: u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(); break; + case SH1106: u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(); break; + case SSD1306_64: u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(); break; + case SSD1305: u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(); break; + case SSD1305_64: u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(); break; + // U8X8 uses global SPI variable that is attached to VSPI bus on ESP32 + case SSD1306_SPI: u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset + case SSD1306_SPI64: u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset + case SSD1309_SPI64: u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset + // catchall + default: u8x8 = (U8X8 *) new U8X8_NULL(); enabled = false; break; // catchall to create U8x8 instance + } + + if (nullptr == u8x8) { + DEBUG_PRINTLN(F("Display init failed.")); + if (isSPI) { + pinManager.deallocateMultiplePins((const uint8_t*)ioPin, 3, PinOwner::UM_FourLineDisplay); + } + type = NONE; + return; + } + + startDisplay(); + onUpdateBegin(false); // create Display task + initDone = true; +} + +// gets called every time WiFi is (re-)connected. Initialize own network +// interfaces here +void FourLineDisplayUsermod::connected() { + knownSsid = WiFi.SSID(); //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : + knownIp = Network.localIP(); //apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); + networkOverlay(PSTR("NETWORK INFO"),7000); +} + +/** + * Da loop. + */ +void FourLineDisplayUsermod::loop() { +#if !(defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)) + if (!enabled || strip.isUpdating()) return; + unsigned long now = millis(); + if (now < nextUpdate) return; + nextUpdate = now + ((displayTurnedOff && clockMode && showSeconds) ? 1000 : refreshRate); + redraw(false); +#endif +} + +/** + * Redraw the screen (but only if things have changed + * or if forceRedraw). + */ +void FourLineDisplayUsermod::redraw(bool forceRedraw) { + bool needRedraw = false; + unsigned long now = millis(); + + if (type == NONE || !enabled) return; + if (overlayUntil > 0) { + if (now >= overlayUntil) { + // Time to display the overlay has elapsed. + overlayUntil = 0; + forceRedraw = true; + } else { + // We are still displaying the overlay + // Don't redraw. + return; + } + } + + while (drawing && millis()-now < 25) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; + + if (apActive && WLED_WIFI_CONFIGURED && now<15000) { + knownSsid = apSSID; + networkOverlay(PSTR("NETWORK INFO"),30000); + return; + } + + // Check if values which are shown on display changed from the last time. + if (forceRedraw) { + needRedraw = true; + clear(); + } else if ((bri == 0 && powerON) || (bri > 0 && !powerON)) { //trigger power icon + powerON = !powerON; + drawStatusIcons(); + return; + } else if (knownnightlight != nightlightActive) { //trigger moon icon + knownnightlight = nightlightActive; + drawStatusIcons(); + if (knownnightlight) { + String timer = PSTR("Timer On"); + center(timer,LINE_BUFFER_SIZE-1); + overlay(timer.c_str(), 2500, 6); + } + return; + } else if (wificonnected != interfacesInited) { //trigger wifi icon + wificonnected = interfacesInited; + drawStatusIcons(); + return; + } else if (knownMode != effectCurrent || knownPalette != effectPalette) { + if (displayTurnedOff) needRedraw = true; + else { + if (knownPalette != effectPalette) { showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); knownPalette = effectPalette; } + if (knownMode != effectCurrent) { showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); knownMode = effectCurrent; } + lastRedraw = now; + return; + } + } else if (knownBrightness != bri) { + if (displayTurnedOff && nightlightActive) { knownBrightness = bri; } + else if (!displayTurnedOff) { updateBrightness(); lastRedraw = now; return; } + } else if (knownEffectSpeed != effectSpeed) { + if (displayTurnedOff) needRedraw = true; + else { updateSpeed(); lastRedraw = now; return; } + } else if (knownEffectIntensity != effectIntensity) { + if (displayTurnedOff) needRedraw = true; + else { updateIntensity(); lastRedraw = now; return; } + } + + if (!needRedraw) { + // Nothing to change. + // Turn off display after 1 minutes with no change. + if (sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { + // We will still check if there is a change in redraw() + // and turn it back on if it changed. + clear(); + sleepOrClock(true); + } else if (displayTurnedOff && ntpEnabled) { + showTime(); + } + return; + } + + lastRedraw = now; + + // Turn the display back on + wakeDisplay(); + + // Update last known values. + knownBrightness = bri; + knownMode = effectCurrent; + knownPalette = effectPalette; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + knownnightlight = nightlightActive; + wificonnected = interfacesInited; + + // Do the actual drawing + // First row: Icons + draw2x2GlyphIcons(); + drawArrow(); + drawStatusIcons(); + + // Second row + updateBrightness(); + updateSpeed(); + updateIntensity(); + + // Third row + showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); //Palette info + + // Fourth row + showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); //Effect Mode info +} + +void FourLineDisplayUsermod::updateBrightness() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + knownBrightness = bri; + if (overlayUntil == 0) { + lockRedraw = true; + brightness100 = ((uint16_t)bri*100)/255; + char lineBuffer[4]; + sprintf_P(lineBuffer, PSTR("%-3d"), brightness100); + drawString(1, lineHeight, lineBuffer); + lockRedraw = false; + } +} + +void FourLineDisplayUsermod::updateSpeed() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + knownEffectSpeed = effectSpeed; + if (overlayUntil == 0) { + lockRedraw = true; + fxspeed100 = ((uint16_t)effectSpeed*100)/255; + char lineBuffer[4]; + sprintf_P(lineBuffer, PSTR("%-3d"), fxspeed100); + drawString(5, lineHeight, lineBuffer); + lockRedraw = false; + } +} + +void FourLineDisplayUsermod::updateIntensity() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + knownEffectIntensity = effectIntensity; + if (overlayUntil == 0) { + lockRedraw = true; + fxintensity100 = ((uint16_t)effectIntensity*100)/255; + char lineBuffer[4]; + sprintf_P(lineBuffer, PSTR("%-3d"), fxintensity100); + drawString(9, lineHeight, lineBuffer); + lockRedraw = false; + } +} + +void FourLineDisplayUsermod::drawStatusIcons() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + uint8_t col = 15; + uint8_t row = 0; + lockRedraw = true; + drawGlyph(col, row, (wificonnected ? 20 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // wifi icon + if (lineHeight==2) { col--; } else { row++; } + drawGlyph(col, row, (bri > 0 ? 9 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // power icon + if (lineHeight==2) { col--; } else { col = row = 0; } + drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nighlight mode + lockRedraw = false; +} + +/** + * marks the position of the arrow showing + * the current setting being changed + * pass line and colum info + */ +void FourLineDisplayUsermod::setMarkLine(byte newMarkLineNum, byte newMarkColNum) { + markLineNum = newMarkLineNum; + markColNum = newMarkColNum; +} + +//Draw the arrow for the current setting being changed +void FourLineDisplayUsermod::drawArrow() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + if (markColNum != 255 && markLineNum !=255) drawGlyph(markColNum, markLineNum*lineHeight, 21, u8x8_4LineDisplay_WLED_icons_1x1); + lockRedraw = false; +} + +//Display the current effect or palette (desiredEntry) +// on the appropriate line (row). +void FourLineDisplayUsermod::showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + char lineBuffer[MAX_JSON_CHARS]; + if (overlayUntil == 0) { + lockRedraw = true; + // Find the mode name in JSON + uint8_t printedChars = extractModeName(inputEffPal, qstring, lineBuffer, MAX_JSON_CHARS-1); + if (lineBuffer[0]=='*' && lineBuffer[1]==' ') { + // remove "* " from dynamic palettes + for (byte i=2; i<=printedChars; i++) lineBuffer[i-2] = lineBuffer[i]; //include '\0' + printedChars -= 2; + } else if ((lineBuffer[0]==' ' && lineBuffer[1]>127)) { + // remove note symbol from effect names + for (byte i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' + printedChars -= 5; + } + if (lineHeight == 2) { // use this code for 8 line display + char smallBuffer1[MAX_MODE_LINE_SPACE]; + char smallBuffer2[MAX_MODE_LINE_SPACE]; + uint8_t smallChars1 = 0; + uint8_t smallChars2 = 0; + if (printedChars < MAX_MODE_LINE_SPACE) { // use big font if the text fits + while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' '; + lineBuffer[printedChars] = 0; + drawString(1, row*lineHeight, lineBuffer); + } else { // for long names divide the text into 2 lines and print them small + bool spaceHit = false; + for (uint8_t i = 0; i < printedChars; i++) { + switch (lineBuffer[i]) { + case ' ': + if (i > 4 && !spaceHit) { + spaceHit = true; + break; + } + if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; + else smallBuffer1[smallChars1++] = lineBuffer[i]; + break; + default: + if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; + else smallBuffer1[smallChars1++] = lineBuffer[i]; + break; + } + } + while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' '; + smallBuffer1[smallChars1] = 0; + drawString(1, row*lineHeight, smallBuffer1, true); + while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; + smallBuffer2[smallChars2] = 0; + drawString(1, row*lineHeight+1, smallBuffer2, true); + } + } else { // use this code for 4 ling displays + char smallBuffer3[MAX_MODE_LINE_SPACE+1]; // uses 1x1 icon for mode/palette + uint8_t smallChars3 = 0; + for (uint8_t i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; + smallBuffer3[smallChars3] = 0; + drawString(1, row*lineHeight, smallBuffer3, true); + } + lockRedraw = false; + } +} + +/** + * If there screen is off or in clock is displayed, + * this will return true. This allows us to throw away + * the first input from the rotary encoder but + * to wake up the screen. + */ +bool FourLineDisplayUsermod::wakeDisplay() { + if (type == NONE || !enabled) return false; + if (displayTurnedOff) { + #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return false; + #endif + lockRedraw = true; + clear(); + // Turn the display back on + sleepOrClock(false); + lockRedraw = false; + return true; + } + return false; +} + +/** + * Allows you to show one line and a glyph as overlay for a period of time. + * Clears the screen and prints. + * Used in Rotary Encoder usermod. + */ +void FourLineDisplayUsermod::overlay(const char* line1, long showHowLong, byte glyphType) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (glyphType>0 && glyphType<255) { + if (lineHeight == 2) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font + else drawGlyph(6, 0, glyphType, u8x8_4LineDisplay_WLED_icons_3x3, true); + } + if (line1) { + String buf = line1; + center(buf, getCols()); + drawString(0, (glyphType<255?3:0)*lineHeight, buf.c_str()); + } + overlayUntil = millis() + showHowLong; + lockRedraw = false; +} + +/** + * Allows you to show Akemi WLED logo overlay for a period of time. + * Clears the screen and prints. + */ +void FourLineDisplayUsermod::overlayLogo(long showHowLong) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (lineHeight == 2) { + //add a bit of randomness + switch (millis()%3) { + case 0: + //WLED + draw2x2Glyph( 0, 2, 1, u8x8_wled_logo_2x2); + draw2x2Glyph( 4, 2, 2, u8x8_wled_logo_2x2); + draw2x2Glyph( 8, 2, 3, u8x8_wled_logo_2x2); + draw2x2Glyph(12, 2, 4, u8x8_wled_logo_2x2); + break; + case 1: + //WLED Akemi + drawGlyph( 2, 2, 1, u8x8_wled_logo_akemi_4x4, true); + drawGlyph( 6, 2, 2, u8x8_wled_logo_akemi_4x4, true); + drawGlyph(10, 2, 3, u8x8_wled_logo_akemi_4x4, true); + break; + case 2: + //Akemi + //draw2x2Glyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_3x3); // use this if flash runs short and comment out 6x6 font + drawGlyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_6x6, true); + drawString(6, 6, "WLED"); + break; + } + } else { + switch (millis()%3) { + case 0: + //WLED + draw2x2Glyph( 0, 0, 1, u8x8_wled_logo_2x2); + draw2x2Glyph( 4, 0, 2, u8x8_wled_logo_2x2); + draw2x2Glyph( 8, 0, 3, u8x8_wled_logo_2x2); + draw2x2Glyph(12, 0, 4, u8x8_wled_logo_2x2); + break; + case 1: + //WLED Akemi + drawGlyph( 2, 0, 1, u8x8_wled_logo_akemi_4x4); + drawGlyph( 6, 0, 2, u8x8_wled_logo_akemi_4x4); + drawGlyph(10, 0, 3, u8x8_wled_logo_akemi_4x4); + break; + case 2: + //Akemi + //drawGlyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_4x4); // a bit nicer, but uses extra 1.5k flash + draw2x2Glyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_2x2); + break; + } + } + overlayUntil = millis() + showHowLong; + lockRedraw = false; +} + +/** + * Allows you to show two lines as overlay for a period of time. + * Clears the screen and prints. + * Used in Auto Save usermod + */ +void FourLineDisplayUsermod::overlay(const char* line1, const char* line2, long showHowLong) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (line1) { + String buf = line1; + center(buf, getCols()); + drawString(0, 1*lineHeight, buf.c_str()); + } + if (line2) { + String buf = line2; + center(buf, getCols()); + drawString(0, 2*lineHeight, buf.c_str()); + } + overlayUntil = millis() + showHowLong; + lockRedraw = false; +} + +void FourLineDisplayUsermod::networkOverlay(const char* line1, long showHowLong) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + + String line; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (line1) { + line = line1; + center(line, getCols()); + drawString(0, 0, line.c_str()); + } + // Second row with Wifi name + line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); + if (line.length() < getCols()) center(line, getCols()); + drawString(0, lineHeight, line.c_str()); + // Print `~` char to indicate that SSID is longer, than our display + if (knownSsid.length() > getCols()) { + drawString(getCols() - 1, 0, "~"); + } + // Third row with IP and Password in AP Mode + line = knownIp.toString(); + center(line, getCols()); + drawString(0, lineHeight*2, line.c_str()); + line = ""; + if (apActive) { + line = apPass; + } else if (strcmp(serverDescription, "WLED") != 0) { + line = serverDescription; + } + center(line, getCols()); + drawString(0, lineHeight*3, line.c_str()); + overlayUntil = millis() + showHowLong; + lockRedraw = false; +} + + +/** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ +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) { + return false; + } + + unsigned long now = millis(); + static bool buttonPressedBefore = false; + static bool buttonLongPressed = false; + static unsigned long buttonPressedTime = 0; + static unsigned long buttonWaitTime = 0; + bool handled = false; + + //momentary button logic + if (isButtonPressed(b)) { //pressed + + if (!buttonPressedBefore) buttonPressedTime = now; + buttonPressedBefore = true; + + if (now - buttonPressedTime > 600) { //long press + //TODO: handleButton() handles button 0 without preset in a different way for double click + //so we need to override with same behaviour + //DEBUG_PRINTLN(F("4LD action.")); + //if (!buttonLongPressed) longPressAction(0); + buttonLongPressed = true; + return false; + } + + } else if (!isButtonPressed(b) && buttonPressedBefore) { //released + + long dur = now - buttonPressedTime; + if (dur < 50) { + buttonPressedBefore = false; + return true; + } //too short "press", debounce + + bool doublePress = buttonWaitTime; //did we have short press before? + buttonWaitTime = 0; + + if (!buttonLongPressed) { //short press + // if this is second release within 350ms it is a double press (buttonWaitTime!=0) + //TODO: handleButton() handles button 0 without preset in a different way for double click + if (doublePress) { + networkOverlay(PSTR("NETWORK INFO"),7000); + handled = true; + } else { + buttonWaitTime = now; + } + } + buttonPressedBefore = false; + buttonLongPressed = false; + } + // if 350ms elapsed since last press/release it is a short press + if (buttonWaitTime && now - buttonWaitTime > 350 && !buttonPressedBefore) { + buttonWaitTime = 0; + //TODO: handleButton() handles button 0 without preset in a different way for double click + //so we need to override with same behaviour + //shortPressAction(0); + //handled = false; + } + return handled; +} + +#if CONFIG_FREERTOS_UNICORE +#define ARDUINO_RUNNING_CORE 0 +#else +#define ARDUINO_RUNNING_CORE 1 +#endif +void FourLineDisplayUsermod::onUpdateBegin(bool init) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + if (init && Display_Task) { + vTaskSuspend(Display_Task); // update is about to begin, disable task to prevent crash + } else { + // update has failed or create task requested + if (Display_Task) + vTaskResume(Display_Task); + else + xTaskCreatePinnedToCore( + [](void * par) { // Function to implement the task + // see https://www.freertos.org/vtaskdelayuntil.html + const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; + TickType_t xLastWakeTime = xTaskGetTickCount(); + for(;;) { + delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. + // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. + vTaskDelayUntil(&xLastWakeTime, xFrequency); // release CPU, by doing nothing for REFRESH_RATE_MS millis + FourLineDisplayUsermod::getInstance()->redraw(false); + } + }, + "4LD", // Name of the task + 3072, // Stack size in words + NULL, // Task input parameter + 1, // Priority of the task (not idle) + &Display_Task, // Task handle + ARDUINO_RUNNING_CORE + ); + } +#endif +} + +/* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ +//void FourLineDisplayUsermod::addToJsonInfo(JsonObject& root) { + //JsonObject user = root["u"]; + //if (user.isNull()) user = root.createNestedObject("u"); + //JsonArray data = user.createNestedArray(F("4LineDisplay")); + //data.add(F("Loaded.")); +//} + +/* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ +//void FourLineDisplayUsermod::addToJsonState(JsonObject& root) { +//} + +/* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ +//void FourLineDisplayUsermod::readFromJsonState(JsonObject& root) { +// if (!initDone) return; // prevent crash on boot applyPreset() +//} + +void FourLineDisplayUsermod::appendConfigData() { + oappend(SET_F("dd=addDropdown('4LineDisplay','type');")); + oappend(SET_F("addOption(dd,'None',0);")); + oappend(SET_F("addOption(dd,'SSD1306',1);")); + oappend(SET_F("addOption(dd,'SH1106',2);")); + oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); + oappend(SET_F("addOption(dd,'SSD1305',4);")); + oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); + oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); + oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); + oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);")); + oappend(SET_F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); + oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); + oappend(SET_F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); + oappend(SET_F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); +} + +/* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ +void FourLineDisplayUsermod::addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + + top["type"] = type; + JsonArray io_pin = top.createNestedArray("pin"); + for (int i=0; i<3; i++) io_pin.add(ioPin[i]); + top[FPSTR(_flip)] = (bool) flip; + top[FPSTR(_contrast)] = contrast; + top[FPSTR(_contrastFix)] = (bool) contrastFix; + #ifndef ARDUINO_ARCH_ESP32 + top[FPSTR(_refreshRate)] = refreshRate; + #endif + top[FPSTR(_screenTimeOut)] = screenTimeout/1000; + top[FPSTR(_sleepMode)] = (bool) sleepMode; + top[FPSTR(_clockMode)] = (bool) clockMode; + top[FPSTR(_showSeconds)] = (bool) showSeconds; + top[FPSTR(_busClkFrequency)] = ioFrequency/1000; + DEBUG_PRINTLN(F("4 Line Display config saved.")); +} + +/* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ +bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { + bool needsRedraw = false; + DisplayType newType = type; + int8_t oldPin[3]; for (byte i=0; i<3; i++) oldPin[i] = ioPin[i]; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_enabled)] | enabled; + newType = top["type"] | newType; + for (byte i=0; i<3; i++) ioPin[i] = top["pin"][i] | ioPin[i]; + flip = top[FPSTR(_flip)] | flip; + contrast = top[FPSTR(_contrast)] | contrast; + #ifndef ARDUINO_ARCH_ESP32 + refreshRate = top[FPSTR(_refreshRate)] | refreshRate; + refreshRate = min(5000, max(250, (int)refreshRate)); + #endif + screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; + sleepMode = top[FPSTR(_sleepMode)] | sleepMode; + clockMode = top[FPSTR(_clockMode)] | clockMode; + showSeconds = top[FPSTR(_showSeconds)] | showSeconds; + contrastFix = top[FPSTR(_contrastFix)] | contrastFix; + if (newType == SSD1306_SPI || newType == SSD1306_SPI64) + ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency + else + ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + type = newType; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + bool pinsChanged = false; + for (byte i=0; i<3; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } + if (pinsChanged || type!=newType) { + bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); + bool newSPI = (newType == SSD1306_SPI || newType == SSD1306_SPI64 || newType == SSD1309_SPI64); + if (isSPI) { + if (pinsChanged || !newSPI) pinManager.deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay); + if (!newSPI) { + // was SPI but is no longer SPI + if (i2c_scl<0 || i2c_sda<0) { newType=NONE; } + } else { + // still SPI but pins changed + PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; + if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } + else if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + } + } else if (newSPI) { + // was I2C but is now SPI + if (spi_sclk<0 || spi_mosi<0) { + newType=NONE; + } else { + PinManagerPinType pins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; + if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } + else if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + } + } else { + // just I2C type changed + } + type = newType; + switch (type) { + case SSD1306: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SH1106: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_sh1106_128x64_winstar, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SSD1306_64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SSD1305: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x32_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SSD1305_64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SSD1306_SPI: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset + break; + case SSD1306_SPI64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset + break; + case SSD1309_SPI64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1309_128x64_noname0, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset + default: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_null_cb, u8x8_cad_empty, u8x8_byte_empty, u8x8_dummy_cb); + enabled = false; + break; + } + startDisplay(); + needsRedraw |= true; + } else { + u8x8->setBusClock(ioFrequency); // can be used for SPI too + setVcomh(contrastFix); + setContrast(contrast); + setFlipMode(flip); + } + knownHour = 99; + if (needsRedraw && !wakeDisplay()) redraw(true); + else overlayLogo(3500); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_contrastFix)].isNull(); +} diff --git a/usermods/usermod_v2_klipper_percentage/readme.md b/usermods/usermod_v2_klipper_percentage/readme.md new file mode 100644 index 0000000000..e967d6b217 --- /dev/null +++ b/usermods/usermod_v2_klipper_percentage/readme.md @@ -0,0 +1,40 @@ +# Klipper Percentage Usermod +This usermod polls the Klipper API every 10s for the progressvalue. +The leds are then filled with a solid color according to that progress percentage. +the solid color is the secondary color of the segment. + +A corresponding curl command would be: +``` +curl --location --request GET 'http://[]/printer/objects/query?virtual_sdcard=progress' +``` +## Usage +Compile the source with the buildflag `-D USERMOD_KLIPPER_PERCENTAGE` added. + +You can also use the WLBD bot in the Discord by simply extending an existing build environment: +``` +[env:esp32klipper] +extends = env:esp32dev +build_flags = ${common.build_flags_esp32} -D USERMOD_KLIPPER_PERCENTAGE +``` + +## Settings + +### Enabled: +Checkbox to enable or disable the overlay + +### Klipper IP: +IP address of your Klipper instance you want to poll. ESP has to be restarted after change + +### Direction : +0 = normal + +1 = reversed + +2 = center + +----- +Author: + +Sören Willrodt + +Discord: Sören#5281 \ No newline at end of file diff --git a/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h b/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h new file mode 100644 index 0000000000..2f591b1547 --- /dev/null +++ b/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h @@ -0,0 +1,222 @@ +#pragma once + +#include "wled.h" + +class klipper_percentage : public Usermod +{ +private: + unsigned long lastTime = 0; + String ip = "192.168.25.207"; + WiFiClient wifiClient; + char errorMessage[100] = ""; + int printPercent = 0; + int direction = 0; // 0 for along the strip, 1 for reversed direction + + static const char _name[]; + static const char _enabled[]; + bool enabled = false; + + void httpGet(WiFiClient &client, char *errorMessage) + { + // https://arduinojson.org/v6/example/http-client/ + // is this the most compact way to do http get and put it in arduinojson object??? + // would like async response ... ??? + client.setTimeout(10000); + if (!client.connect(ip.c_str(), 80)) + { + strcat(errorMessage, PSTR("Connection failed")); + } + else + { + // Send HTTP request + client.println(F("GET /printer/objects/query?virtual_sdcard=progress HTTP/1.0")); + client.println("Host: " + ip); + client.println(F("Connection: close")); + if (client.println() == 0) + { + strcat(errorMessage, PSTR("Failed to send request")); + } + else + { + // Check HTTP status + char status[32] = {0}; + client.readBytesUntil('\r', status, sizeof(status)); + if (strcmp(status, "HTTP/1.1 200 OK") != 0) + { + strcat(errorMessage, PSTR("Unexpected response: ")); + strcat(errorMessage, status); + } + else + { + // Skip HTTP headers + char endOfHeaders[] = "\r\n\r\n"; + if (!client.find(endOfHeaders)) + { + strcat(errorMessage, PSTR("Invalid response")); + } + } + } + } + } + +public: + void setup() + { + } + + void connected() + { + } + + void loop() + { + if (enabled) + { + if (WLED_CONNECTED) + { + if (millis() - lastTime > 10000) + { + httpGet(wifiClient, errorMessage); + if (strcmp(errorMessage, "") == 0) + { + PSRAMDynamicJsonDocument klipperDoc(4096); // in practice about 2673 + DeserializationError error = deserializeJson(klipperDoc, wifiClient); + if (error) + { + strcat(errorMessage, PSTR("deserializeJson() failed: ")); + strcat(errorMessage, error.c_str()); + } + printPercent = (int)(klipperDoc["result"]["status"]["virtual_sdcard"]["progress"].as() * 100); + + DEBUG_PRINT("Percent: "); + DEBUG_PRINTLN((int)(klipperDoc["result"]["status"]["virtual_sdcard"]["progress"].as() * 100)); + DEBUG_PRINT("LEDs: "); + DEBUG_PRINTLN(direction == 2 ? (strip.getLengthTotal() / 2) * printPercent / 100 : strip.getLengthTotal() * printPercent / 100); + } + else + { + DEBUG_PRINTLN(errorMessage); + DEBUG_PRINTLN(ip); + } + lastTime = millis(); + } + } + } + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject("Klipper Printing Percentage"); + top["Enabled"] = enabled; + top["Klipper IP"] = ip; + top["Direction"] = direction; + } + + bool readFromConfig(JsonObject &root) + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + + JsonObject top = root["Klipper Printing Percentage"]; + + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top["Klipper IP"], ip); + configComplete &= getJsonValue(top["Enabled"], enabled); + configComplete &= getJsonValue(top["Direction"], direction); + return configComplete; + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + void addToJsonInfo(JsonObject &root) + { + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + String uiDomString = F(""); + infoArr.add(uiDomString); + } + + void addToJsonState(JsonObject &root) + { + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) + { + usermod = root.createNestedObject(FPSTR(_name)); + } + usermod["on"] = enabled; + } + void readFromJsonState(JsonObject &root) + { + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) + { + if (usermod[FPSTR(_enabled)].is()) + { + enabled = usermod[FPSTR(_enabled)].as(); + } + } + } + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() + { + if (enabled) + { + if (direction == 0) // normal + { + for (int i = 0; i < strip.getLengthTotal() * printPercent / 100; i++) + { + strip.setPixelColor(i, strip.getSegment(0).colors[1]); + } + } + else if (direction == 1) // reversed + { + for (int i = 0; i < strip.getLengthTotal() * printPercent / 100; i++) + { + strip.setPixelColor(strip.getLengthTotal() - i, strip.getSegment(0).colors[1]); + } + } + else if (direction == 2) // center + { + for (int i = 0; i < (strip.getLengthTotal() / 2) * printPercent / 100; i++) + { + strip.setPixelColor((strip.getLengthTotal() / 2) + i, strip.getSegment(0).colors[1]); + strip.setPixelColor((strip.getLengthTotal() / 2) - i, strip.getSegment(0).colors[1]); + } + } + else + { + direction = 0; + } + } + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_KLIPPER; + } +}; +const char klipper_percentage::_name[] PROGMEM = "Klipper_Percentage"; +const char klipper_percentage::_enabled[] PROGMEM = "enabled"; \ No newline at end of file diff --git a/usermods/usermod_v2_mode_sort/readme.md b/usermods/usermod_v2_mode_sort/readme.md index b4fe90e73e..c24322f32e 100644 --- a/usermods/usermod_v2_mode_sort/readme.md +++ b/usermods/usermod_v2_mode_sort/readme.md @@ -8,26 +8,26 @@ palettes to other usermods. Notably it provides: ```char **getModesQStrings()``` -Provides an array of char* (pointers) to the names of the -palettes within JSON_mode_names, in the same order as +Provides a char* array (pointers) to the names of the +palettes contained in JSON_mode_names, in the same order as JSON_mode_names. These strings end in double quote (") (or \0 if there is a problem). ```byte *getModesAlphaIndexes()``` -An array of byte designating the indexes of names of the +A byte array designating the indexes of names of the modes in alphabetical order. "Solid" will always remain -at the front of the list. +at the top of the list. ```char **getPalettesQStrings()``` -Provides an array of char* (pointers) to the names of the -palettes within JSON_palette_names, in the same order as +Provides a char* array (pointers) to the names of the +palettes contained in JSON_palette_names, in the same order as JSON_palette_names. These strings end in double quote (") (or \0 if there is a problem). ```byte *getPalettesAlphaIndexes()``` -An array of byte designating the indexes of names of the +A byte array designating the indexes of names of the palettes in alphabetical order. "Default" and those -starting with "(" will always remain at the front of the list. +starting with "(" will always remain at the top of the list. diff --git a/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h b/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h index 2be7ce84c0..092206bb6d 100644 --- a/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h +++ b/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h @@ -162,7 +162,6 @@ class ModeSortUsermod : public Usermod { break; } } - re_sortModes(palettes_qstrings, palettes_alpha_indexes, strip.getPaletteCount(), skipPaletteCount); } @@ -189,6 +188,7 @@ class ModeSortUsermod : public Usermod { bool complete = false; for (size_t i = 0; i < strlen_P(json); i++) { singleJsonSymbol = pgm_read_byte_near(json + i); + if (singleJsonSymbol == '\0') break; switch (singleJsonSymbol) { case '"': insideQuotes = !insideQuotes; @@ -200,18 +200,14 @@ class ModeSortUsermod : public Usermod { case '[': break; case ']': - complete = true; + if (!insideQuotes) complete = true; break; case ',': - modeIndex++; + if (!insideQuotes) modeIndex++; default: - if (!insideQuotes) { - break; - } - } - if (complete) { - break; + if (!insideQuotes) break; } + if (complete) break; } return modeStrings; } diff --git a/usermods/usermod_v2_ping_pong_clock/readme.md b/usermods/usermod_v2_ping_pong_clock/readme.md new file mode 100644 index 0000000000..f8219489d1 --- /dev/null +++ b/usermods/usermod_v2_ping_pong_clock/readme.md @@ -0,0 +1,10 @@ +# Ping Pong LED Clock + +Contains a modification to use WLED in combination with the Ping Pong Ball LED Clock as built in [Instructables](https://www.instructables.com/Ping-Pong-Ball-LED-Clock/). + +## Installation + +To install this Usermod, you instruct PlatformIO to compile the Project with the USERMOD_PING_PONG_CLOCK flag. +WLED then automatically provides you with various settings on the Usermod Page. + +Note: Depending on the size of your clock, you may have to update the led indices for the individual numbers and the base indices. diff --git a/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h b/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h new file mode 100644 index 0000000000..40ff675c08 --- /dev/null +++ b/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h @@ -0,0 +1,119 @@ +#pragma once + +#include "wled.h" + +class PingPongClockUsermod : public Usermod +{ +private: + // Private class members. You can declare variables and functions only accessible to your usermod here + unsigned long lastTime = 0; + bool colonOn = true; + + // ---- Variables modified by settings below ----- + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + bool pingPongClockEnabled = true; + int colorR = 0xFF; + int colorG = 0xFF; + int colorB = 0xFF; + + // ---- Variables for correct LED numbering below, edit only if your clock is built different ---- + + int baseH = 43; // Address for the one place of the hours + int baseHH = 7; // Address for the tens place of the hours + int baseM = 133; // Address for the one place of the minutes + int baseMM = 97; // Address for the tens place of the minutes + int colon1 = 79; // Address for the first colon led + int colon2 = 80; // Address for the second colon led + + // Matrix for the illumination of the numbers + // Note: These only define the increments of the base address. e.g. to define the second Minute you have to add the baseMM to every led position + const int numbers[10][10] = + { + { 0, 1, 4, 6, 13, 15, 18, 19, -1, -1 }, // 0: null + { 13, 14, 15, 18, 19, -1, -1, -1, -1, -1 }, // 1: eins + { 0, 4, 5, 6, 13, 14, 15, 19, -1, -1 }, // 2: zwei + { 4, 5, 6, 13, 14, 15, 18, 19, -1, -1 }, // 3: drei + { 1, 4, 5, 14, 15, 18, 19, -1, -1, -1 }, // 4: vier + { 1, 4, 5, 6, 13, 14, 15, 18, -1, -1 }, // 5: fünf + { 0, 5, 6, 10, 13, 14, 15, 18, -1, -1 }, // 6: sechs + { 4, 6, 9, 13, 14, 19, -1, -1, -1, -1 }, // 7: sieben + { 0, 1, 4, 5, 6, 13, 14, 15, 18, 19 }, // 8: acht + { 1, 4, 5, 6, 9, 13, 14, 19, -1, -1 } // 9: neun + }; + +public: + void setup() + { } + + void loop() + { + if (millis() - lastTime > 1000) + { + lastTime = millis(); + colonOn = !colonOn; + } + } + + void addToJsonInfo(JsonObject& root) + { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray lightArr = user.createNestedArray("Uhrzeit-Anzeige"); //name + lightArr.add(pingPongClockEnabled ? "aktiv" : "inaktiv"); //value + lightArr.add(""); //unit + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject("Ping Pong Clock"); + top["enabled"] = pingPongClockEnabled; + top["colorR"] = colorR; + top["colorG"] = colorG; + top["colorB"] = colorB; + } + + bool readFromConfig(JsonObject &root) + { + JsonObject top = root["Ping Pong Clock"]; + + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top["enabled"], pingPongClockEnabled); + configComplete &= getJsonValue(top["colorR"], colorR); + configComplete &= getJsonValue(top["colorG"], colorG); + configComplete &= getJsonValue(top["colorB"], colorB); + + return configComplete; + } + + void drawNumber(int base, int number) + { + for(int i = 0; i < 10; i++) + { + if(numbers[number][i] > -1) + strip.setPixelColor(numbers[number][i] + base, RGBW32(colorR, colorG, colorB, 0)); + } + } + + void handleOverlayDraw() + { + if(pingPongClockEnabled){ + if(colonOn) + { + strip.setPixelColor(colon1, RGBW32(colorR, colorG, colorB, 0)); + strip.setPixelColor(colon2, RGBW32(colorR, colorG, colorB, 0)); + } + drawNumber(baseHH, (hour(localTime) / 10) % 10); + drawNumber(baseH, hour(localTime) % 10); + drawNumber(baseM, (minute(localTime) / 10) % 10); + drawNumber(baseMM, minute(localTime) % 10); + } + } + + uint16_t getId() + { + return USERMOD_ID_PING_PONG_CLOCK; + } + +}; diff --git a/usermods/usermod_v2_rotary_encoder_ui/readme.md b/usermods/usermod_v2_rotary_encoder_ui/readme.md index 9bbcd6a6ca..5e4f3cff63 100644 --- a/usermods/usermod_v2_rotary_encoder_ui/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui/readme.md @@ -15,11 +15,17 @@ This file should be placed in the same directory as `platformio.ini`. ### Define Your Options -* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) -* `ENCODER_DT_PIN` - The encoders DT pin, defaults to 12 -* `ENCODER_CLK_PIN` - The encoders CLK pin, defaults to 14 -* `ENCODER_SW_PIN` - The encoders SW pin, defaults to 13 +* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp +* `USERMOD_ROTARY_ENCODER_GPIO` - define the GPIO function (INPUT, INPUT_PULLUP, etc...) +* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp + also tells this usermod that the display is available + (see the Four Line Display usermod `readme.md` for more details) +* `ENCODER_DT_PIN`   - defaults to 12 +* `ENCODER_CLK_PIN` - defaults to 14 +* `ENCODER_SW_PIN`   - defaults to 13 +* `USERMOD_ROTARY_ENCODER_GPIO` - GPIO functionality: + `INPUT_PULLUP` to use internal pull-up + `INPUT` to use pull-up on the PCB ### PlatformIO requirements diff --git a/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h index 0ea77e8442..9e207339f1 100644 --- a/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h +++ b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h @@ -20,7 +20,7 @@ // Change between modes by pressing a button. // // Dependencies -// * This usermod REQURES the ModeSortUsermod +// * This usermod REQUIRES the ModeSortUsermod // * This Usermod works best coupled with // FourLineDisplayUsermod. // @@ -97,6 +97,7 @@ class RotaryEncoderUIUsermod : public Usermod { */ void setup() { + DEBUG_PRINTLN(F("Usermod Rotary Encoder init.")); PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { // BUG: configuring this usermod with conflicting pins @@ -109,9 +110,13 @@ class RotaryEncoderUIUsermod : public Usermod { return; } - pinMode(pinA, INPUT_PULLUP); - pinMode(pinB, INPUT_PULLUP); - pinMode(pinC, INPUT_PULLUP); + #ifndef USERMOD_ROTARY_ENCODER_GPIO + #define USERMOD_ROTARY_ENCODER_GPIO INPUT_PULLUP + #endif + pinMode(pinA, USERMOD_ROTARY_ENCODER_GPIO); + pinMode(pinB, USERMOD_ROTARY_ENCODER_GPIO); + pinMode(pinC, USERMOD_ROTARY_ENCODER_GPIO); + currentTime = millis(); loopTime = currentTime; @@ -439,14 +444,11 @@ class RotaryEncoderUIUsermod : public Usermod { DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } - int8_t newDTpin = pinA; - int8_t newCLKpin = pinB; - int8_t newSWpin = pinC; + int8_t newDTpin = top[FPSTR(_DT_pin)] | pinA; + int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB; + int8_t newSWpin = top[FPSTR(_SW_pin)] | pinC; enabled = top[FPSTR(_enabled)] | enabled; - newDTpin = top[FPSTR(_DT_pin)] | newDTpin; - newCLKpin = top[FPSTR(_CLK_pin)] | newCLKpin; - newSWpin = top[FPSTR(_SW_pin)] | newSWpin; DEBUG_PRINT(FPSTR(_name)); if (!initDone) { diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md index a140f25b91..516362380d 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -4,12 +4,12 @@ Thank you to the authors of the original version of these usermods. It would not "usermod_v2_four_line_display" "usermod_v2_rotary_encoder_ui" -The core of these usermods are a copy of the originals. The main changes are done to the FourLineDisplay usermod. +The core of these usermods are a copy of the originals. The main changes are to the FourLineDisplay usermod. The display usermod UI has been completely changed. The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. -Without the display it functions identical to the original. +Without the display, it functions identical to the original. The original "usermod_v2_auto_save" will not work with the display just yet. Press the encoder to cycle through the options: @@ -22,17 +22,17 @@ Press the encoder to cycle through the options: *Saturation (only if display is used) Press and hold the encoder to display Network Info - if AP is active then it will display AP ssid and Password + if AP is active, it will display the AP, SSID and Password -Also shows if the timer is enabled +Also shows if the timer is enabled. [See the pair of usermods in action](https://www.youtube.com/watch?v=ulZnBt9z3TI) ## Installation -Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions -Then to activate this alternative usermod add `#define USE_ALT_DISPlAY` to the `usermods_list.cpp` file, - or add `-D USE_ALT_DISPlAY` to the original `platformio_override.ini.sample` file +Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions.
+To activate this alternative usermod, add `#define USE_ALT_DISPlAY` to the `usermods_list.cpp` file, +or add `-D USE_ALT_DISPlAY` to the original `platformio_override.ini.sample` file. ### PlatformIO requirements @@ -42,4 +42,4 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. ## Change Log 2021-10 -* First public release \ No newline at end of file +* First public release diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 4629f547a7..20af680000 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -4,7 +4,7 @@ // // Inspired by the original v2 usermods -// * usermod_v2_rotaty_encoder_ui +// * usermod_v2_rotary_encoder_ui // // v2 usermod that provides a rotary encoder-based UI. // @@ -45,9 +45,29 @@ #define ENCODER_SW_PIN 19 #endif -// The last UI state, remove color and saturation option if diplay not active(too many options) +#ifndef ENCODER_MAX_DELAY_MS // max delay between polling encoder pins +#define ENCODER_MAX_DELAY_MS 8 // 8 milliseconds => max 120 change impulses in 1 second, for full turn of a 30/30 encoder (4 changes per segment, 30 segments for one turn) +#endif + +#ifndef USERMOD_USE_PCF8574 + #undef USE_PCF8574 + #define USE_PCF8574 false +#else + #undef USE_PCF8574 + #define USE_PCF8574 true +#endif + +#ifndef PCF8574_ADDRESS + #define PCF8574_ADDRESS 0x20 // some may start at 0x38 +#endif + +#ifndef PCF8574_INT_PIN + #define PCF8574_INT_PIN -1 // GPIO connected to INT pin on PCF8574 +#endif + +// The last UI state, remove color and saturation option if display not active (too many options) #ifdef USERMOD_FOUR_LINE_DISPLAY - #define LAST_UI_STATE 8 + #define LAST_UI_STATE 11 #else #define LAST_UI_STATE 4 #endif @@ -56,7 +76,7 @@ #define MODE_SORT_SKIP_COUNT 1 // Which list is being sorted -static char **listBeingSorted; +static const char **listBeingSorted; /** * Modes and palettes are stored as strings that @@ -65,8 +85,8 @@ static char **listBeingSorted; * JSON_mode_names or JSON_palette_names. */ static int re_qstringCmp(const void *ap, const void *bp) { - char *a = listBeingSorted[*((byte *)ap)]; - char *b = listBeingSorted[*((byte *)bp)]; + const char *a = listBeingSorted[*((byte *)ap)]; + const char *b = listBeingSorted[*((byte *)bp)]; int i = 0; do { char aVal = pgm_read_byte_near(a + i); @@ -79,7 +99,7 @@ static int re_qstringCmp(const void *ap, const void *bp) { // Lowercase bVal -= 32; } - // Relly we shouldn't ever get to '\0' + // Really we shouldn't ever get to '\0' if (aVal == '"' || bVal == '"' || aVal == '\0' || bVal == '\0') { // We're done. one is a substring of the other // or something happenend and the quote didn't stop us. @@ -113,709 +133,1035 @@ static int re_qstringCmp(const void *ap, const void *bp) { } +static volatile uint8_t pcfPortData = 0; // port expander port state +static volatile uint8_t addrPcf8574 = PCF8574_ADDRESS; // has to be accessible in ISR + +// Interrupt routine to read I2C rotary state +// if we are to use PCF8574 port expander we will need to rely on interrupts as polling I2C every 2ms +// is a waste of resources and causes 4LD to fail. +// in such case rely on ISR to read pin values and store them into static variable +static void IRAM_ATTR i2cReadingISR() { + Wire.requestFrom(addrPcf8574, 1U); + if (Wire.available()) { + pcfPortData = Wire.read(); + } +} + + class RotaryEncoderUIUsermod : public Usermod { -private: - int8_t fadeAmount = 5; // Amount to change every step (brightness) - unsigned long loopTime; - unsigned long buttonPressedTime = 0; - unsigned long buttonWaitTime = 0; - bool buttonPressedBefore = false; - bool buttonLongPressed = false; + private: - int8_t pinA = ENCODER_DT_PIN; // DT from encoder - int8_t pinB = ENCODER_CLK_PIN; // CLK from encoder - int8_t pinC = ENCODER_SW_PIN; // SW from encoder + const int8_t fadeAmount; // Amount to change every step (brightness) + unsigned long loopTime; - unsigned char select_state = 0; // 0: brightness, 1: effect, 2: effect speed, ... + unsigned long buttonPressedTime; + unsigned long buttonWaitTime; + bool buttonPressedBefore; + bool buttonLongPressed; - uint16_t currentHue1 = 16; // default boot color - byte currentSat1 = 255; - -#ifdef USERMOD_FOUR_LINE_DISPLAY - FourLineDisplayUsermod *display; -#else - void* display = nullptr; -#endif + int8_t pinA; // DT from encoder + int8_t pinB; // CLK from encoder + int8_t pinC; // SW from encoder - // Pointers the start of the mode names within JSON_mode_names - char **modes_qstrings = nullptr; + unsigned char select_state; // 0: brightness, 1: effect, 2: effect speed, ... - // Array of mode indexes in alphabetical order. - byte *modes_alpha_indexes = nullptr; + uint16_t currentHue1; // default boot color + byte currentSat1; + uint8_t currentCCT; + + #ifdef USERMOD_FOUR_LINE_DISPLAY + FourLineDisplayUsermod *display; + #else + void* display; + #endif - // Pointers the start of the palette names within JSON_palette_names - char **palettes_qstrings = nullptr; + // Pointers the start of the mode names within JSON_mode_names + const char **modes_qstrings; - // Array of palette indexes in alphabetical order. - byte *palettes_alpha_indexes = nullptr; + // Array of mode indexes in alphabetical order. + byte *modes_alpha_indexes; - unsigned char Enc_A; - unsigned char Enc_B; - unsigned char Enc_A_prev = 0; + // Pointers the start of the palette names within JSON_palette_names + const char **palettes_qstrings; - bool currentEffectAndPaletteInitialized = false; - uint8_t effectCurrentIndex = 0; - uint8_t effectPaletteIndex = 0; - uint8_t knownMode = 0; - uint8_t knownPalette = 0; + // Array of palette indexes in alphabetical order. + byte *palettes_alpha_indexes; - uint8_t currentCCT = 128; - bool isRgbw = false; + struct { // reduce memory footprint + bool Enc_A : 1; + bool Enc_B : 1; + bool Enc_A_prev : 1; + }; - byte presetHigh = 0; - byte presetLow = 0; + bool currentEffectAndPaletteInitialized; + uint8_t effectCurrentIndex; + uint8_t effectPaletteIndex; + uint8_t knownMode; + uint8_t knownPalette; - bool applyToAll = true; + byte presetHigh; + byte presetLow; - bool initDone = false; - bool enabled = true; + bool applyToAll; - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _DT_pin[]; - static const char _CLK_pin[]; - static const char _SW_pin[]; - static const char _presetHigh[]; - static const char _presetLow[]; - static const char _applyToAll[]; + bool initDone; + bool enabled; - /** - * Sort the modes and palettes to the index arrays - * modes_alpha_indexes and palettes_alpha_indexes. - */ - void sortModesAndPalettes() { - modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); - modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); - re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); + bool usePcf8574; + int8_t pinIRQ; - palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); - palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _DT_pin[]; + static const char _CLK_pin[]; + static const char _SW_pin[]; + static const char _presetHigh[]; + static const char _presetLow[]; + static const char _applyToAll[]; + static const char _pcf8574[]; + static const char _pcfAddress[]; + static const char _pcfINTpin[]; + + /** + * readPin() - read rotary encoder pin value + */ + byte readPin(uint8_t pin); + + /** + * Sort the modes and palettes to the index arrays + * modes_alpha_indexes and palettes_alpha_indexes. + */ + void sortModesAndPalettes(); + byte *re_initIndexArray(int numModes); + + /** + * Return an array of mode or palette names from the JSON string. + * They don't end in '\0', they end in '"'. + */ + const char **re_findModeStrings(const char json[], int numModes); + + /** + * Sort either the modes or the palettes using quicksort. + */ + void re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip); + + public: + + RotaryEncoderUIUsermod() + : fadeAmount(5) + , buttonPressedTime(0) + , buttonWaitTime(0) + , buttonPressedBefore(false) + , buttonLongPressed(false) + , pinA(ENCODER_DT_PIN) + , pinB(ENCODER_CLK_PIN) + , pinC(ENCODER_SW_PIN) + , select_state(0) + , currentHue1(16) + , currentSat1(255) + , currentCCT(128) + , display(nullptr) + , modes_qstrings(nullptr) + , modes_alpha_indexes(nullptr) + , palettes_qstrings(nullptr) + , palettes_alpha_indexes(nullptr) + , currentEffectAndPaletteInitialized(false) + , effectCurrentIndex(0) + , effectPaletteIndex(0) + , knownMode(0) + , knownPalette(0) + , presetHigh(0) + , presetLow(0) + , applyToAll(true) + , initDone(false) + , enabled(true) + , usePcf8574(USE_PCF8574) + , pinIRQ(PCF8574_INT_PIN) + {} + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { return USERMOD_ID_ROTARY_ENC_UI; } + /** + * Enable/Disable the usermod + */ + inline void enable(bool enable) { if (!(pinA<0 || pinB<0 || pinC<0)) enabled = enable; } + + /** + * Get usermod enabled/disabled state + */ + inline bool isEnabled() { return enabled; } + + /** + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup(); + + /** + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + //void connected(); + + /** + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop(); - // How many palette names start with '*' and should not be sorted? - // (Also skipping the first one, 'Default'). - int skipPaletteCount = 1; - while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount++]) == '*') ; - re_sortModes(palettes_qstrings, palettes_alpha_indexes, strip.getPaletteCount(), skipPaletteCount); +#ifndef WLED_DISABLE_MQTT + //bool onMqttMessage(char* topic, char* payload); + //void onMqttConnect(bool sessionPresent); +#endif + + /** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ + //bool handleButton(uint8_t b); + + /** + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + */ + //void addToJsonInfo(JsonObject &root); + + /** + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + //void addToJsonState(JsonObject &root); + + /** + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + //void readFromJsonState(JsonObject &root); + + /** + * provide the changeable values + */ + void addToConfig(JsonObject &root); + + void appendConfigData(); + + /** + * restore the changeable values + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject &root); + + // custom methods + void displayNetworkInfo(); + void findCurrentEffectAndPalette(); + bool changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph); + void lampUdated(); + void changeBrightness(bool increase); + void changeEffect(bool increase); + void changeEffectSpeed(bool increase); + void changeEffectIntensity(bool increase); + void changeCustom(uint8_t par, bool increase); + void changePalette(bool increase); + void changeHue(bool increase); + void changeSat(bool increase); + void changePreset(bool increase); + void changeCCT(bool increase); +}; + + +/** + * readPin() - read rotary encoder pin value + */ +byte RotaryEncoderUIUsermod::readPin(uint8_t pin) { + if (usePcf8574) { + if (pin >= 100) pin -= 100; // PCF I/O ports + return (pcfPortData>>pin) & 1; + } else { + return digitalRead(pin); } +} - byte *re_initIndexArray(int numModes) { - byte *indexes = (byte *)malloc(sizeof(byte) * numModes); - for (byte i = 0; i < numModes; i++) { - indexes[i] = i; - } - return indexes; - } - - /** - * Return an array of mode or palette names from the JSON string. - * They don't end in '\0', they end in '"'. - */ - char **re_findModeStrings(const char json[], int numModes) { - char **modeStrings = (char **)malloc(sizeof(char *) * numModes); - uint8_t modeIndex = 0; - bool insideQuotes = false; - // advance past the mark for markLineNum that may exist. - char singleJsonSymbol; - - // Find the mode name in JSON - bool complete = false; - for (size_t i = 0; i < strlen_P(json); i++) { - singleJsonSymbol = pgm_read_byte_near(json + i); - if (singleJsonSymbol == '\0') break; - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - if (insideQuotes) { - // We have a new mode or palette - modeStrings[modeIndex] = (char *)(json + i + 1); - } - break; - case '[': - break; - case ']': - if (!insideQuotes) complete = true; - break; - case ',': - if (!insideQuotes) modeIndex++; - default: - if (!insideQuotes) break; - } - if (complete) break; +/** + * Sort the modes and palettes to the index arrays + * modes_alpha_indexes and palettes_alpha_indexes. + */ +void RotaryEncoderUIUsermod::sortModesAndPalettes() { + DEBUG_PRINTLN(F("Sorting modes and palettes.")); + //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); + modes_qstrings = strip.getModeDataSrc(); + modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); + re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); + + palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()+strip.customPalettes.size()); + palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()+strip.customPalettes.size()); + if (strip.customPalettes.size()) { + for (int i=0; i= 0 && pinManager.allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) { + pinMode(pinIRQ, INPUT_PULLUP); + attachInterrupt(pinIRQ, i2cReadingISR, FALLING); // RISING, FALLING, CHANGE, ONLOW, ONHIGH + DEBUG_PRINTLN(F("Interrupt attached.")); + } else { + DEBUG_PRINTLN(F("Unable to allocate interrupt pin, disabling.")); + pinIRQ = -1; + enabled = false; + return; + } + } + } else { PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; - if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { - // BUG: configuring this usermod with conflicting pins - // will cause it to de-allocate pins it does not own - // (at second config) - // This is the exact type of bug solved by pinManager - // tracking the owner tags.... + if (pinA<0 || pinB<0 || !pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { pinA = pinB = pinC = -1; enabled = false; return; } - pinMode(pinA, INPUT_PULLUP); - pinMode(pinB, INPUT_PULLUP); - pinMode(pinC, INPUT_PULLUP); - loopTime = millis(); - - for (uint8_t s = 0; s < busses.getNumBusses(); s++) { - Bus *bus = busses.getBus(s); - if (!bus || bus->getLength()==0) break; - isRgbw |= bus->isRgbw(); - } + #ifndef USERMOD_ROTARY_ENCODER_GPIO + #define USERMOD_ROTARY_ENCODER_GPIO INPUT_PULLUP + #endif + pinMode(pinA, USERMOD_ROTARY_ENCODER_GPIO); + pinMode(pinB, USERMOD_ROTARY_ENCODER_GPIO); + if (pinC>=0) pinMode(pinC, USERMOD_ROTARY_ENCODER_GPIO); + } - currentCCT = (approximateKelvinFromRGB(RGBW32(col[0], col[1], col[2], col[3])) - 1900) >> 5; + loopTime = millis(); - if (!initDone) sortModesAndPalettes(); + currentCCT = (approximateKelvinFromRGB(RGBW32(col[0], col[1], col[2], col[3])) - 1900) >> 5; -#ifdef USERMOD_FOUR_LINE_DISPLAY - // This Usermod uses FourLineDisplayUsermod for the best experience. - // But it's optional. But you want it. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); - if (display != nullptr) { - display->setMarkLine(1, 0); - } -#endif + if (!initDone) sortModesAndPalettes(); - initDone = true; - Enc_A = digitalRead(pinA); // Read encoder pins - Enc_B = digitalRead(pinB); - Enc_A_prev = Enc_A; +#ifdef USERMOD_FOUR_LINE_DISPLAY + // This Usermod uses FourLineDisplayUsermod for the best experience. + // But it's optional. But you want it. + display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + if (display != nullptr) { + display->setMarkLine(1, 0); } +#endif - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() - { - //Serial.println("Connected to WiFi!"); - } - - /* - * loop() is called continuously. Here you can check for events, read sensors, etc. - * - * Tips: - * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. - * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. - * - * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. - * Instead, use a timer check as shown here. - */ - void loop() - { - if (!enabled || strip.isUpdating()) return; - unsigned long currentTime = millis(); // get the current elapsed time + initDone = true; + Enc_A = readPin(pinA); // Read encoder pins + Enc_B = readPin(pinB); + Enc_A_prev = Enc_A; +} - // Initialize effectCurrentIndex and effectPaletteIndex to - // current state. We do it here as (at least) effectCurrent - // is not yet initialized when setup is called. - - if (!currentEffectAndPaletteInitialized) { - findCurrentEffectAndPalette(); - } +/* + * loop() is called continuously. Here you can check for events, read sensors, etc. + * + * Tips: + * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. + * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. + * + * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. + * Instead, use a timer check as shown here. + */ +void RotaryEncoderUIUsermod::loop() +{ + if (!enabled) return; + unsigned long currentTime = millis(); // get the current elapsed time + if (strip.isUpdating() && ((currentTime - loopTime) < ENCODER_MAX_DELAY_MS)) return; // be nice, but not too nice + + // Initialize effectCurrentIndex and effectPaletteIndex to + // current state. We do it here as (at least) effectCurrent + // is not yet initialized when setup is called. + + if (!currentEffectAndPaletteInitialized) { + findCurrentEffectAndPalette(); + } - if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || palettes_alpha_indexes[effectPaletteIndex] != effectPalette) { - currentEffectAndPaletteInitialized = false; - } + if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || palettes_alpha_indexes[effectPaletteIndex] != effectPalette) { + DEBUG_PRINTLN(F("Current mode or palette changed.")); + currentEffectAndPaletteInitialized = false; + } - if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz - { - loopTime = currentTime; // Updates loopTime - - bool buttonPressed = !digitalRead(pinC); //0=pressed, 1=released - if (buttonPressed) { - if (!buttonPressedBefore) buttonPressedTime = currentTime; - buttonPressedBefore = true; - if (currentTime-buttonPressedTime > 3000) { - if (!buttonLongPressed) displayNetworkInfo(); //long press for network info - buttonLongPressed = true; - } - } else if (!buttonPressed && buttonPressedBefore) { - bool doublePress = buttonWaitTime; - buttonWaitTime = 0; - if (!buttonLongPressed) { - if (doublePress) { - toggleOnOff(); - lampUdated(); - } else { - buttonWaitTime = currentTime; - } - } - buttonLongPressed = false; - buttonPressedBefore = false; + if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz + { + bool buttonPressed = !readPin(pinC); //0=pressed, 1=released + if (buttonPressed) { + if (!buttonPressedBefore) buttonPressedTime = currentTime; + buttonPressedBefore = true; + if (currentTime-buttonPressedTime > 3000) { + if (!buttonLongPressed) displayNetworkInfo(); //long press for network info + buttonLongPressed = true; } - if (buttonWaitTime && currentTime-buttonWaitTime>350 && !buttonPressedBefore) { //same speed as in button.cpp - buttonWaitTime = 0; - char newState = select_state + 1; - bool changedState = true; - if (newState > LAST_UI_STATE || (newState == 8 && presetHigh==0 && presetLow == 0)) newState = 0; - if (display != nullptr) { - switch (newState) { - case 0: changedState = changeState(PSTR("Brightness"), 1, 0, 1); break; //1 = sun - case 1: changedState = changeState(PSTR("Speed"), 1, 4, 2); break; //2 = skip forward - case 2: changedState = changeState(PSTR("Intensity"), 1, 8, 3); break; //3 = fire - case 3: changedState = changeState(PSTR("Color Palette"), 2, 0, 4); break; //4 = custom palette - case 4: changedState = changeState(PSTR("Effect"), 3, 0, 5); break; //5 = puzzle piece - case 5: changedState = changeState(PSTR("Main Color"), 255, 255, 7); break; //7 = brush - case 6: changedState = changeState(PSTR("Saturation"), 255, 255, 8); break; //8 = contrast - case 7: changedState = changeState(PSTR("CCT"), 255, 255, 10); break; //10 = star - case 8: changedState = changeState(PSTR("Preset"), 255, 255, 11); break; //11 = heart - } + } else if (!buttonPressed && buttonPressedBefore) { + bool doublePress = buttonWaitTime; + buttonWaitTime = 0; + if (!buttonLongPressed) { + if (doublePress) { + toggleOnOff(); + lampUdated(); + } else { + buttonWaitTime = currentTime; } - if (changedState) select_state = newState; } - - Enc_A = digitalRead(pinA); // Read encoder pins - Enc_B = digitalRead(pinB); - if ((Enc_A) && (!Enc_A_prev)) - { // A has gone from high to low - if (Enc_B == LOW) //changes to LOW so that then encoder registers a change at the very end of a pulse - { // B is high so clockwise - switch(select_state) { - case 0: changeBrightness(true); break; - case 1: changeEffectSpeed(true); break; - case 2: changeEffectIntensity(true); break; - case 3: changePalette(true); break; - case 4: changeEffect(true); break; - case 5: changeHue(true); break; - case 6: changeSat(true); break; - case 7: changeCCT(true); break; - case 8: changePreset(true); break; - } + buttonLongPressed = false; + buttonPressedBefore = false; + } + if (buttonWaitTime && currentTime-buttonWaitTime>350 && !buttonPressedBefore) { //same speed as in button.cpp + buttonWaitTime = 0; + char newState = select_state + 1; + bool changedState = false; + char lineBuffer[64]; + do { + // find new state + switch (newState) { + case 0: strcpy_P(lineBuffer, PSTR("Brightness")); changedState = true; break; + case 1: if (!extractModeSlider(effectCurrent, 0, lineBuffer, 63)) newState++; else changedState = true; break; // speed + case 2: if (!extractModeSlider(effectCurrent, 1, lineBuffer, 63)) newState++; else changedState = true; break; // intensity + case 3: strcpy_P(lineBuffer, PSTR("Color Palette")); changedState = true; break; + case 4: strcpy_P(lineBuffer, PSTR("Effect")); changedState = true; break; + case 5: strcpy_P(lineBuffer, PSTR("Main Color")); changedState = true; break; + case 6: strcpy_P(lineBuffer, PSTR("Saturation")); changedState = true; break; + case 7: + if (!(strip.getSegment(applyToAll ? strip.getFirstSelectedSegId() : strip.getMainSegmentId()).getLightCapabilities() & 0x04)) newState++; + else { strcpy_P(lineBuffer, PSTR("CCT")); changedState = true; } + break; + case 8: if (presetHigh==0 || presetLow == 0) newState++; else { strcpy_P(lineBuffer, PSTR("Preset")); changedState = true; } break; + case 9: + case 10: + case 11: if (!extractModeSlider(effectCurrent, newState-7, lineBuffer, 63)) newState++; else changedState = true; break; // custom } - else if (Enc_B == HIGH) - { // B is low so counter-clockwise - switch(select_state) { - case 0: changeBrightness(false); break; - case 1: changeEffectSpeed(false); break; - case 2: changeEffectIntensity(false); break; - case 3: changePalette(false); break; - case 4: changeEffect(false); break; - case 5: changeHue(false); break; - case 6: changeSat(false); break; - case 7: changeCCT(false); break; - case 8: changePreset(false); break; - } + if (newState > LAST_UI_STATE) newState = 0; + } while (!changedState); + if (display != nullptr) { + switch (newState) { + case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun + case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward + case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire + case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette + case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece + case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush + case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast + case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart + case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star } } - Enc_A_prev = Enc_A; // Store value of A for next time + if (changedState) select_state = newState; } - } - void displayNetworkInfo() { - #ifdef USERMOD_FOUR_LINE_DISPLAY - display->networkOverlay(PSTR("NETWORK INFO"), 10000); - #endif - } - - void findCurrentEffectAndPalette() { - currentEffectAndPaletteInitialized = true; - for (uint8_t i = 0; i < strip.getModeCount(); i++) { - if (modes_alpha_indexes[i] == effectCurrent) { - effectCurrentIndex = i; - break; + Enc_A = readPin(pinA); // Read encoder pins + Enc_B = readPin(pinB); + if ((Enc_A) && (!Enc_A_prev)) + { // A has gone from high to low + if (Enc_B == LOW) //changes to LOW so that then encoder registers a change at the very end of a pulse + { // B is high so clockwise + switch(select_state) { + case 0: changeBrightness(true); break; + case 1: changeEffectSpeed(true); break; + case 2: changeEffectIntensity(true); break; + case 3: changePalette(true); break; + case 4: changeEffect(true); break; + case 5: changeHue(true); break; + case 6: changeSat(true); break; + case 7: changeCCT(true); break; + case 8: changePreset(true); break; + case 9: changeCustom(1,true); break; + case 10: changeCustom(2,true); break; + case 11: changeCustom(3,true); break; + } } - } - - for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { - if (palettes_alpha_indexes[i] == effectPalette) { - effectPaletteIndex = i; - break; + else if (Enc_B == HIGH) + { // B is low so counter-clockwise + switch(select_state) { + case 0: changeBrightness(false); break; + case 1: changeEffectSpeed(false); break; + case 2: changeEffectIntensity(false); break; + case 3: changePalette(false); break; + case 4: changeEffect(false); break; + case 5: changeHue(false); break; + case 6: changeSat(false); break; + case 7: changeCCT(false); break; + case 8: changePreset(false); break; + case 9: changeCustom(1,false); break; + case 10: changeCustom(2,false); break; + case 11: changeCustom(3,false); break; + } } } + Enc_A_prev = Enc_A; // Store value of A for next time + loopTime = currentTime; // Updates loopTime } +} - boolean changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { +void RotaryEncoderUIUsermod::displayNetworkInfo() { #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display != nullptr) { - if (display->wakeDisplay()) { - // Throw away wake up input - display->redraw(true); - return false; - } - display->overlay(stateName, 750, glyph); - display->setMarkLine(markedLine, markedCol); - } + display->networkOverlay(PSTR("NETWORK INFO"), 10000); #endif - return true; +} + +void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { + DEBUG_PRINTLN(F("Finding current mode and palette.")); + currentEffectAndPaletteInitialized = true; + + effectCurrentIndex = 0; + for (int i = 0; i < strip.getModeCount(); i++) { + if (modes_alpha_indexes[i] == effectCurrent) { + effectCurrentIndex = i; + DEBUG_PRINTLN(F("Found current mode.")); + break; + } } - void lampUdated() { - //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) - // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa - //setValuesFromFirstSelectedSeg(); //to make transition work on main segment (should no longer be required) - stateUpdated(CALL_MODE_BUTTON); - updateInterfaces(CALL_MODE_BUTTON); + effectPaletteIndex = 0; + DEBUG_PRINTLN(effectPalette); + for (uint8_t i = 0; i < strip.getPaletteCount()+strip.customPalettes.size(); i++) { + if (palettes_alpha_indexes[i] == effectPalette) { + effectPaletteIndex = i; + DEBUG_PRINTLN(F("Found palette.")); + break; + } } +} - void changeBrightness(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); +bool RotaryEncoderUIUsermod::changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display != nullptr) { + if (display->wakeDisplay()) { // Throw away wake up input - return; + display->redraw(true); + return false; } - display->updateRedrawTime(); - #endif - bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); - lampUdated(); - #ifdef USERMOD_FOUR_LINE_DISPLAY - display->updateBrightness(); - #endif + display->overlay(stateName, 750, glyph); + display->setMarkLine(markedLine, markedCol); } +#endif + return true; +} +void RotaryEncoderUIUsermod::lampUdated() { + //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) + // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa + //setValuesFromFirstSelectedSeg(); //to make transition work on main segment (should no longer be required) + stateUpdated(CALL_MODE_BUTTON); + updateInterfaces(CALL_MODE_BUTTON); +} - void changeEffect(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0); - effectCurrent = modes_alpha_indexes[effectCurrentIndex]; - stateChanged = true; - if (applyToAll) { - for (byte i=0; ishowCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); - #endif +void RotaryEncoderUIUsermod::changeBrightness(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; } + display->updateRedrawTime(); +#endif + //bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); + if (bri < 40) bri = max(min((increase ? bri+fadeAmount/2 : bri-fadeAmount/2), 255), 0); // slower steps when brightness < 16% + else bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateBrightness(); +#endif +} - void changeEffectSpeed(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; +void RotaryEncoderUIUsermod::changeEffect(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0); + effectCurrent = modes_alpha_indexes[effectCurrentIndex]; + stateChanged = true; + if (applyToAll) { + for (byte i=0; iupdateRedrawTime(); - #endif - effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0); - stateChanged = true; - if (applyToAll) { - for (byte i=0; ishowCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); +#endif +} + + +void RotaryEncoderUIUsermod::changeEffectSpeed(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0); + stateChanged = true; + if (applyToAll) { + for (byte i=0; iupdateSpeed(); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.speed = effectSpeed; } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateSpeed(); +#endif +} - void changeEffectIntensity(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0); - stateChanged = true; - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0); + stateChanged = true; + if (applyToAll) { + for (byte i=0; iupdateIntensity(); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.intensity = effectIntensity; } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateIntensity(); +#endif +} - void changePalette(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; +void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) { + uint8_t val = 0; +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + stateChanged = true; + if (applyToAll) { + uint8_t id = strip.getFirstSelectedSegId(); + Segment& sid = strip.getSegment(id); + switch (par) { + case 3: val = sid.custom3 = max(min((increase ? sid.custom3+fadeAmount : sid.custom3-fadeAmount), 255), 0); break; + case 2: val = sid.custom2 = max(min((increase ? sid.custom2+fadeAmount : sid.custom2-fadeAmount), 255), 0); break; + default: val = sid.custom1 = max(min((increase ? sid.custom1+fadeAmount : sid.custom1-fadeAmount), 255), 0); break; } - display->updateRedrawTime(); - #endif - effectPaletteIndex = max(min((increase ? effectPaletteIndex+1 : effectPaletteIndex-1), strip.getPaletteCount()-1), 0); - effectPalette = palettes_alpha_indexes[effectPaletteIndex]; - stateChanged = true; - if (applyToAll) { - for (byte i=0; ishowCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); - #endif + } else { + Segment& seg = strip.getMainSegment(); + switch (par) { + case 3: val = seg.custom3 = max(min((increase ? seg.custom3+fadeAmount : seg.custom3-fadeAmount), 255), 0); break; + case 2: val = seg.custom2 = max(min((increase ? seg.custom2+fadeAmount : seg.custom2-fadeAmount), 255), 0); break; + default: val = seg.custom1 = max(min((increase ? seg.custom1+fadeAmount : seg.custom1-fadeAmount), 255), 0); break; + } } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + char lineBuffer[64]; + sprintf(lineBuffer, "%d", val); + display->overlay(lineBuffer, 500, 10); // use star +#endif +} - void changeHue(bool increase){ - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; +void RotaryEncoderUIUsermod::changePalette(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), strip.getPaletteCount()+strip.customPalettes.size()-1), 0U); + effectPalette = palettes_alpha_indexes[effectPaletteIndex]; + stateChanged = true; + if (applyToAll) { + for (byte i=0; iupdateRedrawTime(); - #endif - currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); - stateChanged = true; - if (applyToAll) { - for (byte i=0; ishowCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); +#endif +} + + +void RotaryEncoderUIUsermod::changeHue(bool increase){ +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); + colorHStoRGB(currentHue1*256, currentSat1, col); + stateChanged = true; + if (applyToAll) { + for (byte i=0; ioverlay(lineBuffer, 500, 7); // use brush +#endif +} - void changeSat(bool increase){ - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); + colorHStoRGB(currentHue1*256, currentSat1, col); + if (applyToAll) { + for (byte i=0; ioverlay(lineBuffer, 500, 8); // use contrast +#endif +} - void changePreset(bool increase) { +void RotaryEncoderUIUsermod::changePreset(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + if (presetHigh && presetLow && presetHigh > presetLow) { + StaticJsonDocument<64> root; + char str[64]; + sprintf_P(str, PSTR("%d~%d~%s"), presetLow, presetHigh, increase?"":"-"); + root["ps"] = str; + deserializeState(root.as(), CALL_MODE_BUTTON_PRESET); +/* + String apireq = F("win&PL=~"); + if (!increase) apireq += '-'; + apireq += F("&P1="); + apireq += presetLow; + apireq += F("&P2="); + apireq += presetHigh; + handleSet(nullptr, apireq, false); +*/ + lampUdated(); #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); + sprintf(str, "%d", currentPreset); + display->overlay(str, 500, 11); // use heart #endif - if (presetHigh && presetLow && presetHigh > presetLow) { - String apireq = F("win&PL=~"); - if (!increase) apireq += '-'; - apireq += F("&P1="); - apireq += presetLow; - apireq += F("&P2="); - apireq += presetHigh; - handleSet(nullptr, apireq, false); - lampUdated(); - } } +} - void changeCCT(bool increase){ - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0); +void RotaryEncoderUIUsermod::changeCCT(bool increase){ +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0); // if (applyToAll) { - for (byte i=0; ioverlay(lineBuffer, 500, 10); // use star +#endif +} - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - /* - void addToJsonInfo(JsonObject& root) - { - int reading = 20; - //this code adds "u":{"Light":[20," lux"]} to the info object - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - JsonArray lightArr = user.createNestedArray("Light"); //name - lightArr.add(reading); //value - lightArr.add(" lux"); //unit - } +/* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor */ +/* +void RotaryEncoderUIUsermod::addToJsonInfo(JsonObject& root) +{ + int reading = 20; + //this code adds "u":{"Light":[20," lux"]} to the info object + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + JsonArray lightArr = user.createNestedArray("Light"); //name + lightArr.add(reading); //value + lightArr.add(" lux"); //unit +} +*/ - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - /* - void addToJsonState(JsonObject &root) - { - //root["user0"] = userVar0; - } +/* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients */ +/* +void RotaryEncoderUIUsermod::addToJsonState(JsonObject &root) +{ + //root["user0"] = userVar0; +} +*/ - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - /* - void readFromJsonState(JsonObject &root) - { - //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value - //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); - } +/* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients */ +/* +void RotaryEncoderUIUsermod::readFromJsonState(JsonObject &root) +{ + //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); +} +*/ - /** - * addToConfig() (called from set.cpp) stores persistent properties to cfg.json - */ - void addToConfig(JsonObject &root) { - // we add JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_DT_pin)] = pinA; - top[FPSTR(_CLK_pin)] = pinB; - top[FPSTR(_SW_pin)] = pinC; - top[FPSTR(_presetLow)] = presetLow; - top[FPSTR(_presetHigh)] = presetHigh; - top[FPSTR(_applyToAll)] = applyToAll; - DEBUG_PRINTLN(F("Rotary Encoder config saved.")); - } - - /** - * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - * - * The function should return true if configuration was successfully loaded or false if there was no configuration. - */ - bool readFromConfig(JsonObject &root) { - // we look for JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - int8_t newDTpin = top[FPSTR(_DT_pin)] | pinA; - int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB; - int8_t newSWpin = top[FPSTR(_SW_pin)] | pinC; - - presetHigh = top[FPSTR(_presetHigh)] | presetHigh; - presetLow = top[FPSTR(_presetLow)] | presetLow; - presetHigh = MIN(250,MAX(0,presetHigh)); - presetLow = MIN(250,MAX(0,presetLow)); +/** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ +void RotaryEncoderUIUsermod::addToConfig(JsonObject &root) { + // we add JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_DT_pin)] = pinA; + top[FPSTR(_CLK_pin)] = pinB; + top[FPSTR(_SW_pin)] = pinC; + top[FPSTR(_presetLow)] = presetLow; + top[FPSTR(_presetHigh)] = presetHigh; + top[FPSTR(_applyToAll)] = applyToAll; + top[FPSTR(_pcf8574)] = usePcf8574; + top[FPSTR(_pcfAddress)] = addrPcf8574; + top[FPSTR(_pcfINTpin)] = pinIRQ; + DEBUG_PRINTLN(F("Rotary Encoder config saved.")); +} - enabled = top[FPSTR(_enabled)] | enabled; - applyToAll = top[FPSTR(_applyToAll)] | applyToAll; +void RotaryEncoderUIUsermod::appendConfigData() { + oappend(SET_F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); + oappend(SET_F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); +} +/** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ +bool RotaryEncoderUIUsermod::readFromConfig(JsonObject &root) { + // we look for JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - // first run: reading from cfg.json - pinA = newDTpin; - pinB = newCLKpin; - pinC = newSWpin; - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing parameters from settings page - if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + int8_t newDTpin = top[FPSTR(_DT_pin)] | pinA; + int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB; + int8_t newSWpin = top[FPSTR(_SW_pin)] | pinC; + int8_t newIRQpin = top[FPSTR(_pcfINTpin)] | pinIRQ; + bool oldPcf8574 = usePcf8574; + + presetHigh = top[FPSTR(_presetHigh)] | presetHigh; + presetLow = top[FPSTR(_presetLow)] | presetLow; + presetHigh = MIN(250,MAX(0,presetHigh)); + presetLow = MIN(250,MAX(0,presetLow)); + + enabled = top[FPSTR(_enabled)] | enabled; + applyToAll = top[FPSTR(_applyToAll)] | applyToAll; + + usePcf8574 = top[FPSTR(_pcf8574)] | usePcf8574; + addrPcf8574 = top[FPSTR(_pcfAddress)] | addrPcf8574; + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + pinA = newDTpin; + pinB = newCLKpin; + pinC = newSWpin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin || pinIRQ!=newIRQpin) { + if (oldPcf8574) { + if (pinIRQ >= 0) { + detachInterrupt(pinIRQ); + pinManager.deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI); + DEBUG_PRINTLN(F("Deallocated old IRQ pin.")); + } + pinIRQ = newIRQpin<100 ? newIRQpin : -1; // ignore PCF8574 pins + } else { pinManager.deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); pinManager.deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); pinManager.deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); - pinA = newDTpin; - pinB = newCLKpin; - pinC = newSWpin; - if (pinA<0 || pinB<0 || pinC<0) { - enabled = false; - return true; - } - setup(); + DEBUG_PRINTLN(F("Deallocated old pins.")); + } + pinA = newDTpin; + pinB = newCLKpin; + pinC = newSWpin; + if (pinA<0 || pinB<0 || pinC<0) { + enabled = false; + return true; } + setup(); } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_applyToAll)].isNull(); } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_pcfINTpin)].isNull(); +} - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_ROTARY_ENC_UI; - } -}; // strings to reduce flash memory usage (used more than twice) const char RotaryEncoderUIUsermod::_name[] PROGMEM = "Rotary-Encoder"; @@ -826,3 +1172,6 @@ const char RotaryEncoderUIUsermod::_SW_pin[] PROGMEM = "SW-pin"; const char RotaryEncoderUIUsermod::_presetHigh[] PROGMEM = "preset-high"; const char RotaryEncoderUIUsermod::_presetLow[] PROGMEM = "preset-low"; const char RotaryEncoderUIUsermod::_applyToAll[] PROGMEM = "apply-2-all-seg"; +const char RotaryEncoderUIUsermod::_pcf8574[] PROGMEM = "use-PCF8574"; +const char RotaryEncoderUIUsermod::_pcfAddress[] PROGMEM = "PCF8574-address"; +const char RotaryEncoderUIUsermod::_pcfINTpin[] PROGMEM = "PCF8574-INT-pin"; diff --git a/usermods/usermod_v2_word_clock/readme.md b/usermods/usermod_v2_word_clock/readme.md new file mode 100644 index 0000000000..c42ee0ee47 --- /dev/null +++ b/usermods/usermod_v2_word_clock/readme.md @@ -0,0 +1,39 @@ +# Word Clock Usermod V2 + +This usermod drives an 11x10 pixel matrix wordclock with WLED. There are 4 additional dots for the minutes. +The visualisation is described by 4 masks with LED numbers (single dots for minutes, minutes, hours and "clock"). The index of the LEDs in the masks always starts at 0, even if the ledOffset is not 0. +There are 3 parameters that control behavior: + +active: enable/disable usermod +diplayItIs: enable/disable display of "Es ist" on the clock +ledOffset: number of LEDs before the wordclock LEDs + +### Update for alternative wiring pattern +Based on this fantastic work I added an alternative wiring pattern. +The original used a long wire to connect DO to DI, from one line to the next line. + +I wired my clock in meander style. So the first LED in the second line is on the right. +With this method, every other line was inverted and showed the wrong letter. + +I added a switch in usermod called "meander wiring?" to enable/disable the alternate wiring pattern. + + +## Installation + +Copy and update the example `platformio_override.ini.sample` +from the Rotary Encoder UI usermod folder to the root directory of your particular build. +This file should be placed in the same directory as `platformio.ini`. + +### Define Your Options + +* `USERMOD_WORDCLOCK` - define this to have this usermod included wled00\usermods_list.cpp + +### PlatformIO requirements + +No special requirements. + +## Change Log + +2022/08/18 added meander wiring pattern. + +2022/03/30 initial commit diff --git a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h new file mode 100644 index 0000000000..b66be290a5 --- /dev/null +++ b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h @@ -0,0 +1,507 @@ +#pragma once + +#include "wled.h" + +/* + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * + * This usermod can be used to drive a wordclock with a 11x10 pixel matrix with WLED. There are also 4 additional dots for the minutes. + * The visualisation is described in 4 mask with LED numbers (single dots for minutes, minutes, hours and "clock/Uhr"). + * There are 2 parameters to change the behaviour: + * + * active: enable/disable usermod + * diplayItIs: enable/disable display of "Es ist" on the clock. + */ + +class WordClockUsermod : public Usermod +{ + private: + unsigned long lastTime = 0; + int lastTimeMinutes = -1; + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + bool usermodActive = false; + bool displayItIs = false; + int ledOffset = 100; + bool meander = false; + bool nord = false; + + // defines for mask sizes + #define maskSizeLeds 114 + #define maskSizeMinutes 12 + #define maskSizeMinutesMea 12 + #define maskSizeHours 6 + #define maskSizeHoursMea 6 + #define maskSizeItIs 5 + #define maskSizeMinuteDots 4 + + // "minute" masks + // Normal wiring + const int maskMinutes[14][maskSizeMinutes] = + { + {107, 108, 109, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // 0 - 00 + { 7, 8, 9, 10, 40, 41, 42, 43, -1, -1, -1, -1}, // 1 - 05 fünf nach + { 11, 12, 13, 14, 40, 41, 42, 43, -1, -1, -1, -1}, // 2 - 10 zehn nach + { 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1}, // 3 - 15 viertel + { 15, 16, 17, 18, 19, 20, 21, 40, 41, 42, 43, -1}, // 4 - 20 zwanzig nach + { 7, 8, 9, 10, 33, 34, 35, 44, 45, 46, 47, -1}, // 5 - 25 fünf vor halb + { 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1}, // 6 - 30 halb + { 7, 8, 9, 10, 40, 41, 42, 43, 44, 45, 46, 47}, // 7 - 35 fünf nach halb + { 15, 16, 17, 18, 19, 20, 21, 33, 34, 35, -1, -1}, // 8 - 40 zwanzig vor + { 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1}, // 9 - 45 dreiviertel + { 11, 12, 13, 14, 33, 34, 35, -1, -1, -1, -1, -1}, // 10 - 50 zehn vor + { 7, 8, 9, 10, 33, 34, 35, -1, -1, -1, -1, -1}, // 11 - 55 fünf vor + { 26, 27, 28, 29, 30, 31, 32, 40, 41, 42, 43, -1}, // 12 - 15 alternative viertel nach + { 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1} // 13 - 45 alternative viertel vor + }; + + // Meander wiring + const int maskMinutesMea[14][maskSizeMinutesMea] = + { + { 99, 100, 101, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // 0 - 00 + { 7, 8, 9, 10, 33, 34, 35, 36, -1, -1, -1, -1}, // 1 - 05 fünf nach + { 18, 19, 20, 21, 33, 34, 35, 36, -1, -1, -1, -1}, // 2 - 10 zehn nach + { 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1}, // 3 - 15 viertel + { 11, 12, 13, 14, 15, 16, 17, 33, 34, 35, 36, -1}, // 4 - 20 zwanzig nach + { 7, 8, 9, 10, 41, 42, 43, 44, 45, 46, 47, -1}, // 5 - 25 fünf vor halb + { 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1}, // 6 - 30 halb + { 7, 8, 9, 10, 33, 34, 35, 36, 44, 45, 46, 47}, // 7 - 35 fünf nach halb + { 11, 12, 13, 14, 15, 16, 17, 41, 42, 43, -1, -1}, // 8 - 40 zwanzig vor + { 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1}, // 9 - 45 dreiviertel + { 18, 19, 20, 21, 41, 42, 43, -1, -1, -1, -1, -1}, // 10 - 50 zehn vor + { 7, 8, 9, 10, 41, 42, 43, -1, -1, -1, -1, -1}, // 11 - 55 fünf vor + { 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, -1}, // 12 - 15 alternative viertel nach + { 26, 27, 28, 29, 30, 31, 32, 41, 42, 43, -1, -1} // 13 - 45 alternative viertel vor + }; + + + // hour masks + // Normal wiring + const int maskHours[13][maskSizeHours] = + { + { 55, 56, 57, -1, -1, -1}, // 01: ein + { 55, 56, 57, 58, -1, -1}, // 01: eins + { 62, 63, 64, 65, -1, -1}, // 02: zwei + { 66, 67, 68, 69, -1, -1}, // 03: drei + { 73, 74, 75, 76, -1, -1}, // 04: vier + { 51, 52, 53, 54, -1, -1}, // 05: fünf + { 77, 78, 79, 80, 81, -1}, // 06: sechs + { 88, 89, 90, 91, 92, 93}, // 07: sieben + { 84, 85, 86, 87, -1, -1}, // 08: acht + {102, 103, 104, 105, -1, -1}, // 09: neun + { 99, 100, 101, 102, -1, -1}, // 10: zehn + { 49, 50, 51, -1, -1, -1}, // 11: elf + { 94, 95, 96, 97, 98, -1} // 12: zwölf and 00: null + }; + // Meander wiring + const int maskHoursMea[13][maskSizeHoursMea] = + { + { 63, 64, 65, -1, -1, -1}, // 01: ein + { 62, 63, 64, 65, -1, -1}, // 01: eins + { 55, 56, 57, 58, -1, -1}, // 02: zwei + { 66, 67, 68, 69, -1, -1}, // 03: drei + { 73, 74, 75, 76, -1, -1}, // 04: vier + { 51, 52, 53, 54, -1, -1}, // 05: fünf + { 83, 84, 85, 86, 87, -1}, // 06: sechs + { 88, 89, 90, 91, 92, 93}, // 07: sieben + { 77, 78, 79, 80, -1, -1}, // 08: acht + {103, 104, 105, 106, -1, -1}, // 09: neun + {106, 107, 108, 109, -1, -1}, // 10: zehn + { 49, 50, 51, -1, -1, -1}, // 11: elf + { 94, 95, 96, 97, 98, -1} // 12: zwölf and 00: null + }; + + // mask "it is" + const int maskItIs[maskSizeItIs] = {0, 1, 3, 4, 5}; + + // mask minute dots + const int maskMinuteDots[maskSizeMinuteDots] = {110, 111, 112, 113}; + + // overall mask to define which LEDs are on + int maskLedsOn[maskSizeLeds] = + { + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0 + }; + + // update led mask + void updateLedMask(const int wordMask[], int arraySize) + { + // loop over array + for (int x=0; x < arraySize; x++) + { + // check if mask has a valid LED number + if (wordMask[x] >= 0 && wordMask[x] < maskSizeLeds) + { + // turn LED on + maskLedsOn[wordMask[x]] = 1; + } + } + } + + // set hours + void setHours(int hours, bool fullClock) + { + int index = hours; + + // handle 00:xx as 12:xx + if (hours == 0) + { + index = 12; + } + + // check if we get an overrun of 12 o´clock + if (hours == 13) + { + index = 1; + } + + // special handling for "ein Uhr" instead of "eins Uhr" + if (hours == 1 && fullClock == true) + { + index = 0; + } + + // update led mask + if (meander) + { + updateLedMask(maskHoursMea[index], maskSizeHoursMea); + } else { + updateLedMask(maskHours[index], maskSizeHours); + } + } + + // set minutes + void setMinutes(int index) + { + // update led mask + if (meander) + { + updateLedMask(maskMinutesMea[index], maskSizeMinutesMea); + } else { + updateLedMask(maskMinutes[index], maskSizeMinutes); + } + } + + // set minutes dot + void setSingleMinuteDots(int minutes) + { + // modulo to get minute dots + int minutesDotCount = minutes % 5; + + // check if minute dots are active + if (minutesDotCount > 0) + { + // activate all minute dots until number is reached + for (int i = 0; i < minutesDotCount; i++) + { + // activate LED + maskLedsOn[maskMinuteDots[i]] = 1; + } + } + } + + // update the display + void updateDisplay(uint8_t hours, uint8_t minutes) + { + // disable complete matrix at the bigging + for (int x = 0; x < maskSizeLeds; x++) + { + maskLedsOn[x] = 0; + } + + // display it is/es ist if activated + if (displayItIs) + { + updateLedMask(maskItIs, maskSizeItIs); + } + + // set single minute dots + setSingleMinuteDots(minutes); + + // switch minutes + switch (minutes / 5) + { + case 0: + // full hour + setMinutes(0); + setHours(hours, true); + break; + case 1: + // 5 nach + setMinutes(1); + setHours(hours, false); + break; + case 2: + // 10 nach + setMinutes(2); + setHours(hours, false); + break; + case 3: + if (nord) { + // viertel nach + setMinutes(12); + setHours(hours, false); + } else { + // viertel + setMinutes(3); + setHours(hours + 1, false); + }; + break; + case 4: + // 20 nach + setMinutes(4); + setHours(hours, false); + break; + case 5: + // 5 vor halb + setMinutes(5); + setHours(hours + 1, false); + break; + case 6: + // halb + setMinutes(6); + setHours(hours + 1, false); + break; + case 7: + // 5 nach halb + setMinutes(7); + setHours(hours + 1, false); + break; + case 8: + // 20 vor + setMinutes(8); + setHours(hours + 1, false); + break; + case 9: + // viertel vor + if (nord) { + setMinutes(13); + } + // dreiviertel + else { + setMinutes(9); + } + setHours(hours + 1, false); + break; + case 10: + // 10 vor + setMinutes(10); + setHours(hours + 1, false); + break; + case 11: + // 5 vor + setMinutes(11); + setHours(hours + 1, false); + break; + } + } + + public: + //Functions called by WLED + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() + { + } + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + * + * Tips: + * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. + * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. + * + * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. + * Instead, use a timer check as shown here. + */ + void loop() { + + // do it every 5 seconds + if (millis() - lastTime > 5000) + { + // check the time + int minutes = minute(localTime); + + // check if we already updated this minute + if (lastTimeMinutes != minutes) + { + // update the display with new time + updateDisplay(hourFormat12(localTime), minute(localTime)); + + // remember last update time + lastTimeMinutes = minutes; + } + + // remember last update + lastTime = millis(); + } + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + /* + void addToJsonInfo(JsonObject& root) + { + } + */ + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) + { + } + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) + { + } + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will make your settings editable through the Usermod Settings page automatically. + * + * Usermod Settings Overview: + * - Numeric values are treated as floats in the browser. + * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float + * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and + * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. + * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. + * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a + * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. + * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type + * used in the Usermod when reading the value from ArduinoJson. + * - Pin values can be treated differently from an integer value by using the key name "pin" + * - "pin" can contain a single or array of integer values + * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins + * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) + * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used + * + * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings + * + * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. + * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. + * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(F("WordClockUsermod")); + top[F("active")] = usermodActive; + top[F("displayItIs")] = displayItIs; + top[F("ledOffset")] = ledOffset; + top[F("Meander wiring?")] = meander; + top[F("Norddeutsch")] = nord; + } + + void appendConfigData() + { + oappend(SET_F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); + oappend(SET_F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + * + * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present + * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them + * + * This function is guaranteed to be called on boot, but could also be called every time settings are updated + */ + bool readFromConfig(JsonObject& root) + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + + JsonObject top = root[F("WordClockUsermod")]; + + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[F("active")], usermodActive); + configComplete &= getJsonValue(top[F("displayItIs")], displayItIs); + configComplete &= getJsonValue(top[F("ledOffset")], ledOffset); + configComplete &= getJsonValue(top[F("Meander wiring?")], meander); + configComplete &= getJsonValue(top[F("Norddeutsch")], nord); + + return configComplete; + } + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() + { + // check if usermod is active + if (usermodActive == true) + { + // loop over all leds + for (int x = 0; x < maskSizeLeds; x++) + { + // check mask + if (maskLedsOn[x] == 0) + { + // set pixel off + strip.setPixelColor(x + ledOffset, RGBW32(0,0,0,0)); + } + } + } + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_WORDCLOCK; + } + + //More methods can be added in the future, this example will then be extended. + //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! +}; \ No newline at end of file diff --git a/usermods/wireguard/platformio_override.ini b/usermods/wireguard/platformio_override.ini new file mode 100644 index 0000000000..fc0ae5fc99 --- /dev/null +++ b/usermods/wireguard/platformio_override.ini @@ -0,0 +1,22 @@ +# Example PlatformIO Project Configuration Override for WireGuard +# ------------------------------------------------------------------------------ +# Copy to platformio_override.ini to activate. +# ------------------------------------------------------------------------------ +# Please visit documentation: https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = WLED_ESP32-WireGuard + +[env:WLED_ESP32-WireGuard] +board = esp32dev +platform = ${esp32.platform} +platform_packages = ${esp32.platform_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp32} + -D WLED_RELEASE_NAME=ESP32-WireGuard + -D USERMOD_WIREGUARD +lib_deps = ${esp32.lib_deps} + https://github.com/kienvu58/WireGuard-ESP32-Arduino.git +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.default_partitions} +upload_speed = 921600 \ No newline at end of file diff --git a/usermods/wireguard/readme.md b/usermods/wireguard/readme.md new file mode 100644 index 0000000000..071bea9f91 --- /dev/null +++ b/usermods/wireguard/readme.md @@ -0,0 +1,19 @@ +# WireGuard VPN + +This usermod will connect your WLED instance to a remote WireGuard subnet. + +Configuration is performed via the Usermod menu. There are no parameters to set in code! + +## Installation + +Copy the `platformio_override.ini` file to the root project directory, review the build options, and select the `WLED_ESP32-WireGuard` environment. + + +## Author + +Aiden Vigue [vigue.me](https://vigue.me) +[@acvigue](https://github.com/acvigue) +aiden@vigue.me + + + diff --git a/usermods/wireguard/wireguard.h b/usermods/wireguard/wireguard.h new file mode 100644 index 0000000000..a83b9fe78e --- /dev/null +++ b/usermods/wireguard/wireguard.h @@ -0,0 +1,127 @@ +#pragma once + +#include + +#include "wled.h" + +class WireguardUsermod : public Usermod { + public: + void setup() { configTzTime(posix_tz, ntpServerName); } + + void connected() { + if (wg.is_initialized()) { + wg.end(); + } + } + + void loop() { + if (millis() - lastTime > 5000) { + if (is_enabled && WLED_CONNECTED) { + if (!wg.is_initialized()) { + struct tm timeinfo; + if (getLocalTime(&timeinfo, 0)) { + if (strlen(preshared_key) < 1) { + wg.begin(local_ip, private_key, endpoint_address, public_key, endpoint_port, NULL); + } else { + wg.begin(local_ip, private_key, endpoint_address, public_key, endpoint_port, preshared_key); + } + } + } + } + + lastTime = millis(); + } + } + + void addToJsonInfo(JsonObject& root) { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(F("WireGuard")); + String uiDomString; + + struct tm timeinfo; + if (!getLocalTime(&timeinfo, 0)) { + uiDomString = "Time out of sync!"; + } else { + if (wg.is_initialized()) { + uiDomString = "netif up!"; + } else { + uiDomString = "netif down :("; + } + } + if (is_enabled) infoArr.add(uiDomString); + } + + void appendConfigData() { + oappend(SET_F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field + } + + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(F("WireGuard")); + top[F("host")] = endpoint_address; + top[F("port")] = endpoint_port; + top[F("ip")] = local_ip.toString(); + top[F("psk")] = preshared_key; + top[F("pem")] = private_key; + top[F("pub")] = public_key; + top[F("tz")] = posix_tz; + } + + bool readFromConfig(JsonObject& root) { + JsonObject top = root[F("WireGuard")]; + + if (top["host"].isNull() || top["port"].isNull() || top["ip"].isNull() || top["pem"].isNull() || top["pub"].isNull() || top["tz"].isNull()) { + is_enabled = false; + return false; + } else { + const char* host = top["host"]; + strncpy(endpoint_address, host, 100); + + const char* ip_s = top["ip"]; + uint8_t ip[4]; + sscanf(ip_s, "%u.%u.%u.%u", &ip[0], &ip[1], &ip[2], &ip[3]); + local_ip = IPAddress(ip[0], ip[1], ip[2], ip[3]); + + const char* pem = top["pem"]; + strncpy(private_key, pem, 45); + + const char* pub = top["pub"]; + strncpy(public_key, pub, 45); + + const char* tz = top["tz"]; + strncpy(posix_tz, tz, 150); + + endpoint_port = top["port"]; + + if (!top["psk"].isNull()) { + const char* psk = top["psk"]; + strncpy(preshared_key, psk, 45); + } + + is_enabled = true; + } + + return is_enabled; + } + + uint16_t getId() { return USERMOD_ID_WIREGUARD; } + + private: + WireGuard wg; + char preshared_key[45]; + char private_key[45]; + IPAddress local_ip; + char public_key[45]; + char endpoint_address[100]; + char posix_tz[150]; + int endpoint_port = 0; + bool is_enabled = false; + unsigned long lastTime = 0; +}; \ No newline at end of file diff --git a/usermods/wizlights/readme.md b/usermods/wizlights/readme.md new file mode 100644 index 0000000000..9e633043bf --- /dev/null +++ b/usermods/wizlights/readme.md @@ -0,0 +1,35 @@ +# Controlling Wiz lights + +Enables controlling [WiZ](https://www.wizconnected.com/en/consumer/) lights that are part of the same network as the WLED controller. + +The mod takes the colors from the first few pixels and sends them to the lights. + +## Configuration + +- Interval (ms) + - How frequently to update the WiZ lights, in milliseconds. + - Setting it too low may cause the ESP to become unresponsive. +- Send Delay (ms) + - An optional millisecond delay after updating each WiZ light. + - Can help smooth out effects when using a large number of WiZ lights +- Use Enhanced White + - Uses the WiZ lights onboard white LEDs instead of sending maximum RGB values. + - Tunable with warm and cool LEDs as supported by WiZ bulbs + - Note: Only sent when max RGB value is set, the automatic brightness limiter must be disabled + - ToDo: Have better logic for white value mixing to take advantage of the light's capabilities +- Always Force Update + - Can be enabled to always send update message to light even if the new value matches the old value. +- Force update every x minutes + - adjusts the default force update timeout of 5 minutes. + - Setting to 0 is the same as enabling Always Force Update + - +Next, enter the IP addresses for the lights to be controlled, in order. The limit is 15 devices, but that number +can be easily changed by updating _MAX_WIZ_LIGHTS_. + + + + +## Related project + +If you use these lights and python, make sure to check out the [pywizlight](https://github.com/sbidy/pywizlight) project. You can learn how to +format the messages to control the lights from that project. diff --git a/usermods/wizlights/wizlights.h b/usermods/wizlights/wizlights.h new file mode 100644 index 0000000000..08d204934c --- /dev/null +++ b/usermods/wizlights/wizlights.h @@ -0,0 +1,158 @@ +#pragma once + +#include "wled.h" +#include + +// Maximum number of lights supported +#define MAX_WIZ_LIGHTS 15 + +WiFiUDP UDP; + + + + +class WizLightsUsermod : public Usermod { + + private: + unsigned long lastTime = 0; + long updateInterval; + long sendDelay; + + long forceUpdateMinutes; + bool forceUpdate; + + bool useEnhancedWhite; + long warmWhite; + long coldWhite; + + IPAddress lightsIP[MAX_WIZ_LIGHTS]; // Stores Light IP addresses + bool lightsValid[MAX_WIZ_LIGHTS]; // Stores Light IP address validity + uint32_t colorsSent[MAX_WIZ_LIGHTS]; // Stores last color sent for each light + + + + public: + + + + // Send JSON blob to WiZ Light over UDP + // RGB or C/W white + // TODO: + // Better utilize WLED existing white mixing logic + void wizSendColor(IPAddress ip, uint32_t color) { + UDP.beginPacket(ip, 38899); + + // If no LED color, turn light off. Note wiz light setting for "Off fade-out" will be applied by the light itself. + if (color == 0) { + UDP.print("{\"method\":\"setPilot\",\"params\":{\"state\":false}}"); + + // If color is WHITE, try and use the lights WHITE LEDs instead of mixing RGB LEDs + } else if (color == 16777215 && useEnhancedWhite){ + + // set cold white light only + if (coldWhite > 0 && warmWhite == 0){ + UDP.print("{\"method\":\"setPilot\",\"params\":{\"c\":"); UDP.print(coldWhite) ;UDP.print("}}");} + + // set warm white light only + if (warmWhite > 0 && coldWhite == 0){ + UDP.print("{\"method\":\"setPilot\",\"params\":{\"w\":"); UDP.print(warmWhite) ;UDP.print("}}");} + + // set combination of warm and cold white light + if (coldWhite > 0 && warmWhite > 0){ + UDP.print("{\"method\":\"setPilot\",\"params\":{\"c\":"); UDP.print(coldWhite) ;UDP.print(",\"w\":"); UDP.print(warmWhite); UDP.print("}}");} + + // Send color as RGB + } else { + UDP.print("{\"method\":\"setPilot\",\"params\":{\"r\":"); + UDP.print(R(color)); + UDP.print(",\"g\":"); + UDP.print(G(color)); + UDP.print(",\"b\":"); + UDP.print(B(color)); + UDP.print("}}"); + } + + UDP.endPacket(); + } + + // Override definition so it compiles + void setup() { + + } + + + // TODO: Check millis() rollover + void loop() { + + // Make sure we are connected first + if (!WLED_CONNECTED) return; + + unsigned long ellapsedTime = millis() - lastTime; + if (ellapsedTime > updateInterval) { + bool update = false; + for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) { + if (!lightsValid[i]) { continue; } + uint32_t newColor = strip.getPixelColor(i); + if (forceUpdate || (newColor != colorsSent[i]) || (ellapsedTime > forceUpdateMinutes*60000)){ + wizSendColor(lightsIP[i], newColor); + colorsSent[i] = newColor; + update = true; + delay(sendDelay); + } + } + if (update) lastTime = millis(); + } + } + + + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject("wizLightsUsermod"); + top["Interval (ms)"] = updateInterval; + top["Send Delay (ms)"] = sendDelay; + top["Use Enhanced White *"] = useEnhancedWhite; + top["* Warm White Value (0-255)"] = warmWhite; + top["* Cold White Value (0-255)"] = coldWhite; + top["Always Force Update"] = forceUpdate; + top["Force Update Every x Minutes"] = forceUpdateMinutes; + + for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) { + top[getJsonLabel(i)] = lightsIP[i].toString(); + } + } + + + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root["wizLightsUsermod"]; + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top["Interval (ms)"], updateInterval, 1000); // How frequently to update the wiz lights + configComplete &= getJsonValue(top["Send Delay (ms)"], sendDelay, 0); // Optional delay after sending each UDP message + configComplete &= getJsonValue(top["Use Enhanced White *"], useEnhancedWhite, false); // When color is white use wiz white LEDs instead of mixing RGB + configComplete &= getJsonValue(top["* Warm White Value (0-255)"], warmWhite, 0); // Warm White LED value for Enhanced White + configComplete &= getJsonValue(top["* Cold White Value (0-255)"], coldWhite, 50); // Cold White LED value for Enhanced White + configComplete &= getJsonValue(top["Always Force Update"], forceUpdate, false); // Update wiz light every loop, even if color value has not changed + configComplete &= getJsonValue(top["Force Update Every x Minutes"], forceUpdateMinutes, 5); // Update wiz light if color value has not changed, every x minutes + + // Read list of IPs + String tempIp; + for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) { + configComplete &= getJsonValue(top[getJsonLabel(i)], tempIp, "0.0.0.0"); + lightsValid[i] = lightsIP[i].fromString(tempIp); + + // If the IP is not valid, force the value to be empty + if (!lightsValid[i]){lightsIP[i].fromString("0.0.0.0");} + } + + return configComplete; + } + + + // Create label for the usermod page (I cannot make it work with JSON arrays...) + String getJsonLabel(uint8_t i) {return "WiZ Light IP #" + String(i+1);} + + uint16_t getId(){return USERMOD_ID_WIZLIGHTS;} +}; diff --git a/usermods/word-clock-matrix/readme.md b/usermods/word-clock-matrix/readme.md index d226537f42..cfaa93e24d 100644 --- a/usermods/word-clock-matrix/readme.md +++ b/usermods/word-clock-matrix/readme.md @@ -2,5 +2,18 @@ By @bwente -See https://www.hackster.io/bwente/word-clock-with-just-two-components-073834 for the hardware guide! -Includes a customizable feature to lower the brightness at night. +See https://www.hackster.io/bwente/word-clock-with-just-two-components-073834 for the hardware guide!
+Includes a customizable feature to reduce the brightness at night. + +![image](https://user-images.githubusercontent.com/371964/197094071-f8ccaf59-1d85-4dd2-8e09-1389675291e1.png) + + +![image](https://user-images.githubusercontent.com/371964/197094211-6c736257-95ff-491f-9f0d-35d5135ecfea.png) + + + + + +![mini_8x8_word_clock_reverse_stencil_sZFti6chj4(1)](https://user-images.githubusercontent.com/371964/197094410-7c275f3f-743b-477a-bc15-5e7bdbcbd833.svg) + +![mini_8x8_word_clock_box_epUWJOBOhr(1)](https://user-images.githubusercontent.com/371964/197094496-fa49b355-164b-4bf5-84fd-f22f5206c645.svg) diff --git a/usermods/word-clock-matrix/usermod_word_clock_matrix.h b/usermods/word-clock-matrix/usermod_word_clock_matrix.h new file mode 100644 index 0000000000..5825630043 --- /dev/null +++ b/usermods/word-clock-matrix/usermod_word_clock_matrix.h @@ -0,0 +1,338 @@ +#pragma once + +#include "wled.h" + +/* + * Things to do... + * Turn on ntp clock 24h format + * 64 LEDS + */ + + +class WordClockMatrix : public Usermod +{ +private: + unsigned long lastTime = 0; + uint8_t minuteLast = 99; + int dayBrightness = 128; + int nightBrightness = 16; + +public: + void setup() + { + Serial.println("Hello from my usermod!"); + + //saveMacro(14, "A=128", false); + //saveMacro(15, "A=64", false); + //saveMacro(16, "A=16", false); + + //saveMacro(1, "&FX=0&R=255&G=255&B=255", false); + + //strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); + + //select first two segments (background color + FX settable) + WS2812FX::Segment &seg = strip.getSegment(0); + seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((0 & 0xFF) << 8) | ((0 & 0xFF))); + strip.getSegment(0).setOption(0, false); + strip.getSegment(0).setOption(2, false); + //other segments are text + for (int i = 1; i < 10; i++) + { + WS2812FX::Segment &seg = strip.getSegment(i); + seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); + strip.getSegment(i).setOption(0, true); + strip.setBrightness(64); + } + } + + void connected() + { + Serial.println("Connected to WiFi!"); + } + + void selectWordSegments(bool state) + { + for (int i = 1; i < 10; i++) + { + //WS2812FX::Segment &seg = strip.getSegment(i); + strip.getSegment(i).setOption(0, state); + // strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); + //seg.mode = 12; + //seg.palette = 1; + //strip.setBrightness(255); + } + strip.getSegment(0).setOption(0, !state); + } + + void hourChime() + { + //strip.resetSegments(); + selectWordSegments(true); + colorUpdated(CALL_MODE_FX_CHANGED); + savePreset(13, false); + selectWordSegments(false); + //strip.getSegment(0).setOption(0, true); + strip.getSegment(0).setOption(2, true); + applyPreset(12); + colorUpdated(CALL_MODE_FX_CHANGED); + } + + void displayTime(byte hour, byte minute) + { + bool isToHour = false; //true if minute > 30 + strip.setSegment(0, 0, 64); // background + strip.setSegment(1, 0, 2); //It is + + strip.setSegment(2, 0, 0); + strip.setSegment(3, 0, 0); //disable minutes + strip.setSegment(4, 0, 0); //past + strip.setSegment(6, 0, 0); //to + strip.setSegment(8, 0, 0); //disable o'clock + + if (hour < 24) //valid time, display + { + if (minute == 30) + { + strip.setSegment(2, 3, 6); //half + strip.setSegment(3, 0, 0); //minutes + } + else if (minute == 15 || minute == 45) + { + strip.setSegment(3, 0, 0); //minutes + } + else if (minute == 10) + { + //strip.setSegment(5, 6, 8); //ten + } + else if (minute == 5) + { + //strip.setSegment(5, 16, 18); //five + } + else if (minute == 0) + { + strip.setSegment(3, 0, 0); //minutes + //hourChime(); + } + else + { + strip.setSegment(3, 18, 22); //minutes + } + + //past or to? + if (minute == 0) + { //full hour + strip.setSegment(3, 0, 0); //disable minutes + strip.setSegment(4, 0, 0); //disable past + strip.setSegment(6, 0, 0); //disable to + strip.setSegment(8, 60, 64); //o'clock + } + else if (minute > 34) + { + //strip.setSegment(6, 22, 24); //to + //minute = 60 - minute; + isToHour = true; + } + else + { + //strip.setSegment(4, 24, 27); //past + //isToHour = false; + } + } + + //byte minuteRem = minute %10; + + if (minute <= 4) + { + strip.setSegment(3, 0, 0); //nothing + strip.setSegment(5, 0, 0); //nothing + strip.setSegment(6, 0, 0); //nothing + strip.setSegment(8, 60, 64); //o'clock + } + else if (minute <= 9) + { + strip.setSegment(5, 16, 18); // five past + strip.setSegment(4, 24, 27); //past + } + else if (minute <= 14) + { + strip.setSegment(5, 6, 8); // ten past + strip.setSegment(4, 24, 27); //past + } + else if (minute <= 19) + { + strip.setSegment(5, 8, 12); // quarter past + strip.setSegment(3, 0, 0); //minutes + strip.setSegment(4, 24, 27); //past + } + else if (minute <= 24) + { + strip.setSegment(5, 12, 16); // twenty past + strip.setSegment(4, 24, 27); //past + } + else if (minute <= 29) + { + strip.setSegment(5, 12, 18); // twenty-five past + strip.setSegment(4, 24, 27); //past + } + else if (minute <= 34) + { + strip.setSegment(5, 3, 6); // half past + strip.setSegment(3, 0, 0); //minutes + strip.setSegment(4, 24, 27); //past + } + else if (minute <= 39) + { + strip.setSegment(5, 12, 18); // twenty-five to + strip.setSegment(6, 22, 24); //to + } + else if (minute <= 44) + { + strip.setSegment(5, 12, 16); // twenty to + strip.setSegment(6, 22, 24); //to + } + else if (minute <= 49) + { + strip.setSegment(5, 8, 12); // quarter to + strip.setSegment(3, 0, 0); //minutes + strip.setSegment(6, 22, 24); //to + } + else if (minute <= 54) + { + strip.setSegment(5, 6, 8); // ten to + strip.setSegment(6, 22, 24); //to + } + else if (minute <= 59) + { + strip.setSegment(5, 16, 18); // five to + strip.setSegment(6, 22, 24); //to + } + + //hours + if (hour > 23) + return; + if (isToHour) + hour++; + if (hour > 12) + hour -= 12; + if (hour == 0) + hour = 12; + + switch (hour) + { + case 1: + strip.setSegment(7, 27, 29); + break; //one + case 2: + strip.setSegment(7, 35, 37); + break; //two + case 3: + strip.setSegment(7, 29, 32); + break; //three + case 4: + strip.setSegment(7, 32, 35); + break; //four + case 5: + strip.setSegment(7, 37, 40); + break; //five + case 6: + strip.setSegment(7, 43, 45); + break; //six + case 7: + strip.setSegment(7, 40, 43); + break; //seven + case 8: + strip.setSegment(7, 45, 48); + break; //eight + case 9: + strip.setSegment(7, 48, 50); + break; //nine + case 10: + strip.setSegment(7, 54, 56); + break; //ten + case 11: + strip.setSegment(7, 50, 54); + break; //eleven + case 12: + strip.setSegment(7, 56, 60); + break; //twelve + } + + selectWordSegments(true); + applyMacro(1); + } + + void timeOfDay() + { + // NOT USED: use timed macros instead + //Used to set brightness dependant of time of day - lights dimmed at night + + //monday to thursday and sunday + + if ((weekday(localTime) == 6) | (weekday(localTime) == 7)) + { + if ((hour(localTime) > 0) | (hour(localTime) < 8)) + { + strip.setBrightness(nightBrightness); + } + else + { + strip.setBrightness(dayBrightness); + } + } + else + { + if ((hour(localTime) < 6) | (hour(localTime) >= 22)) + { + strip.setBrightness(nightBrightness); + } + else + { + strip.setBrightness(dayBrightness); + } + } + } + + //loop. You can use "if (WLED_CONNECTED)" to check for successful connection + void loop() + { + + if (millis() - lastTime > 1000) { + //Serial.println("I'm alive!"); + Serial.println(hour(localTime)); + lastTime = millis(); + } + + + if (minute(localTime) != minuteLast) + { + updateLocalTime(); + //timeOfDay(); + minuteLast = minute(localTime); + displayTime(hour(localTime), minute(localTime)); + if (minute(localTime) == 0) + { + hourChime(); + } + if (minute(localTime) == 1) + { + //turn off background segment; + strip.getSegment(0).setOption(2, false); + //applyPreset(13); + } + } + } + + void addToConfig(JsonObject& root) + { + JsonObject modName = root.createNestedObject("id"); + modName["mdns"] = "wled-word-clock"; + modName["name"] = "WLED WORD CLOCK"; + } + + uint16_t getId() + { + return USERMOD_ID_WORD_CLOCK_MATRIX; + } + + +}; diff --git a/usermods/word-clock-matrix/word-clock-matrix.cpp b/usermods/word-clock-matrix/word-clock-matrix.cpp index 628b52625a..67c5b1e472 100644 --- a/usermods/word-clock-matrix/word-clock-matrix.cpp +++ b/usermods/word-clock-matrix/word-clock-matrix.cpp @@ -27,14 +27,14 @@ saveMacro(1, "&FX=0&R=255&G=255&B=255", false); //strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); //select first two segments (background color + FX settable) - WS2812FX::Segment &seg = strip.getSegment(0); + Segment &seg = strip.getSegment(0); seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((0 & 0xFF) << 8) | ((0 & 0xFF))); strip.getSegment(0).setOption(0, false); strip.getSegment(0).setOption(2, false); //other segments are text for (int i = 1; i < 10; i++) { - WS2812FX::Segment &seg = strip.getSegment(i); + Segment &seg = strip.getSegment(i); seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); strip.getSegment(i).setOption(0, true); strip.setBrightness(128); @@ -50,7 +50,7 @@ void selectWordSegments(bool state) { for (int i = 1; i < 10; i++) { - //WS2812FX::Segment &seg = strip.getSegment(i); + //Segment &seg = strip.getSegment(i); strip.getSegment(i).setOption(0, state); // strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); //seg.mode = 12; diff --git a/wled00.sln b/wled00.sln deleted file mode 100644 index b2f12b8359..0000000000 --- a/wled00.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28010.2046 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wled00", "wled00\wled00.vcxproj", "{C5F80730-F44F-4478-BDAE-6634EFC2CA88}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Debug|x86.ActiveCfg = Debug|Win32 - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Debug|x86.Build.0 = Debug|Win32 - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Release|x86.ActiveCfg = Release|Win32 - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Release|x86.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9A679C2B-61D3-400B-B96F-06E604E9CED2} - EndGlobalSection -EndGlobal diff --git a/wled00/FX.cpp b/wled00/FX.cpp index fa6cb35513..cb8813c930 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -24,19 +24,65 @@ Modified heavily for WLED */ -#include "FX.h" #include "wled.h" +#include "FX.h" +#include "fcn_declare.h" #define IBN 5100 -#define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3) + +// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) +#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) +#define PALETTE_MOVING_WRAP !(strip.paletteBlend == 2 || (strip.paletteBlend == 0 && SEGMENT.speed == 0)) + +#define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16)) + +// effect utility functions +uint8_t sin_gap(uint16_t in) { + if (in & 0x100) return 0; + return sin8(in + 192); // correct phase shift of sine so that it starts and stops at 0 +} + +uint16_t triwave16(uint16_t in) { + if (in < 0x8000) return in *2; + return 0xFFFF - (in - 0x8000)*2; +} + +/* + * Generates a tristate square wave w/ attac & decay + * @param x input value 0-255 + * @param pulsewidth 0-127 + * @param attdec attack & decay, max. pulsewidth / 2 + * @returns signed waveform value + */ +int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { + int8_t a = 127; + if (x > 127) { + a = -127; + x -= 127; + } + + if (x < attdec) { //inc to max + return (int16_t) x * a / attdec; + } + else if (x < pulsewidth - attdec) { //max + return a; + } + else if (x < pulsewidth) { //dec to 0 + return (int16_t) (pulsewidth - x) * a / attdec; + } + return 0; +} + +// effect functions /* * No blinking. Just plain old static light. */ -uint16_t WS2812FX::mode_static(void) { - fill(SEGCOLOR(0)); - return (SEGMENT.getOption(SEG_OPTION_TRANSITIONAL)) ? FRAMETIME : 350; //update faster if in transition +uint16_t mode_static(void) { + SEGMENT.fill(SEGCOLOR(0)); + return strip.isOffRefreshRequired() ? FRAMETIME : 350; } +static const char _data_FX_MODE_STATIC[] PROGMEM = "Solid"; /* @@ -44,64 +90,68 @@ uint16_t WS2812FX::mode_static(void) { * Alternate between color1 and color2 * if(strobe == true) then create a strobe effect */ -uint16_t WS2812FX::blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { +uint16_t blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { uint32_t cycleTime = (255 - SEGMENT.speed)*20; uint32_t onTime = FRAMETIME; if (!strobe) onTime += ((cycleTime * SEGMENT.intensity) >> 8); cycleTime += FRAMETIME*2; - uint32_t it = now / cycleTime; - uint32_t rem = now % cycleTime; - + uint32_t it = strip.now / cycleTime; + uint32_t rem = strip.now % cycleTime; + bool on = false; if (it != SEGENV.step //new iteration, force on state for one frame, even if set time is too brief - || rem <= onTime) { + || rem <= onTime) { on = true; } - + SEGENV.step = it; //save previous iteration uint32_t color = on ? color1 : color2; if (color == color1 && do_palette) { - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } - } else fill(color); + } else SEGMENT.fill(color); return FRAMETIME; } /* - * Normal blinking. 50% on/off time. + * Normal blinking. Intensity sets duty cycle. */ -uint16_t WS2812FX::mode_blink(void) { +uint16_t mode_blink(void) { return blink(SEGCOLOR(0), SEGCOLOR(1), false, true); } +static const char _data_FX_MODE_BLINK[] PROGMEM = "Blink@!,Duty cycle;!,!;!;01"; /* * Classic Blink effect. Cycling through the rainbow. */ -uint16_t WS2812FX::mode_blink_rainbow(void) { - return blink(color_wheel(SEGENV.call & 0xFF), SEGCOLOR(1), false, false); +uint16_t mode_blink_rainbow(void) { + return blink(SEGMENT.color_wheel(SEGENV.call & 0xFF), SEGCOLOR(1), false, false); } +static const char _data_FX_MODE_BLINK_RAINBOW[] PROGMEM = "Blink Rainbow@Frequency,Blink duration;!,!;!;01"; /* * Classic Strobe effect. */ -uint16_t WS2812FX::mode_strobe(void) { +uint16_t mode_strobe(void) { return blink(SEGCOLOR(0), SEGCOLOR(1), true, true); } +static const char _data_FX_MODE_STROBE[] PROGMEM = "Strobe@!;!,!;!;01"; /* * Classic Strobe effect. Cycling through the rainbow. */ -uint16_t WS2812FX::mode_strobe_rainbow(void) { - return blink(color_wheel(SEGENV.call & 0xFF), SEGCOLOR(1), true, false); +uint16_t mode_strobe_rainbow(void) { + return blink(SEGMENT.color_wheel(SEGENV.call & 0xFF), SEGCOLOR(1), true, false); } +static const char _data_FX_MODE_STROBE_RAINBOW[] PROGMEM = "Strobe Rainbow@!;,!;!;01"; /* @@ -109,9 +159,9 @@ uint16_t WS2812FX::mode_strobe_rainbow(void) { * LEDs are turned on (color1) in sequence, then turned off (color2) in sequence. * if (bool rev == true) then LEDs are turned off in reverse order */ -uint16_t WS2812FX::color_wipe(bool rev, bool useRandomColors) { +uint16_t color_wipe(bool rev, bool useRandomColors) { uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; - uint32_t perc = now % cycleTime; + uint32_t perc = strip.now % cycleTime; uint16_t prog = (perc * 65535) / cycleTime; bool back = (prog > 32767); if (back) { @@ -142,21 +192,21 @@ uint16_t WS2812FX::color_wipe(bool rev, bool useRandomColors) { rem /= (SEGMENT.intensity +1); if (rem > 255) rem = 255; - uint32_t col1 = useRandomColors? color_wheel(SEGENV.aux1) : SEGCOLOR(1); - for (uint16_t i = 0; i < SEGLEN; i++) + uint32_t col1 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux1) : SEGCOLOR(1); + for (int i = 0; i < SEGLEN; i++) { uint16_t index = (rev && back)? SEGLEN -1 -i : i; - uint32_t col0 = useRandomColors? color_wheel(SEGENV.aux0) : color_from_palette(index, true, PALETTE_SOLID_WRAP, 0); - - if (i < ledIndex) + uint32_t col0 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux0) : SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0); + + if (i < ledIndex) { - setPixelColor(index, back? col1 : col0); + SEGMENT.setPixelColor(index, back? col1 : col0); } else { - setPixelColor(index, back? col0 : col1); - if (i == ledIndex) setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, rem)); + SEGMENT.setPixelColor(index, back? col0 : col1); + if (i == ledIndex) SEGMENT.setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, rem)); } - } + } return FRAMETIME; } @@ -164,43 +214,48 @@ uint16_t WS2812FX::color_wipe(bool rev, bool useRandomColors) { /* * Lights all LEDs one after another. */ -uint16_t WS2812FX::mode_color_wipe(void) { +uint16_t mode_color_wipe(void) { return color_wipe(false, false); } +static const char _data_FX_MODE_COLOR_WIPE[] PROGMEM = "Wipe@!,!;!,!;!"; + /* * Lights all LEDs one after another. Turns off opposite */ -uint16_t WS2812FX::mode_color_sweep(void) { +uint16_t mode_color_sweep(void) { return color_wipe(true, false); } +static const char _data_FX_MODE_COLOR_SWEEP[] PROGMEM = "Sweep@!,!;!,!;!"; /* * Turns all LEDs after each other to a random color. * Then starts over with another color. */ -uint16_t WS2812FX::mode_color_wipe_random(void) { +uint16_t mode_color_wipe_random(void) { return color_wipe(false, true); } +static const char _data_FX_MODE_COLOR_WIPE_RANDOM[] PROGMEM = "Wipe Random@!;;!"; /* * Random color introduced alternating from start and end of strip. */ -uint16_t WS2812FX::mode_color_sweep_random(void) { +uint16_t mode_color_sweep_random(void) { return color_wipe(true, true); } +static const char _data_FX_MODE_COLOR_SWEEP_RANDOM[] PROGMEM = "Sweep Random@!;;!"; /* - * Lights all LEDs in one random color up. Then switches them + * Lights all LEDs up in one random color. Then switches them * to the next random color. */ -uint16_t WS2812FX::mode_random_color(void) { +uint16_t mode_random_color(void) { uint32_t cycleTime = 200 + (255 - SEGMENT.speed)*50; - uint32_t it = now / cycleTime; - uint32_t rem = now % cycleTime; + uint32_t it = strip.now / cycleTime; + uint32_t rem = strip.now % cycleTime; uint16_t fadedur = (cycleTime * SEGMENT.intensity) >> 8; uint32_t fade = 255; @@ -220,119 +275,124 @@ uint16_t WS2812FX::mode_random_color(void) { SEGENV.step = it; } - fill(color_blend(color_wheel(SEGENV.aux1), color_wheel(SEGENV.aux0), fade)); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), fade)); return FRAMETIME; } +static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade time;;!;01"; /* * Lights every LED in a random color. Changes all LED at the same time * to new random colors. */ -uint16_t WS2812FX::dynamic(boolean smooth=false) { +uint16_t mode_dynamic(void) { if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed - + if(SEGENV.call == 0) { - for (uint16_t i = 0; i < SEGLEN; i++) SEGENV.data[i] = random8(); + //SEGMENT.fill(BLACK); + for (int i = 0; i < SEGLEN; i++) SEGENV.data[i] = random8(); } uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*15; - uint32_t it = now / cycleTime; + uint32_t it = strip.now / cycleTime; if (it != SEGENV.step && SEGMENT.speed != 0) //new color { - for (uint16_t i = 0; i < SEGLEN; i++) { - if (random8() <= SEGMENT.intensity) SEGENV.data[i] = random8(); + for (int i = 0; i < SEGLEN; i++) { + if (random8() <= SEGMENT.intensity) SEGENV.data[i] = random8(); // random color index } SEGENV.step = it; } - - if (smooth) { - for (uint16_t i = 0; i < SEGLEN; i++) { - blendPixelColor(i, color_wheel(SEGENV.data[i]),16); + + if (SEGMENT.check1) { + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.blendPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i]), 16); } } else { - for (uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_wheel(SEGENV.data[i])); + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i])); } - } + } return FRAMETIME; } +static const char _data_FX_MODE_DYNAMIC[] PROGMEM = "Dynamic@!,!,,,,Smooth;;!"; -/* - * Original effect "Dynamic" - */ -uint16_t WS2812FX::mode_dynamic(void) { - return dynamic(false); -} /* - * effect "Dynamic" with smoth color-fading + * effect "Dynamic" with smooth color-fading */ -uint16_t WS2812FX::mode_dynamic_smooth(void) { - return dynamic(true); +uint16_t mode_dynamic_smooth(void) { + bool old = SEGMENT.check1; + SEGMENT.check1 = true; + mode_dynamic(); + SEGMENT.check1 = old; + return FRAMETIME; } +static const char _data_FX_MODE_DYNAMIC_SMOOTH[] PROGMEM = "Dynamic Smooth@!,!;;!"; + /* * Does the "standby-breathing" of well known i-Devices. */ -uint16_t WS2812FX::mode_breath(void) { +uint16_t mode_breath(void) { uint16_t var = 0; - uint16_t counter = (now * ((SEGMENT.speed >> 3) +10)); + uint16_t counter = (strip.now * ((SEGMENT.speed >> 3) +10)); counter = (counter >> 2) + (counter >> 4); //0-16384 + 0-2048 if (counter < 16384) { if (counter > 8192) counter = 8192 - (counter - 8192); var = sin16(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 } - + uint8_t lum = 30 + var; - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_blend(SEGCOLOR(1), color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } return FRAMETIME; } +static const char _data_FX_MODE_BREATH[] PROGMEM = "Breathe@!;!,!;!;01"; /* * Fades the LEDs between two colors */ -uint16_t WS2812FX::mode_fade(void) { - uint16_t counter = (now * ((SEGMENT.speed >> 3) +10)); +uint16_t mode_fade(void) { + uint16_t counter = (strip.now * ((SEGMENT.speed >> 3) +10)); uint8_t lum = triwave16(counter) >> 8; - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_blend(SEGCOLOR(1), color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } return FRAMETIME; } +static const char _data_FX_MODE_FADE[] PROGMEM = "Fade@!;!,!;!;01"; /* * Scan mode parent function */ -uint16_t WS2812FX::scan(bool dual) +uint16_t scan(bool dual) { uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; - uint32_t perc = now % cycleTime; + uint32_t perc = strip.now % cycleTime; uint16_t prog = (perc * 65535) / cycleTime; uint16_t size = 1 + ((SEGMENT.intensity * SEGLEN) >> 9); uint16_t ledIndex = (prog * ((SEGLEN *2) - size *2)) >> 16; - fill(SEGCOLOR(1)); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); int led_offset = ledIndex - (SEGLEN - size); led_offset = abs(led_offset); if (dual) { - for (uint16_t j = led_offset; j < led_offset + size; j++) { + for (int j = led_offset; j < led_offset + size; j++) { uint16_t i2 = SEGLEN -1 -j; - setPixelColor(i2, color_from_palette(i2, true, PALETTE_SOLID_WRAP, (SEGCOLOR(2))? 2:0)); + SEGMENT.setPixelColor(i2, SEGMENT.color_from_palette(i2, true, PALETTE_SOLID_WRAP, (SEGCOLOR(2))? 2:0)); } } - for (uint16_t j = led_offset; j < led_offset + size; j++) { - setPixelColor(j, color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); + for (int j = led_offset; j < led_offset + size; j++) { + SEGMENT.setPixelColor(j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; @@ -342,49 +402,82 @@ uint16_t WS2812FX::scan(bool dual) /* * Runs a single pixel back and forth. */ -uint16_t WS2812FX::mode_scan(void) { +uint16_t mode_scan(void) { return scan(false); } +static const char _data_FX_MODE_SCAN[] PROGMEM = "Scan@!,# of dots,,,,,Overlay;!,!,!;!"; /* * Runs two pixel back and forth in opposite directions. */ -uint16_t WS2812FX::mode_dual_scan(void) { +uint16_t mode_dual_scan(void) { return scan(true); } +static const char _data_FX_MODE_DUAL_SCAN[] PROGMEM = "Scan Dual@!,# of dots,,,,,Overlay;!,!,!;!"; /* * Cycles all LEDs at once through a rainbow. */ -uint16_t WS2812FX::mode_rainbow(void) { - uint16_t counter = (now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; +uint16_t mode_rainbow(void) { + uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; if (SEGMENT.intensity < 128){ - fill(color_blend(color_wheel(counter),WHITE,128-SEGMENT.intensity)); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(counter),WHITE,128-SEGMENT.intensity)); } else { - fill(color_wheel(counter)); + SEGMENT.fill(SEGMENT.color_wheel(counter)); } return FRAMETIME; } +static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!;01"; /* * Cycles a rainbow over the entire string of LEDs. */ -uint16_t WS2812FX::mode_rainbow_cycle(void) { - uint16_t counter = (now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; +uint16_t mode_rainbow_cycle(void) { + uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; - - for(uint16_t i = 0; i < SEGLEN; i++) { + + for (int i = 0; i < SEGLEN; i++) { //intensity/29 = 0 (1/16) 1 (1/8) 2 (1/4) 3 (1/2) 4 (1) 5 (2) 6 (4) 7 (8) 8 (16) uint8_t index = (i * (16 << (SEGMENT.intensity /29)) / SEGLEN) + counter; - setPixelColor(i, color_wheel(index)); + SEGMENT.setPixelColor(i, SEGMENT.color_wheel(index)); + } + + return FRAMETIME; +} +static const char _data_FX_MODE_RAINBOW_CYCLE[] PROGMEM = "Rainbow@!,Size;;!"; + + +/* + * Alternating pixels running function. + */ +uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) { + uint8_t width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window + uint32_t cycleTime = 50 + (255 - SEGMENT.speed); + uint32_t it = strip.now / cycleTime; + bool usePalette = color1 == SEGCOLOR(0); + + for (int i = 0; i < SEGLEN; i++) { + uint32_t col = color2; + if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); + if (theatre) { + if ((i % width) == SEGENV.aux0) col = color1; + } else { + int8_t pos = (i % (width<<1)); + if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1; + } + SEGMENT.setPixelColor(i,col); } + if (it != SEGENV.step) { + SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1)); + SEGENV.step = it; + } return FRAMETIME; } @@ -393,28 +486,30 @@ uint16_t WS2812FX::mode_rainbow_cycle(void) { * Theatre-style crawling lights. * Inspired by the Adafruit examples. */ -uint16_t WS2812FX::mode_theater_chase(void) { +uint16_t mode_theater_chase(void) { return running(SEGCOLOR(0), SEGCOLOR(1), true); } +static const char _data_FX_MODE_THEATER_CHASE[] PROGMEM = "Theater@!,Gap size;!,!;!"; /* * Theatre-style crawling lights with rainbow effect. * Inspired by the Adafruit examples. */ -uint16_t WS2812FX::mode_theater_chase_rainbow(void) { - return running(color_wheel(SEGENV.step), SEGCOLOR(1), true); +uint16_t mode_theater_chase_rainbow(void) { + return running(SEGMENT.color_wheel(SEGENV.step), SEGCOLOR(1), true); } +static const char _data_FX_MODE_THEATER_CHASE_RAINBOW[] PROGMEM = "Theater Rainbow@!,Gap size;,!;!"; /* * Running lights effect with smooth sine transition base. */ -uint16_t WS2812FX::running_base(bool saw, bool dual=false) { +uint16_t running_base(bool saw, bool dual=false) { uint8_t x_scale = SEGMENT.intensity >> 2; - uint32_t counter = (now * SEGMENT.speed) >> 9; + uint32_t counter = (strip.now * SEGMENT.speed) >> 9; - for(uint16_t i = 0; i < SEGLEN; i++) { + for (int i = 0; i < SEGLEN; i++) { uint16_t a = i*x_scale - counter; if (saw) { a &= 0xFF; @@ -427,15 +522,16 @@ uint16_t WS2812FX::running_base(bool saw, bool dual=false) { a = 255 - a; } uint8_t s = dual ? sin_gap(a) : sin8(a); - uint32_t ca = color_blend(SEGCOLOR(1), color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s); + uint32_t ca = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s); if (dual) { uint16_t b = (SEGLEN-1-i)*x_scale - counter; uint8_t t = sin_gap(b); - uint32_t cb = color_blend(SEGCOLOR(1), color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), t); + uint32_t cb = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), t); ca = color_blend(ca, cb, 127); } - setPixelColor(i, ca); + SEGMENT.setPixelColor(i, ca); } + return FRAMETIME; } @@ -444,36 +540,39 @@ uint16_t WS2812FX::running_base(bool saw, bool dual=false) { * Running lights in opposite directions. * Idea: Make the gap width controllable with a third slider in the future */ -uint16_t WS2812FX::mode_running_dual(void) { +uint16_t mode_running_dual(void) { return running_base(false, true); } +static const char _data_FX_MODE_RUNNING_DUAL[] PROGMEM = "Running Dual@!,Wave width;L,!,R;!"; /* * Running lights effect with smooth sine transition. */ -uint16_t WS2812FX::mode_running_lights(void) { +uint16_t mode_running_lights(void) { return running_base(false); } +static const char _data_FX_MODE_RUNNING_LIGHTS[] PROGMEM = "Running@!,Wave width;!,!;!"; /* * Running lights effect with sawtooth transition. */ -uint16_t WS2812FX::mode_saw(void) { +uint16_t mode_saw(void) { return running_base(true); } +static const char _data_FX_MODE_SAW[] PROGMEM = "Saw@!,Width;!,!;!"; /* * Blink several LEDs in random colors on, reset, repeat. * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ -uint16_t WS2812FX::mode_twinkle(void) { - fill(SEGCOLOR(1)); +uint16_t mode_twinkle(void) { + SEGMENT.fade_out(224); uint32_t cycleTime = 20 + (255 - SEGMENT.speed)*5; - uint32_t it = now / cycleTime; + uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { uint16_t maxOn = map(SEGMENT.intensity, 0, 255, 1, SEGLEN); // make sure at least one LED is on @@ -485,7 +584,7 @@ uint16_t WS2812FX::mode_twinkle(void) { SEGENV.aux0++; SEGENV.step = it; } - + uint16_t PRNG16 = SEGENV.aux1; for (uint16_t i = 0; i < SEGENV.aux0; i++) @@ -493,46 +592,61 @@ uint16_t WS2812FX::mode_twinkle(void) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next 'random' number uint32_t p = (uint32_t)SEGLEN * (uint32_t)PRNG16; uint16_t j = p >> 16; - setPixelColor(j, color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; } +static const char _data_FX_MODE_TWINKLE[] PROGMEM = "Twinkle@!,!;!,!;!;;m12=0"; //pixels /* * Dissolve function */ -uint16_t WS2812FX::dissolve(uint32_t color) { - bool wa = (SEGCOLOR(1) != 0 && _brightness < 255); //workaround, can't compare getPixel to color if not full brightness - - for (uint16_t j = 0; j <= SEGLEN / 15; j++) - { +uint16_t dissolve(uint32_t color) { + uint16_t dataSize = (SEGLEN+7) >> 3; //1 bit per LED + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + + if (SEGENV.call == 0) { + memset(SEGMENT.data, 0xFF, dataSize); // start by fading pixels up + SEGENV.aux0 = 1; + } + + for (int j = 0; j <= SEGLEN / 15; j++) { if (random8() <= SEGMENT.intensity) { - for (uint8_t times = 0; times < 10; times++) //attempt to spawn a new pixel 5 times - { - uint16_t i = random16(SEGLEN); + for (size_t times = 0; times < 10; times++) { //attempt to spawn a new pixel 10 times + unsigned i = random16(SEGLEN); + unsigned index = i >> 3; + unsigned bitNum = i & 0x07; + bool fadeUp = bitRead(SEGENV.data[index], bitNum); if (SEGENV.aux0) { //dissolve to primary/palette - if (getPixelColor(i) == SEGCOLOR(1) || wa) { - if (color == SEGCOLOR(0)) - { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); - } else { setPixelColor(i, color); } + if (fadeUp) { + if (color == SEGCOLOR(0)) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); + } else { + SEGMENT.setPixelColor(i, color); + } + bitWrite(SEGENV.data[index], bitNum, false); break; //only spawn 1 new pixel per frame per 50 LEDs } } else { //dissolve to secondary - if (getPixelColor(i) != SEGCOLOR(1)) { setPixelColor(i, SEGCOLOR(1)); break; } + if (!fadeUp) { + SEGMENT.setPixelColor(i, SEGCOLOR(1)); break; + bitWrite(SEGENV.data[index], bitNum, true); + } } } } } - if (SEGENV.call > (255 - SEGMENT.speed) + 15U) - { + if (SEGENV.step > (255 - SEGMENT.speed) + 15U) { SEGENV.aux0 = !SEGENV.aux0; - SEGENV.call = 0; + SEGENV.step = 0; + memset(SEGMENT.data, (SEGENV.aux0 ? 0xFF : 0), dataSize); // switch fading + } else { + SEGENV.step++; } - + return FRAMETIME; } @@ -540,120 +654,127 @@ uint16_t WS2812FX::dissolve(uint32_t color) { /* * Blink several LEDs on and then off */ -uint16_t WS2812FX::mode_dissolve(void) { - return dissolve(SEGCOLOR(0)); +uint16_t mode_dissolve(void) { + return dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(random8()) : SEGCOLOR(0)); } +static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random;!,!;!"; /* * Blink several LEDs on and then off in random colors */ -uint16_t WS2812FX::mode_dissolve_random(void) { - return dissolve(color_wheel(random8())); +uint16_t mode_dissolve_random(void) { + return dissolve(SEGMENT.color_wheel(random8())); } +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/ */ -uint16_t WS2812FX::mode_sparkle(void) { - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); +uint16_t mode_sparkle(void) { + if (!SEGMENT.check2) for(int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } uint32_t cycleTime = 10 + (255 - SEGMENT.speed)*2; - uint32_t it = now / cycleTime; + uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { SEGENV.aux0 = random16(SEGLEN); // aux0 stores the random led index SEGENV.step = it; } - - setPixelColor(SEGENV.aux0, SEGCOLOR(0)); + + SEGMENT.setPixelColor(SEGENV.aux0, SEGCOLOR(0)); return FRAMETIME; } +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/ */ -uint16_t WS2812FX::mode_flash_sparkle(void) { - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); +uint16_t mode_flash_sparkle(void) { + if (!SEGMENT.check2) for(uint16_t i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } - if (now - SEGENV.aux0 > SEGENV.step) { + if (strip.now - SEGENV.aux0 > SEGENV.step) { if(random8((255-SEGMENT.intensity) >> 4) == 0) { - setPixelColor(random16(SEGLEN), SEGCOLOR(1)); //flash + SEGMENT.setPixelColor(random16(SEGLEN), SEGCOLOR(1)); //flash } - SEGENV.step = now; + SEGENV.step = strip.now; SEGENV.aux0 = 255-SEGMENT.speed; } return FRAMETIME; } +static const char _data_FX_MODE_FLASH_SPARKLE[] PROGMEM = "Sparkle Dark@!,!,,,,,Overlay;Bg,Fx;!;;m12=0"; /* * Like flash sparkle. With more flash. * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ -uint16_t WS2812FX::mode_hyper_sparkle(void) { - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); +uint16_t mode_hyper_sparkle(void) { + if (!SEGMENT.check2) for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } - if (now - SEGENV.aux0 > SEGENV.step) { - if(random8((255-SEGMENT.intensity) >> 4) == 0) { - for(uint16_t i = 0; i < MAX(1, SEGLEN/3); i++) { - setPixelColor(random16(SEGLEN), SEGCOLOR(1)); + if (strip.now - SEGENV.aux0 > SEGENV.step) { + if (random8((255-SEGMENT.intensity) >> 4) == 0) { + for (int i = 0; i < max(1, SEGLEN/3); i++) { + SEGMENT.setPixelColor(random16(SEGLEN), SEGCOLOR(1)); } } - SEGENV.step = now; + SEGENV.step = strip.now; SEGENV.aux0 = 255-SEGMENT.speed; } return FRAMETIME; } +static const char _data_FX_MODE_HYPER_SPARKLE[] PROGMEM = "Sparkle+@!,!,,,,,Overlay;Bg,Fx;!;;m12=0"; /* * Strobe effect with different strobe count and pause, controlled by speed. */ -uint16_t WS2812FX::mode_multi_strobe(void) { - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); +uint16_t mode_multi_strobe(void) { + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } SEGENV.aux0 = 50 + 20*(uint16_t)(255-SEGMENT.speed); uint16_t count = 2 * ((SEGMENT.intensity / 10) + 1); if(SEGENV.aux1 < count) { if((SEGENV.aux1 & 1) == 0) { - fill(SEGCOLOR(0)); + SEGMENT.fill(SEGCOLOR(0)); SEGENV.aux0 = 15; } else { SEGENV.aux0 = 50; } } - if (now - SEGENV.aux0 > SEGENV.step) { + if (strip.now - SEGENV.aux0 > SEGENV.step) { SEGENV.aux1++; if (SEGENV.aux1 > count) SEGENV.aux1 = 0; - SEGENV.step = now; + SEGENV.step = strip.now; } return FRAMETIME; } +static const char _data_FX_MODE_MULTI_STROBE[] PROGMEM = "Strobe Mega@!,!;!,!;!;01"; + /* * Android loading circle */ -uint16_t WS2812FX::mode_android(void) { - - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); +uint16_t mode_android(void) { + + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } - if (SEGENV.aux1 > ((float)SEGMENT.intensity/255.0)*(float)SEGLEN) + if (SEGENV.aux1 > (SEGMENT.intensity*SEGLEN)/255) { SEGENV.aux0 = 1; } else @@ -662,7 +783,7 @@ uint16_t WS2812FX::mode_android(void) { } uint16_t a = SEGENV.step; - + if (SEGENV.aux0 == 0) { if (SEGENV.call %3 == 1) {a++;} @@ -672,36 +793,38 @@ uint16_t WS2812FX::mode_android(void) { a++; if (SEGENV.call %3 != 1) SEGENV.aux1--; } - + if (a >= SEGLEN) a = 0; if (a + SEGENV.aux1 < SEGLEN) { - for(int i = a; i < a+SEGENV.aux1; i++) { - setPixelColor(i, SEGCOLOR(0)); + for (int i = a; i < a+SEGENV.aux1; i++) { + SEGMENT.setPixelColor(i, SEGCOLOR(0)); } } else { - for(int i = a; i < SEGLEN; i++) { - setPixelColor(i, SEGCOLOR(0)); + for (int i = a; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGCOLOR(0)); } - for(int i = 0; i < SEGENV.aux1 - (SEGLEN -a); i++) { - setPixelColor(i, SEGCOLOR(0)); + for (int i = 0; i < SEGENV.aux1 - (SEGLEN -a); i++) { + SEGMENT.setPixelColor(i, SEGCOLOR(0)); } } SEGENV.step = a; return 3 + ((8 * (uint32_t)(255 - SEGMENT.speed)) / SEGLEN); } +static const char _data_FX_MODE_ANDROID[] PROGMEM = "Android@!,Width;!,!;!;;m12=1"; //vertical + /* * color chase function. * color1 = background color * color2 and color3 = colors of two adjacent leds */ -uint16_t WS2812FX::chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palette) { - uint16_t counter = now * ((SEGMENT.speed >> 2) + 1); - uint16_t a = counter * SEGLEN >> 16; +uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palette) { + uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1); + uint16_t a = (counter * SEGLEN) >> 16; bool chase_random = (SEGMENT.mode == FX_MODE_CHASE_RANDOM); if (chase_random) { @@ -710,14 +833,14 @@ uint16_t WS2812FX::chase(uint32_t color1, uint32_t color2, uint32_t color3, bool SEGENV.aux1 = SEGENV.aux0; //store previous random color SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); } - color1 = color_wheel(SEGENV.aux0); + color1 = SEGMENT.color_wheel(SEGENV.aux0); } SEGENV.step = a; // Use intensity setting to vary chase up to 1/2 string length - uint8_t size = 1 + (SEGMENT.intensity * SEGLEN >> 10); + uint8_t size = 1 + ((SEGMENT.intensity * SEGLEN) >> 10); - uint16_t b = a + size; //"trail" of chase, filled with color1 + uint16_t b = a + size; //"trail" of chase, filled with color1 if (b > SEGLEN) b -= SEGLEN; uint16_t c = b + size; if (c > SEGLEN) c -= SEGLEN; @@ -725,41 +848,41 @@ uint16_t WS2812FX::chase(uint32_t color1, uint32_t color2, uint32_t color3, bool //background if (do_palette) { - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } - } else fill(color1); + } else SEGMENT.fill(color1); //if random, fill old background between a and end if (chase_random) { - color1 = color_wheel(SEGENV.aux1); - for (uint16_t i = a; i < SEGLEN; i++) - setPixelColor(i, color1); + color1 = SEGMENT.color_wheel(SEGENV.aux1); + for (int i = a; i < SEGLEN; i++) + SEGMENT.setPixelColor(i, color1); } //fill between points a and b with color2 if (a < b) { - for (uint16_t i = a; i < b; i++) - setPixelColor(i, color2); + for (int i = a; i < b; i++) + SEGMENT.setPixelColor(i, color2); } else { - for (uint16_t i = a; i < SEGLEN; i++) //fill until end - setPixelColor(i, color2); - for (uint16_t i = 0; i < b; i++) //fill from start until b - setPixelColor(i, color2); + for (int i = a; i < SEGLEN; i++) //fill until end + SEGMENT.setPixelColor(i, color2); + for (int i = 0; i < b; i++) //fill from start until b + SEGMENT.setPixelColor(i, color2); } //fill between points b and c with color2 if (b < c) { - for (uint16_t i = b; i < c; i++) - setPixelColor(i, color3); + for (int i = b; i < c; i++) + SEGMENT.setPixelColor(i, color3); } else { - for (uint16_t i = b; i < SEGLEN; i++) //fill until end - setPixelColor(i, color3); - for (uint16_t i = 0; i < c; i++) //fill from start until c - setPixelColor(i, color3); + for (int i = b; i < SEGLEN; i++) //fill until end + SEGMENT.setPixelColor(i, color3); + for (int i = 0; i < c; i++) //fill from start until c + SEGMENT.setPixelColor(i, color3); } return FRAMETIME; @@ -769,60 +892,64 @@ uint16_t WS2812FX::chase(uint32_t color1, uint32_t color2, uint32_t color3, bool /* * Bicolor chase, more primary color. */ -uint16_t WS2812FX::mode_chase_color(void) { +uint16_t mode_chase_color(void) { return chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), true); } +static const char _data_FX_MODE_CHASE_COLOR[] PROGMEM = "Chase@!,Width;!,!,!;!"; /* * Primary running followed by random color. */ -uint16_t WS2812FX::mode_chase_random(void) { +uint16_t mode_chase_random(void) { return chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), false); } +static const char _data_FX_MODE_CHASE_RANDOM[] PROGMEM = "Chase Random@!,Width;!,,!;!"; /* * Primary, secondary running on rainbow. */ -uint16_t WS2812FX::mode_chase_rainbow(void) { +uint16_t mode_chase_rainbow(void) { uint8_t color_sep = 256 / SEGLEN; if (color_sep == 0) color_sep = 1; // correction for segments longer than 256 LEDs uint8_t color_index = SEGENV.call & 0xFF; - uint32_t color = color_wheel(((SEGENV.step * color_sep) + color_index) & 0xFF); + uint32_t color = SEGMENT.color_wheel(((SEGENV.step * color_sep) + color_index) & 0xFF); return chase(color, SEGCOLOR(0), SEGCOLOR(1), false); } +static const char _data_FX_MODE_CHASE_RAINBOW[] PROGMEM = "Chase Rainbow@!,Width;!,!;!"; /* * Primary running on rainbow. */ -uint16_t WS2812FX::mode_chase_rainbow_white(void) { +uint16_t mode_chase_rainbow_white(void) { uint16_t n = SEGENV.step; uint16_t m = (SEGENV.step + 1) % SEGLEN; - uint32_t color2 = color_wheel(((n * 256 / SEGLEN) + (SEGENV.call & 0xFF)) & 0xFF); - uint32_t color3 = color_wheel(((m * 256 / SEGLEN) + (SEGENV.call & 0xFF)) & 0xFF); + uint32_t color2 = SEGMENT.color_wheel(((n * 256 / SEGLEN) + (SEGENV.call & 0xFF)) & 0xFF); + uint32_t color3 = SEGMENT.color_wheel(((m * 256 / SEGLEN) + (SEGENV.call & 0xFF)) & 0xFF); return chase(SEGCOLOR(0), color2, color3, false); } +static const char _data_FX_MODE_CHASE_RAINBOW_WHITE[] PROGMEM = "Rainbow Runner@!,Size;Bg;!"; /* * Red - Amber - Green - Blue lights running */ -uint16_t WS2812FX::mode_colorful(void) { +uint16_t mode_colorful(void) { uint8_t numColors = 4; //3, 4, or 5 uint32_t cols[9]{0x00FF0000,0x00EEBB00,0x0000EE00,0x000077CC}; if (SEGMENT.intensity > 160 || SEGMENT.palette) { //palette or color if (!SEGMENT.palette) { numColors = 3; - for (uint8_t i = 0; i < 3; i++) cols[i] = SEGCOLOR(i); + for (size_t i = 0; i < 3; i++) cols[i] = SEGCOLOR(i); } else { uint16_t fac = 80; if (SEGMENT.palette == 52) {numColors = 5; fac = 61;} //C9 2 has 5 colors - for (uint8_t i = 0; i < numColors; i++) { - cols[i] = color_from_palette(i*fac, false, true, 255); + for (size_t i = 0; i < numColors; i++) { + cols[i] = SEGMENT.color_from_palette(i*fac, false, true, 255); } } } else if (SEGMENT.intensity < 80) //pastel (easter) colors @@ -832,65 +959,69 @@ uint16_t WS2812FX::mode_colorful(void) { cols[2] = 0x0077FF77; cols[3] = 0x0077F0F0; } - for (uint8_t i = numColors; i < numColors*2 -1; i++) cols[i] = cols[i-numColors]; - + for (size_t i = numColors; i < numColors*2 -1U; i++) cols[i] = cols[i-numColors]; + uint32_t cycleTime = 50 + (8 * (uint32_t)(255 - SEGMENT.speed)); - uint32_t it = now / cycleTime; + uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { if (SEGMENT.speed > 0) SEGENV.aux0++; if (SEGENV.aux0 >= numColors) SEGENV.aux0 = 0; SEGENV.step = it; } - - for (uint16_t i = 0; i < SEGLEN; i+= numColors) + + for (int i = 0; i < SEGLEN; i+= numColors) { - for (uint16_t j = 0; j < numColors; j++) setPixelColor(i + j, cols[SEGENV.aux0 + j]); + for (int j = 0; j < numColors; j++) SEGMENT.setPixelColor(i + j, cols[SEGENV.aux0 + j]); } - + return FRAMETIME; } +static const char _data_FX_MODE_COLORFUL[] PROGMEM = "Colorful@!,Saturation;1,2,3;!"; /* * Emulates a traffic light. */ -uint16_t WS2812FX::mode_traffic_light(void) { - for(uint16_t i=0; i < SEGLEN; i++) - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); +uint16_t mode_traffic_light(void) { + if (SEGLEN == 1) return mode_static(); + for (int i=0; i < SEGLEN; i++) + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); uint32_t mdelay = 500; for (int i = 0; i < SEGLEN-2 ; i+=3) { switch (SEGENV.aux0) { - case 0: setPixelColor(i, 0x00FF0000); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break; - case 1: setPixelColor(i, 0x00FF0000); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed)); setPixelColor(i+1, 0x00EECC00); break; - case 2: setPixelColor(i+2, 0x0000FF00); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break; - case 3: setPixelColor(i+1, 0x00EECC00); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed));break; + case 0: SEGMENT.setPixelColor(i, 0x00FF0000); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break; + case 1: SEGMENT.setPixelColor(i, 0x00FF0000); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed)); SEGMENT.setPixelColor(i+1, 0x00EECC00); break; + case 2: SEGMENT.setPixelColor(i+2, 0x0000FF00); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break; + case 3: SEGMENT.setPixelColor(i+1, 0x00EECC00); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed));break; } } - if (now - SEGENV.step > mdelay) + if (strip.now - SEGENV.step > mdelay) { SEGENV.aux0++; if (SEGENV.aux0 == 1 && SEGMENT.intensity > 140) SEGENV.aux0 = 2; //skip Red + Amber, to get US-style sequence if (SEGENV.aux0 > 3) SEGENV.aux0 = 0; - SEGENV.step = now; + SEGENV.step = strip.now; } - + return FRAMETIME; } +static const char _data_FX_MODE_TRAFFIC_LIGHT[] PROGMEM = "Traffic Light@!,US style;,!;!"; /* * Sec flashes running on prim. */ #define FLASH_COUNT 4 -uint16_t WS2812FX::mode_chase_flash(void) { +uint16_t mode_chase_flash(void) { + if (SEGLEN == 1) return mode_static(); uint8_t flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } uint16_t delay = 10 + ((30 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); @@ -898,8 +1029,8 @@ uint16_t WS2812FX::mode_chase_flash(void) { if(flash_step % 2 == 0) { uint16_t n = SEGENV.step; uint16_t m = (SEGENV.step + 1) % SEGLEN; - setPixelColor( n, SEGCOLOR(1)); - setPixelColor( m, SEGCOLOR(1)); + SEGMENT.setPixelColor( n, SEGCOLOR(1)); + SEGMENT.setPixelColor( m, SEGCOLOR(1)); delay = 20; } else { delay = 30; @@ -909,98 +1040,60 @@ uint16_t WS2812FX::mode_chase_flash(void) { } return delay; } +static const char _data_FX_MODE_CHASE_FLASH[] PROGMEM = "Chase Flash@!;Bg,Fx;!"; /* * Prim flashes running, followed by random color. */ -uint16_t WS2812FX::mode_chase_flash_random(void) { +uint16_t mode_chase_flash_random(void) { + if (SEGLEN == 1) return mode_static(); uint8_t flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); - for(uint16_t i = 0; i < SEGENV.step; i++) { - setPixelColor(i, color_wheel(SEGENV.aux0)); + for (int i = 0; i < SEGENV.aux1; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0)); } uint16_t delay = 1 + ((10 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); if(flash_step < (FLASH_COUNT * 2)) { - uint16_t n = SEGENV.step; - uint16_t m = (SEGENV.step + 1) % SEGLEN; + uint16_t n = SEGENV.aux1; + uint16_t m = (SEGENV.aux1 + 1) % SEGLEN; if(flash_step % 2 == 0) { - setPixelColor( n, SEGCOLOR(0)); - setPixelColor( m, SEGCOLOR(0)); + SEGMENT.setPixelColor( n, SEGCOLOR(0)); + SEGMENT.setPixelColor( m, SEGCOLOR(0)); delay = 20; } else { - setPixelColor( n, color_wheel(SEGENV.aux0)); - setPixelColor( m, SEGCOLOR(1)); + SEGMENT.setPixelColor( n, SEGMENT.color_wheel(SEGENV.aux0)); + SEGMENT.setPixelColor( m, SEGCOLOR(1)); delay = 30; } } else { - SEGENV.step = (SEGENV.step + 1) % SEGLEN; + SEGENV.aux1 = (SEGENV.aux1 + 1) % SEGLEN; - if (SEGENV.step == 0) { + if (SEGENV.aux1 == 0) { SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); } } return delay; } +static const char _data_FX_MODE_CHASE_FLASH_RANDOM[] PROGMEM = "Chase Flash Rnd@!;!,!;!"; -/* - * Alternating pixels running function. - */ -uint16_t WS2812FX::running(uint32_t color1, uint32_t color2, bool theatre) { - uint8_t width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window - uint32_t cycleTime = 50 + (255 - SEGMENT.speed); - uint32_t it = now / cycleTime; - bool usePalette = color1 == SEGCOLOR(0); - - for(uint16_t i = 0; i < SEGLEN; i++) { - uint32_t col = color2; - if (usePalette) color1 = color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); - if (theatre) { - if ((i % width) == SEGENV.aux0) col = color1; - } else { - int8_t pos = (i % (width<<1)); - if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1; - } - setPixelColor(i,col); - } - - if (it != SEGENV.step) { - SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1)); - SEGENV.step = it; - } - return FRAMETIME; -} - /* * Alternating color/sec pixels running. */ -uint16_t WS2812FX::mode_running_color(void) { +uint16_t mode_running_color(void) { return running(SEGCOLOR(0), SEGCOLOR(1)); } - -/* - * Alternating red/white pixels running. - */ -uint16_t WS2812FX::mode_candy_cane(void) { - return running(RED, WHITE); -} - -/* - * Alternating orange/purple pixels running. - */ -uint16_t WS2812FX::mode_halloween(void) { - return running(PURPLE, ORANGE); -} +static const char _data_FX_MODE_RUNNING_COLOR[] PROGMEM = "Chase 2@!,Width;!,!;!"; /* * Random colored pixels running. ("Stream") */ -uint16_t WS2812FX::mode_running_random(void) { +uint16_t mode_running_random(void) { uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); - uint32_t it = now / cycleTime; + uint32_t it = strip.now / cycleTime; if (SEGENV.call == 0) SEGENV.aux0 = random16(); // random seed for PRNG on start uint8_t zoneSize = ((255-SEGMENT.intensity) >> 4) +1; @@ -1008,7 +1101,7 @@ uint16_t WS2812FX::mode_running_random(void) { uint8_t z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); - for (uint16_t i=SEGLEN-1; i > 0; i--) { + for (int i=SEGLEN-1; i > 0; i--) { if (nzone || z >= zoneSize) { uint8_t lastrand = PRNG16 >> 8; int16_t diff = 0; @@ -1022,47 +1115,42 @@ uint16_t WS2812FX::mode_running_random(void) { } z = 0; } - setPixelColor(i, color_wheel(PRNG16 >> 8)); + SEGMENT.setPixelColor(i, SEGMENT.color_wheel(PRNG16 >> 8)); z++; } SEGENV.aux1 = it; return FRAMETIME; } +static const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = "Stream@!,Zone size;;!"; -/* - * K.I.T.T. - */ -uint16_t WS2812FX::mode_larson_scanner(void){ - return larson_scanner(false); -} - -uint16_t WS2812FX::larson_scanner(bool dual) { - uint16_t counter = now * ((SEGMENT.speed >> 2) +8); - uint16_t index = counter * SEGLEN >> 16; +uint16_t larson_scanner(bool dual) { + if (SEGLEN == 1) return mode_static(); + uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); + uint16_t index = (counter * SEGLEN) >> 16; - fade_out(SEGMENT.intensity); + SEGMENT.fade_out(SEGMENT.intensity); if (SEGENV.step > index && SEGENV.step - index > SEGLEN/2) { SEGENV.aux0 = !SEGENV.aux0; } - - for (uint16_t i = SEGENV.step; i < index; i++) { + + for (int i = SEGENV.step; i < index; i++) { uint16_t j = (SEGENV.aux0)?i:SEGLEN-1-i; - setPixelColor( j, color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor( j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); } if (dual) { uint32_t c; if (SEGCOLOR(2) != 0) { c = SEGCOLOR(2); } else { - c = color_from_palette(index, true, PALETTE_SOLID_WRAP, 0); + c = SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0); } - for (uint16_t i = SEGENV.step; i < index; i++) { + for (int i = SEGENV.step; i < index; i++) { uint16_t j = (SEGENV.aux0)?SEGLEN-1-i:i; - setPixelColor(j, c); + SEGMENT.setPixelColor(j, c); } } @@ -1071,136 +1159,180 @@ uint16_t WS2812FX::larson_scanner(bool dual) { } +/* + * K.I.T.T. + */ +uint16_t mode_larson_scanner(void){ + return larson_scanner(false); +} +static const char _data_FX_MODE_LARSON_SCANNER[] PROGMEM = "Scanner@!,Fade rate;!,!;!;;m12=0"; + + +/* + * Creates two Larson scanners moving in opposite directions + * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/DualLarson.h + */ +uint16_t mode_dual_larson_scanner(void){ + return larson_scanner(true); +} +static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Fade rate;!,!,!;!;;m12=0"; + + /* * Firing comets from one end. "Lighthouse" */ -uint16_t WS2812FX::mode_comet(void) { - uint16_t counter = now * ((SEGMENT.speed >>2) +1); - uint16_t index = counter * SEGLEN >> 16; +uint16_t mode_comet(void) { + if (SEGLEN == 1) return mode_static(); + uint16_t counter = strip.now * ((SEGMENT.speed >>2) +1); + uint16_t index = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) SEGENV.aux0 = index; - fade_out(SEGMENT.intensity); + SEGMENT.fade_out(SEGMENT.intensity); - setPixelColor( index, color_from_palette(index, true, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor( index, SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0)); if (index > SEGENV.aux0) { - for (uint16_t i = SEGENV.aux0; i < index ; i++) { - setPixelColor( i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); + for (int i = SEGENV.aux0; i < index ; i++) { + SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } else if (index < SEGENV.aux0 && index < 10) { - for (uint16_t i = 0; i < index ; i++) { - setPixelColor( i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); - } + for (int i = 0; i < index ; i++) { + SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); + } } SEGENV.aux0 = index++; return FRAMETIME; } +static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; /* * Fireworks function. */ -uint16_t WS2812FX::mode_fireworks() { - fade_out(0); +uint16_t mode_fireworks() { + if (SEGLEN == 1) return mode_static(); + const uint16_t width = SEGMENT.is2D() ? SEGMENT.virtualWidth() : SEGMENT.virtualLength(); + const uint16_t height = SEGMENT.virtualHeight(); + if (SEGENV.call == 0) { SEGENV.aux0 = UINT16_MAX; SEGENV.aux1 = UINT16_MAX; } - bool valid1 = (SEGENV.aux0 < SEGLEN); - bool valid2 = (SEGENV.aux1 < SEGLEN); - uint32_t sv1 = 0, sv2 = 0; - if (valid1) sv1 = getPixelColor(SEGENV.aux0); - if (valid2) sv2 = getPixelColor(SEGENV.aux1); - blur(255-SEGMENT.speed); - if (valid1) setPixelColor(SEGENV.aux0 , sv1); - if (valid2) setPixelColor(SEGENV.aux1, sv2); + SEGMENT.fade_out(128); - for(uint16_t i=0; i> 1)) == 0) { - uint16_t index = random(SEGLEN); - setPixelColor(index, color_from_palette(random8(), false, false, 0)); - SEGENV.aux1 = SEGENV.aux0; - SEGENV.aux0 = index; + bool valid1 = (SEGENV.aux0 < width*height); + bool valid2 = (SEGENV.aux1 < width*height); + uint8_t x = SEGENV.aux0%width, y = SEGENV.aux0/width; // 2D coordinates stored in upper and lower byte + uint32_t sv1 = 0, sv2 = 0; + if (valid1) sv1 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux0); // get spark color + if (valid2) sv2 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux1); + if (!SEGENV.step) SEGMENT.blur(16); + if (valid1) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv1); else SEGMENT.setPixelColor(SEGENV.aux0, sv1); } // restore spark color after blur + if (valid2) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv2); else SEGMENT.setPixelColor(SEGENV.aux1, sv2); } // restore old spark color after blur + + for (int i=0; i> 1)) == 0) { + uint16_t index = random16(width*height); + x = index % width; + y = index / width; + uint32_t col = SEGMENT.color_from_palette(random8(), false, false, 0); + if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, col); + else SEGMENT.setPixelColor(index, col); + SEGENV.aux1 = SEGENV.aux0; // old spark + SEGENV.aux0 = index; // remember where spark occurred } } return FRAMETIME; } +static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h -uint16_t WS2812FX::mode_rain() -{ +uint16_t mode_rain() { + if (SEGLEN == 1) return mode_static(); + const uint16_t width = SEGMENT.virtualWidth(); + const uint16_t height = SEGMENT.virtualHeight(); SEGENV.step += FRAMETIME; - if (SEGENV.step > SPEED_FORMULA_L) { - SEGENV.step = 0; - //shift all leds left - uint32_t ctemp = getPixelColor(0); - for(uint16_t i = 0; i < SEGLEN - 1; i++) { - setPixelColor(i, getPixelColor(i+1)); + if (SEGENV.call && SEGENV.step > SPEED_FORMULA_L) { + SEGENV.step = 1; + if (strip.isMatrix) { + //uint32_t ctemp[width]; + //for (int i = 0; i= width*height) SEGENV.aux0 = 0; // ignore + if (SEGENV.aux1 >= width*height) SEGENV.aux1 = 0; } return mode_fireworks(); } +static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; /* * Fire flicker function */ -uint16_t WS2812FX::mode_fire_flicker(void) { +uint16_t mode_fire_flicker(void) { uint32_t cycleTime = 40 + (255 - SEGMENT.speed); - uint32_t it = now / cycleTime; + uint32_t it = strip.now / cycleTime; if (SEGENV.step == it) return FRAMETIME; - + byte w = (SEGCOLOR(0) >> 24); byte r = (SEGCOLOR(0) >> 16); byte g = (SEGCOLOR(0) >> 8); byte b = (SEGCOLOR(0) ); byte lum = (SEGMENT.palette == 0) ? MAX(w, MAX(r, MAX(g, b))) : 255; lum /= (((256-SEGMENT.intensity)/16)+1); - for(uint16_t i = 0; i < SEGLEN; i++) { + for (int i = 0; i < SEGLEN; i++) { byte flicker = random8(lum); if (SEGMENT.palette == 0) { - setPixelColor(i, MAX(r - flicker, 0), MAX(g - flicker, 0), MAX(b - flicker, 0), MAX(w - flicker, 0)); + SEGMENT.setPixelColor(i, MAX(r - flicker, 0), MAX(g - flicker, 0), MAX(b - flicker, 0), MAX(w - flicker, 0)); } else { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, 255 - flicker)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, 255 - flicker)); } } SEGENV.step = it; return FRAMETIME; } +static const char _data_FX_MODE_FIRE_FLICKER[] PROGMEM = "Fire Flicker@!,!;!;!;01"; /* * Gradient run base function */ -uint16_t WS2812FX::gradient_base(bool loading) { - uint16_t counter = now * ((SEGMENT.speed >> 2) + 1); - uint16_t pp = counter * SEGLEN >> 16; +uint16_t gradient_base(bool loading) { + if (SEGLEN == 1) return mode_static(); + uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1); + uint16_t pp = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) pp = 0; - float val; //0.0 = sec 1.0 = pri - float brd = loading ? SEGMENT.intensity : SEGMENT.intensity/2; - if (brd <1.0) brd = 1.0; + int val; //0 = sec 1 = pri + int brd = 1 + loading ? SEGMENT.intensity/2 : SEGMENT.intensity/4; + //if (brd < 1) brd = 1; int p1 = pp-SEGLEN; int p2 = pp+SEGLEN; - for(uint16_t i = 0; i < SEGLEN; i++) - { - if (loading) - { - val = abs(((i>pp) ? p2:pp) -i); + for (int i = 0; i < SEGLEN; i++) { + if (loading) { + val = abs(((i>pp) ? p2:pp) - i); } else { - val = MIN(abs(pp-i),MIN(abs(p1-i),abs(p2-i))); + val = min(abs(pp-i),min(abs(p1-i),abs(p2-i))); } - val = (brd > val) ? val/brd * 255 : 255; - setPixelColor(i, color_blend(SEGCOLOR(0), color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), val)); + val = (brd > val) ? (val * 255) / brd : 255; + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), val)); } return FRAMETIME; @@ -1210,54 +1342,57 @@ uint16_t WS2812FX::gradient_base(bool loading) { /* * Gradient run */ -uint16_t WS2812FX::mode_gradient(void) { +uint16_t mode_gradient(void) { return gradient_base(false); } +static const char _data_FX_MODE_GRADIENT[] PROGMEM = "Gradient@!,Spread;!,!;!;;ix=16"; /* * Gradient run with hard transition */ -uint16_t WS2812FX::mode_loading(void) { +uint16_t mode_loading(void) { return gradient_base(true); } +static const char _data_FX_MODE_LOADING[] PROGMEM = "Loading@!,Fade;!,!;!;;ix=16"; -//American Police Light with all LEDs Red and Blue -uint16_t WS2812FX::police_base(uint32_t color1, uint32_t color2) -{ +//American Police Light with all LEDs Red and Blue +uint16_t police_base(uint32_t color1, uint32_t color2) { + if (SEGLEN == 1) return mode_static(); uint16_t delay = 1 + (FRAMETIME<<3) / SEGLEN; // longer segments should change faster - uint32_t it = now / map(SEGMENT.speed, 0, 255, delay<<4, delay); + uint32_t it = strip.now / map(SEGMENT.speed, 0, 255, delay<<4, delay); uint16_t offset = it % SEGLEN; - - uint16_t width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip + + uint16_t width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip if (!width) width = 1; - for (uint16_t i = 0; i < width; i++) { + for (int i = 0; i < width; i++) { uint16_t indexR = (offset + i) % SEGLEN; uint16_t indexB = (offset + i + (SEGLEN>>1)) % SEGLEN; - setPixelColor(indexR, color1); - setPixelColor(indexB, color2); + SEGMENT.setPixelColor(indexR, color1); + SEGMENT.setPixelColor(indexB, color2); } return FRAMETIME; } -//Police Lights Red and Blue -uint16_t WS2812FX::mode_police() -{ - fill(SEGCOLOR(1)); - return police_base(RED, BLUE); -} +//Police Lights Red and Blue +//uint16_t mode_police() +//{ +// SEGMENT.fill(SEGCOLOR(1)); +// return police_base(RED, BLUE); +//} +//static const char _data_FX_MODE_POLICE[] PROGMEM = "Police@!,Width;,Bg;0"; -//Police Lights with custom colors -uint16_t WS2812FX::mode_two_dots() +//Police Lights with custom colors +uint16_t mode_two_dots() { - fill(SEGCOLOR(2)); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2)); uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? SEGCOLOR(0) : SEGCOLOR(1); - return police_base(SEGCOLOR(0), color2); } +static const char _data_FX_MODE_TWO_DOTS[] PROGMEM = "Two Dots@!,Dot size,,,,,Overlay;1,2,Bg;!"; /* @@ -1267,151 +1402,153 @@ uint16_t WS2812FX::mode_two_dots() typedef struct Flasher { uint16_t stateStart; uint8_t stateDur; - bool stateOn; + bool stateOn; } flasher; #define FLASHERS_PER_ZONE 6 #define MAX_SHIMMER 92 -uint16_t WS2812FX::mode_fairy() { - //set every pixel to a 'random' color from palette (using seed so it doesn't change between frames) - uint16_t PRNG16 = 5100 + _segment_index; - for (uint16_t i = 0; i < SEGLEN; i++) { - PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number - setPixelColor(i, color_from_palette(PRNG16 >> 8, false, false, 0)); - } - - //amount of flasher pixels depending on intensity (0: none, 255: every LED) - if (SEGMENT.intensity == 0) return FRAMETIME; - uint8_t flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 - uint16_t numFlashers = (SEGLEN / flasherDistance) +1; - - uint16_t dataSize = sizeof(flasher) * numFlashers; +uint16_t mode_fairy() { + //set every pixel to a 'random' color from palette (using seed so it doesn't change between frames) + uint16_t PRNG16 = 5100 + strip.getCurrSegmentId(); + for (int i = 0; i < SEGLEN; i++) { + PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0)); + } + + //amount of flasher pixels depending on intensity (0: none, 255: every LED) + if (SEGMENT.intensity == 0) return FRAMETIME; + uint8_t flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 + uint16_t numFlashers = (SEGLEN / flasherDistance) +1; + + uint16_t dataSize = sizeof(flasher) * numFlashers; if (!SEGENV.allocateData(dataSize)) return FRAMETIME; //allocation failed - Flasher* flashers = reinterpret_cast(SEGENV.data); - uint16_t now16 = now & 0xFFFF; - - //Up to 11 flashers in one brightness zone, afterwards a new zone for every 6 flashers - uint16_t zones = numFlashers/FLASHERS_PER_ZONE; - if (!zones) zones = 1; - uint8_t flashersInZone = numFlashers/zones; - uint8_t flasherBri[FLASHERS_PER_ZONE*2 -1]; - - for (uint16_t z = 0; z < zones; z++) { - uint16_t flasherBriSum = 0; - uint16_t firstFlasher = z*flashersInZone; - if (z == zones-1) flashersInZone = numFlashers-(flashersInZone*(zones-1)); - - for (uint16_t f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - uint16_t stateTime = now16 - flashers[f].stateStart; - //random on/off time reached, switch state - if (stateTime > flashers[f].stateDur * 10) { - flashers[f].stateOn = !flashers[f].stateOn; - if (flashers[f].stateOn) { - flashers[f].stateDur = 12 + random8(12 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms - } else { - flashers[f].stateDur = 20 + random8(6 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms - } - //flashers[f].stateDur = 51 + random8(2 + ((255 - SEGMENT.speed) >> 1)); - flashers[f].stateStart = now16; - if (stateTime < 255) { - flashers[f].stateStart -= 255 -stateTime; //start early to get correct bri - flashers[f].stateDur += 26 - stateTime/10; - stateTime = 255 - stateTime; - } else { - stateTime = 0; - } - } - if (stateTime > 255) stateTime = 255; //for flasher brightness calculation, fades in first 255 ms of state - //flasherBri[f - firstFlasher] = (flashers[f].stateOn) ? 255-gamma8((510 - stateTime) >> 1) : gamma8((510 - stateTime) >> 1); - flasherBri[f - firstFlasher] = (flashers[f].stateOn) ? stateTime : 255 - (stateTime >> 0); - flasherBriSum += flasherBri[f - firstFlasher]; - } - //dim factor, to create "shimmer" as other pixels get less voltage if a lot of flashers are on - uint8_t avgFlasherBri = flasherBriSum / flashersInZone; - uint8_t globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 8); //183-255, suitable for 1/5th of LEDs flashers - - for (uint16_t f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - uint8_t bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; - PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number - uint16_t flasherPos = f*flasherDistance; - setPixelColor(flasherPos, color_blend(SEGCOLOR(1), color_from_palette(PRNG16 >> 8, false, false, 0), bri)); - for (uint16_t i = flasherPos+1; i < flasherPos+flasherDistance && i < SEGLEN; i++) { - PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number - setPixelColor(i, color_from_palette(PRNG16 >> 8, false, false, 0, globalPeakBri)); - } - } - } - return FRAMETIME; + Flasher* flashers = reinterpret_cast(SEGENV.data); + uint16_t now16 = strip.now & 0xFFFF; + + //Up to 11 flashers in one brightness zone, afterwards a new zone for every 6 flashers + uint16_t zones = numFlashers/FLASHERS_PER_ZONE; + if (!zones) zones = 1; + uint8_t flashersInZone = numFlashers/zones; + uint8_t flasherBri[FLASHERS_PER_ZONE*2 -1]; + + for (int z = 0; z < zones; z++) { + uint16_t flasherBriSum = 0; + uint16_t firstFlasher = z*flashersInZone; + if (z == zones-1) flashersInZone = numFlashers-(flashersInZone*(zones-1)); + + for (int f = firstFlasher; f < firstFlasher + flashersInZone; f++) { + uint16_t stateTime = now16 - flashers[f].stateStart; + //random on/off time reached, switch state + if (stateTime > flashers[f].stateDur * 10) { + flashers[f].stateOn = !flashers[f].stateOn; + if (flashers[f].stateOn) { + flashers[f].stateDur = 12 + random8(12 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms + } else { + flashers[f].stateDur = 20 + random8(6 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms + } + //flashers[f].stateDur = 51 + random8(2 + ((255 - SEGMENT.speed) >> 1)); + flashers[f].stateStart = now16; + if (stateTime < 255) { + flashers[f].stateStart -= 255 -stateTime; //start early to get correct bri + flashers[f].stateDur += 26 - stateTime/10; + stateTime = 255 - stateTime; + } else { + stateTime = 0; + } + } + if (stateTime > 255) stateTime = 255; //for flasher brightness calculation, fades in first 255 ms of state + //flasherBri[f - firstFlasher] = (flashers[f].stateOn) ? 255-SEGMENT.gamma8((510 - stateTime) >> 1) : SEGMENT.gamma8((510 - stateTime) >> 1); + flasherBri[f - firstFlasher] = (flashers[f].stateOn) ? stateTime : 255 - (stateTime >> 0); + flasherBriSum += flasherBri[f - firstFlasher]; + } + //dim factor, to create "shimmer" as other pixels get less voltage if a lot of flashers are on + uint8_t avgFlasherBri = flasherBriSum / flashersInZone; + uint8_t globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 8); //183-255, suitable for 1/5th of LEDs flashers + + for (int f = firstFlasher; f < firstFlasher + flashersInZone; f++) { + uint8_t bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; + PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number + uint16_t flasherPos = f*flasherDistance; + SEGMENT.setPixelColor(flasherPos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), bri)); + for (int i = flasherPos+1; i < flasherPos+flasherDistance && i < SEGLEN; i++) { + PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0, globalPeakBri)); + } + } + } + return FRAMETIME; } +static const char _data_FX_MODE_FAIRY[] PROGMEM = "Fairy@!,# of flashers;!,!;!"; /* - * Fairytwinkle. Like Colortwinkle, but starting from all lit and not relying on getPixelColor + * Fairytwinkle. Like Colortwinkle, but starting from all lit and not relying on strip.getPixelColor * Warning: Uses 4 bytes of segment data per pixel */ -uint16_t WS2812FX::mode_fairytwinkle() { - uint16_t dataSize = sizeof(flasher) * SEGLEN; +uint16_t mode_fairytwinkle() { + uint16_t dataSize = sizeof(flasher) * SEGLEN; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - Flasher* flashers = reinterpret_cast(SEGENV.data); - uint16_t now16 = now & 0xFFFF; - uint16_t PRNG16 = 5100 + _segment_index; - - uint16_t riseFallTime = 400 + (255-SEGMENT.speed)*3; - uint16_t maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1); - - for (uint16_t f = 0; f < SEGLEN; f++) { - uint16_t stateTime = now16 - flashers[f].stateStart; - //random on/off time reached, switch state - if (stateTime > flashers[f].stateDur * 100) { - flashers[f].stateOn = !flashers[f].stateOn; - bool init = !flashers[f].stateDur; - if (flashers[f].stateOn) { - flashers[f].stateDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + random8(12 + ((255 - SEGMENT.intensity) >> 1)) +1; - } else { - flashers[f].stateDur = riseFallTime/100 + random8(3 + ((255 - SEGMENT.speed) >> 6)) +1; - } - flashers[f].stateStart = now16; - stateTime = 0; - if (init) { - flashers[f].stateStart -= riseFallTime; //start lit - flashers[f].stateDur = riseFallTime/100 + random8(12 + ((255 - SEGMENT.intensity) >> 1)) +5; //fire up a little quicker - stateTime = riseFallTime; - } - } - if (flashers[f].stateOn && flashers[f].stateDur > maxDur) flashers[f].stateDur = maxDur; //react more quickly on intensity change - if (stateTime > riseFallTime) stateTime = riseFallTime; //for flasher brightness calculation, fades in first 255 ms of state - uint8_t fadeprog = 255 - ((stateTime * 255) / riseFallTime); - uint8_t flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); - uint16_t lastR = PRNG16; - uint16_t diff = 0; - while (diff < 0x4000) { //make sure colors of two adjacent LEDs differ enough - PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number - diff = (PRNG16 > lastR) ? PRNG16 - lastR : lastR - PRNG16; - } - setPixelColor(f, color_blend(SEGCOLOR(1), color_from_palette(PRNG16 >> 8, false, false, 0), flasherBri)); - } + Flasher* flashers = reinterpret_cast(SEGENV.data); + uint16_t now16 = strip.now & 0xFFFF; + uint16_t PRNG16 = 5100 + strip.getCurrSegmentId(); + + uint16_t riseFallTime = 400 + (255-SEGMENT.speed)*3; + uint16_t maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1); + + for (int f = 0; f < SEGLEN; f++) { + uint16_t stateTime = now16 - flashers[f].stateStart; + //random on/off time reached, switch state + if (stateTime > flashers[f].stateDur * 100) { + flashers[f].stateOn = !flashers[f].stateOn; + bool init = !flashers[f].stateDur; + if (flashers[f].stateOn) { + flashers[f].stateDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + random8(12 + ((255 - SEGMENT.intensity) >> 1)) +1; + } else { + flashers[f].stateDur = riseFallTime/100 + random8(3 + ((255 - SEGMENT.speed) >> 6)) +1; + } + flashers[f].stateStart = now16; + stateTime = 0; + if (init) { + flashers[f].stateStart -= riseFallTime; //start lit + flashers[f].stateDur = riseFallTime/100 + random8(12 + ((255 - SEGMENT.intensity) >> 1)) +5; //fire up a little quicker + stateTime = riseFallTime; + } + } + if (flashers[f].stateOn && flashers[f].stateDur > maxDur) flashers[f].stateDur = maxDur; //react more quickly on intensity change + if (stateTime > riseFallTime) stateTime = riseFallTime; //for flasher brightness calculation, fades in first 255 ms of state + uint8_t fadeprog = 255 - ((stateTime * 255) / riseFallTime); + uint8_t flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); + uint16_t lastR = PRNG16; + uint16_t diff = 0; + while (diff < 0x4000) { //make sure colors of two adjacent LEDs differ enough + PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number + diff = (PRNG16 > lastR) ? PRNG16 - lastR : lastR - PRNG16; + } + SEGMENT.setPixelColor(f, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), flasherBri)); + } return FRAMETIME; } +static const char _data_FX_MODE_FAIRYTWINKLE[] PROGMEM = "Fairytwinkle@!,!;!,!;!;;m12=0"; //pixels /* * Tricolor chase function */ -uint16_t WS2812FX::tricolor_chase(uint32_t color1, uint32_t color2) { +uint16_t tricolor_chase(uint32_t color1, uint32_t color2) { uint32_t cycleTime = 50 + ((255 - SEGMENT.speed)<<1); - uint32_t it = now / cycleTime; // iterator + uint32_t it = strip.now / cycleTime; // iterator uint8_t width = (1 + (SEGMENT.intensity>>4)); // value of 1-16 for each colour uint8_t index = it % (width*3); - - for (uint16_t i = 0; i < SEGLEN; i++, index++) { + + for (int i = 0; i < SEGLEN; i++, index++) { if (index > (width*3)-1) index = 0; uint32_t color = color1; - if (index > (width<<1)-1) color = color_from_palette(i, true, PALETTE_SOLID_WRAP, 1); + if (index > (width<<1)-1) color = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1); else if (index > width-1) color = color2; - setPixelColor(SEGLEN - i -1, color); + SEGMENT.setPixelColor(SEGLEN - i -1, color); } return FRAMETIME; } @@ -1420,30 +1557,31 @@ uint16_t WS2812FX::tricolor_chase(uint32_t color1, uint32_t color2) { /* * Tricolor chase mode */ -uint16_t WS2812FX::mode_tricolor_chase(void) { +uint16_t mode_tricolor_chase(void) { return tricolor_chase(SEGCOLOR(2), SEGCOLOR(0)); } +static const char _data_FX_MODE_TRICOLOR_CHASE[] PROGMEM = "Chase 3@!,Size;1,2,3;!"; /* * ICU mode */ -uint16_t WS2812FX::mode_icu(void) { +uint16_t mode_icu(void) { uint16_t dest = SEGENV.step & 0xFFFF; uint8_t space = (SEGMENT.intensity >> 3) +2; - fill(SEGCOLOR(1)); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); byte pindex = map(dest, 0, SEGLEN-SEGLEN/space, 0, 255); - uint32_t col = color_from_palette(pindex, false, false, 0); + uint32_t col = SEGMENT.color_from_palette(pindex, false, false, 0); - setPixelColor(dest, col); - setPixelColor(dest + SEGLEN/space, col); + SEGMENT.setPixelColor(dest, col); + SEGMENT.setPixelColor(dest + SEGLEN/space, col); if(SEGENV.aux0 == dest) { // pause between eye movements if(random8(6) == 0) { // blink once in a while - setPixelColor(dest, SEGCOLOR(1)); - setPixelColor(dest + SEGLEN/space, SEGCOLOR(1)); + SEGMENT.setPixelColor(dest, SEGCOLOR(1)); + SEGMENT.setPixelColor(dest + SEGLEN/space, SEGCOLOR(1)); return 200; } SEGENV.aux0 = random16(SEGLEN-SEGLEN/space); @@ -1458,51 +1596,52 @@ uint16_t WS2812FX::mode_icu(void) { dest--; } - setPixelColor(dest, col); - setPixelColor(dest + SEGLEN/space, col); + SEGMENT.setPixelColor(dest, col); + SEGMENT.setPixelColor(dest + SEGLEN/space, col); return SPEED_FORMULA_L; } +static const char _data_FX_MODE_ICU[] PROGMEM = "ICU@!,!,,,,,Overlay;!,!;!"; /* * Custom mode by Aircoookie. Color Wipe, but with 3 colors */ -uint16_t WS2812FX::mode_tricolor_wipe(void) -{ +uint16_t mode_tricolor_wipe(void) { uint32_t cycleTime = 1000 + (255 - SEGMENT.speed)*200; - uint32_t perc = now % cycleTime; + uint32_t perc = strip.now % cycleTime; uint16_t prog = (perc * 65535) / cycleTime; uint16_t ledIndex = (prog * SEGLEN * 3) >> 16; uint16_t ledOffset = ledIndex; - for (uint16_t i = 0; i < SEGLEN; i++) + for (int i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 2)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2)); } - + if(ledIndex < SEGLEN) { //wipe from 0 to 1 - for (uint16_t i = 0; i < SEGLEN; i++) + for (int i = 0; i < SEGLEN; i++) { - setPixelColor(i, (i > ledOffset)? SEGCOLOR(0) : SEGCOLOR(1)); + SEGMENT.setPixelColor(i, (i > ledOffset)? SEGCOLOR(0) : SEGCOLOR(1)); } } else if (ledIndex < SEGLEN*2) { //wipe from 1 to 2 ledOffset = ledIndex - SEGLEN; - for (uint16_t i = ledOffset +1; i < SEGLEN; i++) + for (int i = ledOffset +1; i < SEGLEN; i++) { - setPixelColor(i, SEGCOLOR(1)); + SEGMENT.setPixelColor(i, SEGCOLOR(1)); } } else //wipe from 2 to 0 { ledOffset = ledIndex - SEGLEN*2; - for (uint16_t i = 0; i <= ledOffset; i++) + for (int i = 0; i <= ledOffset; i++) { - setPixelColor(i, SEGCOLOR(0)); + SEGMENT.setPixelColor(i, SEGCOLOR(0)); } } return FRAMETIME; } +static const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = "Tri Wipe@!;1,2,3;!"; /* @@ -1510,9 +1649,8 @@ uint16_t WS2812FX::mode_tricolor_wipe(void) * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/TriFade.h * Modified by Aircoookie */ -uint16_t WS2812FX::mode_tricolor_fade(void) -{ - uint16_t counter = now * ((SEGMENT.speed >> 3) +1); +uint16_t mode_tricolor_fade(void) { + uint16_t counter = strip.now * ((SEGMENT.speed >> 3) +1); uint32_t prog = (counter * 768) >> 16; uint32_t color1 = 0, color2 = 0; @@ -1533,46 +1671,46 @@ uint16_t WS2812FX::mode_tricolor_fade(void) } byte stp = prog; // % 256 - for(uint16_t i = 0; i < SEGLEN; i++) { + for (int i = 0; i < SEGLEN; i++) { uint32_t color; if (stage == 2) { - color = color_blend(color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), color2, stp); + color = color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), color2, stp); } else if (stage == 1) { - color = color_blend(color1, color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), stp); + color = color_blend(color1, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), stp); } else { color = color_blend(color1, color2, stp); } - setPixelColor(i, color); + SEGMENT.setPixelColor(i, color); } return FRAMETIME; } +static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; /* * Creates random comets * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h */ -uint16_t WS2812FX::mode_multi_comet(void) -{ +uint16_t mode_multi_comet(void) { uint32_t cycleTime = 10 + (uint32_t)(255 - SEGMENT.speed); - uint32_t it = now / cycleTime; + uint32_t it = strip.now / cycleTime; if (SEGENV.step == it) return FRAMETIME; if (!SEGENV.allocateData(sizeof(uint16_t) * 8)) return mode_static(); //allocation failed - - fade_out(SEGMENT.intensity); - + + SEGMENT.fade_out(SEGMENT.intensity); + uint16_t* comets = reinterpret_cast(SEGENV.data); - for(uint8_t i=0; i < 8; i++) { + for (int i=0; i < 8; i++) { if(comets[i] < SEGLEN) { uint16_t index = comets[i]; if (SEGCOLOR(2) != 0) { - setPixelColor(index, i % 2 ? color_from_palette(index, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(2)); + SEGMENT.setPixelColor(index, i % 2 ? SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(2)); } else { - setPixelColor(index, color_from_palette(index, true, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(index, SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0)); } comets[i]++; } else { @@ -1585,39 +1723,30 @@ uint16_t WS2812FX::mode_multi_comet(void) SEGENV.step = it; return FRAMETIME; } - - -/* - * Creates two Larson scanners moving in opposite directions - * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/DualLarson.h - */ -uint16_t WS2812FX::mode_dual_larson_scanner(void){ - return larson_scanner(true); -} +static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet"; /* * Running random pixels ("Stream 2") * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/RandomChase.h */ -uint16_t WS2812FX::mode_random_chase(void) -{ +uint16_t mode_random_chase(void) { if (SEGENV.call == 0) { SEGENV.step = RGBW32(random8(), random8(), random8(), 0); SEGENV.aux0 = random16(); } uint16_t prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); - uint32_t it = now / cycleTime; + uint32_t it = strip.now / cycleTime; uint32_t color = SEGENV.step; random16_set_seed(SEGENV.aux0); - for(uint16_t i = SEGLEN -1; i > 0; i--) { + for (int i = SEGLEN -1; i > 0; i--) { uint8_t r = random8(6) != 0 ? (color >> 16 & 0xFF) : random8(); uint8_t g = random8(6) != 0 ? (color >> 8 & 0xFF) : random8(); uint8_t b = random8(6) != 0 ? (color & 0xFF) : random8(); color = RGBW32(r, g, b, 0); - setPixelColor(i, r, g, b); + SEGMENT.setPixelColor(i, r, g, b); if (i == SEGLEN -1 && SEGENV.aux1 != (it & 0xFFFF)) { //new first color in next frame SEGENV.step = color; SEGENV.aux0 = random16_get_seed(); @@ -1629,6 +1758,8 @@ uint16_t WS2812FX::mode_random_chase(void) random16_set_seed(prevSeed); // restore original seed so other effects can use "random" PRNG return FRAMETIME; } +static const char _data_FX_MODE_RANDOM_CHASE[] PROGMEM = "Stream 2@!;;"; + //7 bytes typedef struct Oscillator { @@ -1641,13 +1772,12 @@ typedef struct Oscillator { /* / Oscillating bars of color, updated with standard framerate */ -uint16_t WS2812FX::mode_oscillate(void) -{ +uint16_t mode_oscillate(void) { uint8_t numOscillators = 3; uint16_t dataSize = sizeof(oscillator) * numOscillators; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - + Oscillator* oscillators = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) @@ -1658,9 +1788,9 @@ uint16_t WS2812FX::mode_oscillate(void) } uint32_t cycleTime = 20 + (2 * (uint32_t)(255 - SEGMENT.speed)); - uint32_t it = now / cycleTime; + uint32_t it = strip.now / cycleTime; - for(uint8_t i = 0; i < numOscillators; i++) { + for (int i = 0; i < numOscillators; i++) { // if the counter has increased, move the oscillator by the random step if (it != SEGENV.step) oscillators[i].pos += oscillators[i].dir * oscillators[i].speed; oscillators[i].size = SEGLEN/(3+SEGMENT.intensity/8); @@ -1677,23 +1807,25 @@ uint16_t WS2812FX::mode_oscillate(void) } } - for(uint16_t i=0; i < SEGLEN; i++) { + for (int i = 0; i < SEGLEN; i++) { uint32_t color = BLACK; - for(uint8_t j=0; j < numOscillators; j++) { + for (int j = 0; j < numOscillators; j++) { if(i >= oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), 128); } } - setPixelColor(i, color); + SEGMENT.setPixelColor(i, color); } - + SEGENV.step = it; return FRAMETIME; } +static const char _data_FX_MODE_OSCILLATE[] PROGMEM = "Oscillate"; -uint16_t WS2812FX::mode_lightning(void) -{ +//TODO +uint16_t mode_lightning(void) { + if (SEGLEN == 1) return mode_static(); uint16_t ledstart = random16(SEGLEN); // Determine starting location of flash uint16_t ledlen = 1 + random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) uint8_t bri = 255/random8(1, 3); @@ -1707,12 +1839,12 @@ uint16_t WS2812FX::mode_lightning(void) SEGENV.aux0 = 200; //200ms delay after leader } - fill(SEGCOLOR(1)); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); if (SEGENV.aux1 > 3 && !(SEGENV.aux1 & 0x01)) { //flash on even number >2 for (int i = ledstart; i < ledstart + ledlen; i++) { - setPixelColor(i,color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, bri)); + SEGMENT.setPixelColor(i,SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, bri)); } SEGENV.aux1--; @@ -1732,13 +1864,13 @@ uint16_t WS2812FX::mode_lightning(void) } return FRAMETIME; } +static const char _data_FX_MODE_LIGHTNING[] PROGMEM = "Lightning@!,!,,,,,Overlay;!,!;!"; // Pride2015 // Animated, ever-changing rainbows. // by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5 -uint16_t WS2812FX::mode_pride_2015(void) -{ +uint16_t mode_pride_2015(void) { uint16_t duration = 10 + SEGMENT.speed; uint16_t sPseudotime = SEGENV.step; uint16_t sHue16 = SEGENV.aux0; @@ -1754,9 +1886,8 @@ uint16_t WS2812FX::mode_pride_2015(void) sPseudotime += duration * msmultiplier; sHue16 += duration * beatsin88( 400, 5,9); uint16_t brightnesstheta16 = sPseudotime; - CRGB fastled_col; - for (uint16_t i = 0 ; i < SEGLEN; i++) { + for (int i = 0 ; i < SEGLEN; i++) { hue16 += hueinc16; uint8_t hue8 = hue16 >> 8; @@ -1767,63 +1898,62 @@ uint16_t WS2812FX::mode_pride_2015(void) uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; bri8 += (255 - brightdepth); - CRGB newcolor = CHSV( hue8, sat8, bri8); - fastled_col = col_to_crgb(getPixelColor(i)); - - nblend(fastled_col, newcolor, 64); - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + CRGB newcolor = CHSV(hue8, sat8, bri8); + SEGMENT.blendPixelColor(i, newcolor, 64); } SEGENV.step = sPseudotime; SEGENV.aux0 = sHue16; + return FRAMETIME; } +static const char _data_FX_MODE_PRIDE_2015[] PROGMEM = "Pride 2015@!;;"; //eight colored dots, weaving in and out of sync with each other -uint16_t WS2812FX::mode_juggle(void){ - fade_out(SEGMENT.intensity); +uint16_t mode_juggle(void) { + if (SEGLEN == 1) return mode_static(); + + SEGMENT.fadeToBlackBy(192 - (3*SEGMENT.intensity/4)); CRGB fastled_col; byte dothue = 0; - for ( byte i = 0; i < 8; i++) { - uint16_t index = 0 + beatsin88((128 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); - fastled_col = col_to_crgb(getPixelColor(index)); - fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):ColorFromPalette(currentPalette, dothue, 255); - setPixelColor(index, fastled_col.red, fastled_col.green, fastled_col.blue); + for (int i = 0; i < 8; i++) { + uint16_t index = 0 + beatsin88((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); + fastled_col = CRGB(SEGMENT.getPixelColor(index)); + fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):ColorFromPalette(SEGPALETTE, dothue, 255); + SEGMENT.setPixelColor(index, fastled_col); dothue += 32; } return FRAMETIME; } +static const char _data_FX_MODE_JUGGLE[] PROGMEM = "Juggle@!,Trail;;!;;sx=64,ix=128"; -uint16_t WS2812FX::mode_palette() -{ +uint16_t mode_palette() { uint16_t counter = 0; - if (SEGMENT.speed != 0) + if (SEGMENT.speed != 0) { - counter = (now * ((SEGMENT.speed >> 3) +1)) & 0xFFFF; + counter = (strip.now * ((SEGMENT.speed >> 3) +1)) & 0xFFFF; counter = counter >> 8; } - - bool noWrap = (paletteBlend == 2 || (paletteBlend == 0 && SEGMENT.speed == 0)); - for (uint16_t i = 0; i < SEGLEN; i++) + + for (int i = 0; i < SEGLEN; i++) { uint8_t colorIndex = (i * 255 / SEGLEN) - counter; - - if (noWrap) colorIndex = map(colorIndex, 0, 255, 0, 240); //cut off blend at palette "end" - - setPixelColor(i, color_from_palette(colorIndex, false, true, 255)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, PALETTE_MOVING_WRAP, 255)); } + return FRAMETIME; } +static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Cycle speed;;!;;c3=0,o2=0"; // 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 -//// +//// // This basic one-dimensional 'fire' simulation works roughly as follows: // There's a underlying array of 'heat' cells, that model the temperature -// at each point along the line. Every cycle through the simulation, +// at each point along the line. Every cycle through the simulation, // four steps are performed: // 1) All cells cool down a little bit, losing heat to the air // 2) The heat from each cell drifts 'up' and diffuses a little @@ -1833,8 +1963,8 @@ uint16_t WS2812FX::mode_palette() // // Temperature is in arbitrary units from 0 (cold black) to 255 (white hot). // -// This simulation scales it self a bit depending on NUM_LEDS; it should look -// "OK" on anywhere from 20 to 100 LEDs without too much tweaking. +// This simulation scales it self a bit depending on SEGLEN; it should look +// "OK" on anywhere from 20 to 100 LEDs without too much tweaking. // // I recommend running this simulation at anywhere from 30-100 frames per second, // meaning an interframe delay of about 10-35 milliseconds. @@ -1845,53 +1975,65 @@ uint16_t WS2812FX::mode_palette() // There are two main parameters you can play with to control the look and // feel of your fire: COOLING (used in step 1 above) (Speed = COOLING), and SPARKING (used // in step 3 above) (Effect Intensity = Sparking). +uint16_t mode_fire_2012() { + if (SEGLEN == 1) return mode_static(); + const uint16_t strips = SEGMENT.nrOfVStrips(); + if (!SEGENV.allocateData(strips * SEGLEN)) return mode_static(); //allocation failed + byte* heat = SEGENV.data; + const uint32_t it = strip.now >> 5; //div 32 -uint16_t WS2812FX::mode_fire_2012() -{ - uint32_t it = now >> 5; //div 32 + struct virtualStrip { + static void runStrip(uint16_t stripNr, byte* heat, uint32_t it) { - if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed - - byte* heat = SEGENV.data; + const uint8_t ignition = max(3,SEGLEN/10); // ignition area: 10% of segment length or minimum 3 pixels - if (it != SEGENV.step) - { - uint8_t ignition = max(7,SEGLEN/10); // ignition area: 10% of segment length or minimum 7 pixels - - // Step 1. Cool down every cell a little - for (uint16_t i = 0; i < SEGLEN; i++) { - uint8_t temp = qsub8(heat[i], random8(0, (((20 + SEGMENT.speed /3) * 10) / SEGLEN) + 2)); - heat[i] = (temp==0 && i 1; k--) { - heat[k] = (heat[k - 1] + (heat[k - 2]<<1) ) / 3; // heat[k-2] multiplied by 2 - } - - // Step 3. Randomly ignite new 'sparks' of heat near the bottom - if (random8() <= SEGMENT.intensity) { - uint8_t y = random8(ignition); - if (y < SEGLEN) heat[y] = qadd8(heat[y], random8(160,255)); + // Step 1. Cool down every cell a little + for (int i = 0; i < SEGLEN; i++) { + uint8_t cool = (it != SEGENV.step) ? random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : random(4); + uint8_t minTemp = (i 1; k--) { + heat[k] = (heat[k - 1] + (heat[k - 2]<<1) ) / 3; // heat[k-2] multiplied by 2 + } + + // Step 3. Randomly ignite new 'sparks' of heat near the bottom + if (random8() <= SEGMENT.intensity) { + uint8_t y = random8(ignition); + uint8_t boost = (17+SEGMENT.custom3) * (ignition - y/2) / ignition; // integer math! + heat[y] = qadd8(heat[y], random8(96+2*boost,207+boost)); + } + } + + // Step 4. Map from heat cells to LED colors + for (int j = 0; j < SEGLEN; j++) { + SEGMENT.setPixelColor(indexToVStrip(j, stripNr), ColorFromPalette(SEGPALETTE, MIN(heat[j],240), 255, NOBLEND)); + } } + }; + + for (int stripNr=0; stripNr> 8; uint16_t h16_128 = hue16 >> 7; @@ -1920,160 +2060,150 @@ uint16_t WS2812FX::mode_colorwaves() } brightnesstheta16 += brightnessthetainc16; - uint16_t b16 = sin16( brightnesstheta16 ) + 32768; + uint16_t b16 = sin16(brightnesstheta16) + 32768; uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; bri8 += (255 - brightdepth); - CRGB newcolor = ColorFromPalette(currentPalette, hue8, bri8); - fastled_col = col_to_crgb(getPixelColor(i)); - - nblend(fastled_col, newcolor, 128); - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.blendPixelColor(i, SEGMENT.color_from_palette(hue8, false, PALETTE_SOLID_WRAP, 0, bri8), 128); // 50/50 mix } SEGENV.step = sPseudotime; SEGENV.aux0 = sHue16; + return FRAMETIME; } +static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!"; // colored stripes pulsing at a defined Beats-Per-Minute (BPM) -uint16_t WS2812FX::mode_bpm() -{ - CRGB fastled_col; - uint32_t stp = (now / 20) & 0xFF; +uint16_t mode_bpm() { + uint32_t stp = (strip.now / 20) & 0xFF; uint8_t beat = beatsin8(SEGMENT.speed, 64, 255); - for (uint16_t i = 0; i < SEGLEN; i++) { - fastled_col = ColorFromPalette(currentPalette, stp + (i * 2), beat - stp + (i * 10)); - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(stp + (i * 2), false, PALETTE_SOLID_WRAP, 0, beat - stp + (i * 10))); } + return FRAMETIME; } +static const char _data_FX_MODE_BPM[] PROGMEM = "Bpm@!;!;!;;sx=64"; -uint16_t WS2812FX::mode_fillnoise8() -{ +uint16_t mode_fillnoise8() { if (SEGENV.call == 0) SEGENV.step = random16(12345); - CRGB fastled_col; - for (uint16_t i = 0; i < SEGLEN; i++) { + //CRGB fastled_col; + for (int i = 0; i < SEGLEN; i++) { uint8_t index = inoise8(i * SEGLEN, SEGENV.step + i * SEGLEN); - fastled_col = ColorFromPalette(currentPalette, index, 255, LINEARBLEND); - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + //fastled_col = ColorFromPalette(SEGPALETTE, index, 255, LINEARBLEND); + //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } SEGENV.step += beatsin8(SEGMENT.speed, 1, 6); //10,1,4 return FRAMETIME; } +static const char _data_FX_MODE_FILLNOISE8[] PROGMEM = "Fill Noise@!;!;!"; -uint16_t WS2812FX::mode_noise16_1() -{ - uint16_t scale = 320; // the "zoom factor" for the noise - CRGB fastled_col; - SEGENV.step += (1 + SEGMENT.speed/16); - - for (uint16_t i = 0; i < SEGLEN; i++) { - - uint16_t shift_x = beatsin8(11); // the x position of the noise field swings @ 17 bpm - uint16_t shift_y = SEGENV.step/42; // the y position becomes slowly incremented +uint16_t mode_noise16_1() { + uint16_t scale = 320; // the "zoom factor" for the noise + //CRGB fastled_col; + SEGENV.step += (1 + SEGMENT.speed/16); + for (int i = 0; i < SEGLEN; i++) { + uint16_t shift_x = beatsin8(11); // the x position of the noise field swings @ 17 bpm + uint16_t shift_y = SEGENV.step/42; // the y position becomes slowly incremented uint16_t real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm uint16_t real_y = (i + shift_y) * scale; // the y position becomes slowly incremented - uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented - - uint8_t noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down - - uint8_t index = sin8(noise * 3); // map LED color based on noise data + uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented + uint8_t noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + uint8_t index = sin8(noise * 3); // map LED color based on noise data - fastled_col = ColorFromPalette(currentPalette, index, 255, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + //fastled_col = ColorFromPalette(SEGPALETTE, index, 255, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. + //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; } +static const char _data_FX_MODE_NOISE16_1[] PROGMEM = "Noise 1@!;!;!"; -uint16_t WS2812FX::mode_noise16_2() -{ - uint16_t scale = 1000; // the "zoom factor" for the noise - CRGB fastled_col; +uint16_t mode_noise16_2() { + uint16_t scale = 1000; // the "zoom factor" for the noise + //CRGB fastled_col; SEGENV.step += (1 + (SEGMENT.speed >> 1)); - for (uint16_t i = 0; i < SEGLEN; i++) { - - uint16_t shift_x = SEGENV.step >> 6; // x as a function of time - - uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field - - uint8_t noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down - - uint8_t index = sin8(noise * 3); // map led color based on noise data + for (int i = 0; i < SEGLEN; i++) { + uint16_t shift_x = SEGENV.step >> 6; // x as a function of time + uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field + uint8_t noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down + uint8_t index = sin8(noise * 3); // map led color based on noise data - fastled_col = ColorFromPalette(currentPalette, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + //fastled_col = ColorFromPalette(SEGPALETTE, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. + //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); } return FRAMETIME; } +static const char _data_FX_MODE_NOISE16_2[] PROGMEM = "Noise 2@!;!;!"; -uint16_t WS2812FX::mode_noise16_3() -{ +uint16_t mode_noise16_3() { uint16_t scale = 800; // the "zoom factor" for the noise - CRGB fastled_col; + //CRGB fastled_col; SEGENV.step += (1 + SEGMENT.speed); - for (uint16_t i = 0; i < SEGLEN; i++) { - + for (int i = 0; i < SEGLEN; i++) { uint16_t shift_x = 4223; // no movement along x and y uint16_t shift_y = 1234; - uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field uint32_t real_y = (i + shift_y) * scale; // based on the precalculated positions - uint32_t real_z = SEGENV.step*8; - + uint32_t real_z = SEGENV.step*8; uint8_t noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map led color based on noise data - fastled_col = ColorFromPalette(currentPalette, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + //fastled_col = ColorFromPalette(SEGPALETTE, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. + //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); } return FRAMETIME; } +static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!"; //https://github.com/aykevl/ledstrip-spark/blob/master/ledstrip.ino -uint16_t WS2812FX::mode_noise16_4() -{ - CRGB fastled_col; - uint32_t stp = (now * SEGMENT.speed) >> 7; - for (uint16_t i = 0; i < SEGLEN; i++) { +uint16_t mode_noise16_4() { + //CRGB fastled_col; + uint32_t stp = (strip.now * SEGMENT.speed) >> 7; + for (int i = 0; i < SEGLEN; i++) { int16_t index = inoise16(uint32_t(i) << 12, stp); - fastled_col = ColorFromPalette(currentPalette, index); - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + //fastled_col = ColorFromPalette(SEGPALETTE, index); + //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; } +static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!"; //based on https://gist.github.com/kriegsman/5408ecd397744ba0393e -uint16_t WS2812FX::mode_colortwinkle() -{ +uint16_t mode_colortwinkle() { uint16_t dataSize = (SEGLEN+7) >> 3; //1 bit per LED if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - + CRGB fastled_col, prev; - fract8 fadeUpAmount = _brightness>28 ? 8 + (SEGMENT.speed>>2) : 68-_brightness, fadeDownAmount = _brightness>28 ? 8 + (SEGMENT.speed>>3) : 68-_brightness; + fract8 fadeUpAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>2) : 68-strip.getBrightness(); + fract8 fadeDownAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>3) : 68-strip.getBrightness(); for (uint16_t i = 0; i < SEGLEN; i++) { - fastled_col = col_to_crgb(getPixelColor(i)); + fastled_col = SEGMENT.getPixelColor(i); prev = fastled_col; uint16_t index = i >> 3; uint8_t bitNum = i & 0x07; bool fadeUp = bitRead(SEGENV.data[index], bitNum); - + if (fadeUp) { CRGB incrementalColor = fastled_col; incrementalColor.nscale8_video(fadeUpAmount); @@ -2082,15 +2212,15 @@ uint16_t WS2812FX::mode_colortwinkle() if (fastled_col.red == 255 || fastled_col.green == 255 || fastled_col.blue == 255) { bitWrite(SEGENV.data[index], bitNum, false); } - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); - if (col_to_crgb(getPixelColor(i)) == prev) { //fix "stuck" pixels + if (SEGMENT.getPixelColor(i) == RGBW32(prev.r, prev.g, prev.b, 0)) { //fix "stuck" pixels fastled_col += fastled_col; - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, fastled_col); } } else { fastled_col.nscale8(255 - fadeDownAmount); - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, fastled_col); } } @@ -2098,12 +2228,12 @@ uint16_t WS2812FX::mode_colortwinkle() if (random8() <= SEGMENT.intensity) { for (uint8_t times = 0; times < 5; times++) { //attempt to spawn a new pixel 5 times int i = random16(SEGLEN); - if (getPixelColor(i) == 0) { - fastled_col = ColorFromPalette(currentPalette, random8(), 64, NOBLEND); + if (SEGMENT.getPixelColor(i) == 0) { + fastled_col = ColorFromPalette(SEGPALETTE, random8(), 64, NOBLEND); uint16_t index = i >> 3; uint8_t bitNum = i & 0x07; bitWrite(SEGENV.data[index], bitNum, true); - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, fastled_col); break; //only spawn 1 new pixel per frame per 50 LEDs } } @@ -2111,106 +2241,115 @@ uint16_t WS2812FX::mode_colortwinkle() } return FRAMETIME_FIXED; } +static const char _data_FX_MODE_COLORTWINKLE[] PROGMEM = "Colortwinkles@Fade speed,Spawn speed;;!;;m12=0"; //pixels //Calm effect, like a lake at night -uint16_t WS2812FX::mode_lake() { +uint16_t mode_lake() { uint8_t sp = SEGMENT.speed/10; int wave1 = beatsin8(sp +2, -64,64); int wave2 = beatsin8(sp +1, -64,64); uint8_t wave3 = beatsin8(sp +2, 0,80); - CRGB fastled_col; + //CRGB fastled_col; - for (uint16_t i = 0; i < SEGLEN; i++) + for (int i = 0; i < SEGLEN; i++) { - int index = cos8((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; + int index = cos8((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; uint8_t lum = (index > wave3) ? index - wave3 : 0; - fastled_col = ColorFromPalette(currentPalette, map(index,0,255,0,240), lum, LINEARBLEND); - setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + //fastled_col = ColorFromPalette(SEGPALETTE, map(index,0,255,0,240), lum, LINEARBLEND); + //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, false, 0, lum)); } + return FRAMETIME; } +static const char _data_FX_MODE_LAKE[] PROGMEM = "Lake@!;Fx;!"; // meteor effect // send a meteor from begining to to the end of the strip with a trail that randomly decays. // adapted from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectMeteorRain -uint16_t WS2812FX::mode_meteor() { +uint16_t mode_meteor() { + if (SEGLEN == 1) return mode_static(); if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed byte* trail = SEGENV.data; - - byte meteorSize= 1+ SEGLEN / 10; - uint16_t counter = now * ((SEGMENT.speed >> 2) +8); + + const unsigned meteorSize= 1 + SEGLEN / 20; // 5% + uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); uint16_t in = counter * SEGLEN >> 16; + const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // fade all leds to colors[1] in LEDs one step - for (uint16_t i = 0; i < SEGLEN; i++) { - if (random8() <= 255 - SEGMENT.intensity) - { - byte meteorTrailDecay = 128 + random8(127); + for (int i = 0; i < SEGLEN; i++) { + if (random8() <= 255 - SEGMENT.intensity) { + byte meteorTrailDecay = 162 + random8(92); trail[i] = scale8(trail[i], meteorTrailDecay); - setPixelColor(i, color_from_palette(trail[i], false, true, 255)); + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + SEGMENT.setPixelColor(i, col); } } // draw meteor - for(int j = 0; j < meteorSize; j++) { + for (unsigned j = 0; j < meteorSize; j++) { uint16_t index = in + j; - if(index >= SEGLEN) { - index = (in + j - SEGLEN); + if (index >= SEGLEN) { + index -= SEGLEN; } - - trail[index] = 240; - setPixelColor(index, color_from_palette(trail[index], false, true, 255)); + trail[index] = max; + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); + SEGMENT.setPixelColor(index, col); } return FRAMETIME; } +static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient;;!;1"; // smooth meteor effect // send a meteor from begining to to the end of the strip with a trail that randomly decays. // adapted from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectMeteorRain -uint16_t WS2812FX::mode_meteor_smooth() { +uint16_t mode_meteor_smooth() { + if (SEGLEN == 1) return mode_static(); if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed byte* trail = SEGENV.data; - - byte meteorSize= 1+ SEGLEN / 10; + + const unsigned meteorSize= 1+ SEGLEN / 20; // 5% uint16_t in = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); + const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // fade all leds to colors[1] in LEDs one step - for (uint16_t i = 0; i < SEGLEN; i++) { - if (trail[i] != 0 && random8() <= 255 - SEGMENT.intensity) - { - int change = 3 - random8(12); //change each time between -8 and +3 - trail[i] += change; - if (trail[i] > 245) trail[i] = 0; - if (trail[i] > 240) trail[i] = 240; - setPixelColor(i, color_from_palette(trail[i], false, true, 255)); + for (int i = 0; i < SEGLEN; i++) { + if (/*trail[i] != 0 &&*/ random8() <= 255 - SEGMENT.intensity) { + int change = trail[i] + 4 - random8(24); //change each time between -20 and +4 + trail[i] = constrain(change, 0, max); + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + SEGMENT.setPixelColor(i, col); } } - + // draw meteor - for(int j = 0; j < meteorSize; j++) { - uint16_t index = in + j; - if(in + j >= SEGLEN) { - index = (in + j - SEGLEN); + for (unsigned j = 0; j < meteorSize; j++) { + uint16_t index = in + j; + if (index >= SEGLEN) { + index -= SEGLEN; } - setPixelColor(index, color_blend(getPixelColor(index), color_from_palette(240, false, true, 255), 48)); - trail[index] = 240; + trail[index] = max; + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); + SEGMENT.setPixelColor(index, col); } SEGENV.step += SEGMENT.speed +1; return FRAMETIME; } +static const char _data_FX_MODE_METEOR_SMOOTH[] PROGMEM = "Meteor Smooth@!,Trail,,,,Gradient;;!;1"; //Railway Crossing / Christmas Fairy lights -uint16_t WS2812FX::mode_railway() -{ - uint16_t dur = 40 + (255 - SEGMENT.speed) * 10; +uint16_t mode_railway() { + if (SEGLEN == 1) return mode_static(); + uint16_t dur = (256 - SEGMENT.speed) * 40; uint16_t rampdur = (dur * SEGMENT.intensity) >> 8; if (SEGENV.step > dur) { @@ -2225,17 +2364,18 @@ uint16_t WS2812FX::mode_railway() if (p0 < 255) pos = p0; } if (SEGENV.aux0) pos = 255 - pos; - for (uint16_t i = 0; i < SEGLEN; i += 2) + for (int i = 0; i < SEGLEN; i += 2) { - setPixelColor(i, color_from_palette(255 - pos, false, false, 255)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(255 - pos, false, false, 255)); // do not use color 1 or 2, always use palette if (i < SEGLEN -1) { - setPixelColor(i + 1, color_from_palette(pos, false, false, 255)); + SEGMENT.setPixelColor(i + 1, SEGMENT.color_from_palette(pos, false, false, 255)); // do not use color 1 or 2, always use palette } } SEGENV.step += FRAMETIME; return FRAMETIME; } +static const char _data_FX_MODE_RAILWAY[] PROGMEM = "Railway@!,Smoothness;1,2;!"; //Water ripple @@ -2254,99 +2394,94 @@ typedef struct Ripple { #else #define MAX_RIPPLES 100 #endif -uint16_t WS2812FX::ripple_base(bool rainbow) +uint16_t ripple_base() { uint16_t maxRipples = min(1 + (SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 uint16_t dataSize = sizeof(ripple) * maxRipples; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - + Ripple* ripples = reinterpret_cast(SEGENV.data); - // ranbow background or chosen background, all very dim. - if (rainbow) { - if (SEGENV.call ==0) { - SEGENV.aux0 = random8(); - SEGENV.aux1 = random8(); - } - if (SEGENV.aux0 == SEGENV.aux1) { - SEGENV.aux1 = random8(); - } - else if (SEGENV.aux1 > SEGENV.aux0) { - SEGENV.aux0++; - } else { - SEGENV.aux0--; - } - fill(color_blend(color_wheel(SEGENV.aux0),BLACK,235)); - } else { - fill(SEGCOLOR(1)); - } - //draw wave - for (uint16_t i = 0; i < maxRipples; i++) - { + for (int i = 0; i < maxRipples; i++) { uint16_t ripplestate = ripples[i].state; - if (ripplestate) - { + if (ripplestate) { uint8_t rippledecay = (SEGMENT.speed >> 4) +1; //faster decay if faster propagation uint16_t rippleorigin = ripples[i].pos; - uint32_t col = color_from_palette(ripples[i].color, false, false, 255); - uint16_t propagation = ((ripplestate/rippledecay -1) * SEGMENT.speed); + uint32_t col = SEGMENT.color_from_palette(ripples[i].color, false, false, 255); + uint16_t propagation = ((ripplestate/rippledecay - 1) * (SEGMENT.speed + 1)); int16_t propI = propagation >> 8; uint8_t propF = propagation & 0xFF; - int16_t left = rippleorigin - propI -1; uint8_t amp = (ripplestate < 17) ? triwave8((ripplestate-1)*8) : map(ripplestate,17,255,255,2); - for (int16_t v = left; v < left +4; v++) + #ifndef WLED_DISABLE_2D + if (SEGMENT.is2D()) { + propI /= 2; + uint16_t cx = rippleorigin >> 8; + uint16_t cy = rippleorigin & 0xFF; + uint8_t mag = scale8(sin8((propF>>2)), amp); + if (propI > 0) SEGMENT.draw_circle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag)); + } else + #endif { - uint8_t mag = scale8(cubicwave8((propF>>2)+(v-left)*64), amp); - if (v < SEGLEN && v >= 0) - { - setPixelColor(v, color_blend(getPixelColor(v), col, mag)); - } - int16_t w = left + propI*2 + 3 -(v-left); - if (w < SEGLEN && w >= 0) - { - setPixelColor(w, color_blend(getPixelColor(w), col, mag)); + int16_t left = rippleorigin - propI -1; + for (int16_t v = left; v < left +4; v++) { + uint8_t mag = scale8(cubicwave8((propF>>2)+(v-left)*64), amp); + SEGMENT.setPixelColor(v, color_blend(SEGMENT.getPixelColor(v), col, mag)); // TODO + int16_t w = left + propI*2 + 3 -(v-left); + SEGMENT.setPixelColor(w, color_blend(SEGMENT.getPixelColor(w), col, mag)); // TODO } - } + } ripplestate += rippledecay; ripples[i].state = (ripplestate > 254) ? 0 : ripplestate; - } else //randomly create new wave - { - if (random16(IBN + 10000) <= SEGMENT.intensity) - { + } else {//randomly create new wave + if (random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) { ripples[i].state = 1; - ripples[i].pos = random16(SEGLEN); + ripples[i].pos = SEGMENT.is2D() ? ((random8(SEGENV.virtualWidth())<<8) | (random8(SEGENV.virtualHeight()))) : random16(SEGLEN); ripples[i].color = random8(); //color } } } + return FRAMETIME; } #undef MAX_RIPPLES -uint16_t WS2812FX::mode_ripple(void) { - return ripple_base(false); -} -uint16_t WS2812FX::mode_ripple_rainbow(void) { - return ripple_base(true); +uint16_t mode_ripple(void) { + if (SEGLEN == 1) return mode_static(); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); + else SEGMENT.fade_out(250); + return ripple_base(); } +static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,,,,,Overlay;,!;!;12"; +uint16_t mode_ripple_rainbow(void) { + if (SEGLEN == 1) return mode_static(); + if (SEGENV.call ==0) { + SEGENV.aux0 = random8(); + SEGENV.aux1 = random8(); + } + if (SEGENV.aux0 == SEGENV.aux1) { + SEGENV.aux1 = random8(); + } else if (SEGENV.aux1 > SEGENV.aux0) { + SEGENV.aux0++; + } else { + SEGENV.aux0--; + } + SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux0),BLACK,235)); + return ripple_base(); +} +static const char _data_FX_MODE_RIPPLE_RAINBOW[] PROGMEM = "Ripple Rainbow@!,Wave #;;!;12"; + // TwinkleFOX by Mark Kriegsman: https://gist.github.com/kriegsman/756ea6dcae8e30845b5a // // TwinkleFOX: Twinkling 'holiday' lights that fade in and out. // Colors are chosen from a palette. Read more about this effect using the link above! - -// If COOL_LIKE_INCANDESCENT is set to 1, colors will -// fade out slighted 'reddened', similar to how -// incandescent bulbs change color as they get dim down. -#define COOL_LIKE_INCANDESCENT 1 - -CRGB IRAM_ATTR WS2812FX::twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) +CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) { // Overall twinkle speed (changed) uint16_t ticks = ms / SEGENV.aux0; @@ -2355,7 +2490,7 @@ CRGB IRAM_ATTR WS2812FX::twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool slowcycle16 += sin8(slowcycle16); slowcycle16 = (slowcycle16 * 2053) + 1384; uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); - + // Overall twinkle density. // 0 (NONE lit) to 8 (ALL lit at once). // Default is 5. @@ -2383,12 +2518,12 @@ CRGB IRAM_ATTR WS2812FX::twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool uint8_t hue = slowcycle8 - salt; CRGB c; if (bright > 0) { - c = ColorFromPalette(currentPalette, hue, bright, NOBLEND); - if(COOL_LIKE_INCANDESCENT == 1) { + c = ColorFromPalette(SEGPALETTE, hue, bright, NOBLEND); + if (!SEGMENT.check1) { // This code takes a pixel, and if its in the 'fading down' // part of the cycle, it adjusts the color a little bit like the // way that incandescent bulbs fade toward 'red' as they dim. - if (fastcycle8 >= 128) + if (fastcycle8 >= 128) { uint8_t cooling = (fastcycle8 - 128) >> 4; c.g = qsub8(c.g, cooling); @@ -2406,7 +2541,7 @@ CRGB IRAM_ATTR WS2812FX::twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool // "CalculateOneTwinkle" on each pixel. It then displays // either the twinkle color of the background color, // whichever is brighter. -uint16_t WS2812FX::twinklefox_base(bool cat) +uint16_t twinklefox_base(bool cat) { // "PRNG16" is the pseudorandom number generator // It MUST be reset to the same starting value each time @@ -2419,8 +2554,7 @@ uint16_t WS2812FX::twinklefox_base(bool cat) else SEGENV.aux0 = 22 + ((100 - SEGMENT.speed) >> 1); // Set up the background color, "bg". - CRGB bg; - bg = col_to_crgb(SEGCOLOR(1)); + CRGB bg = CRGB(SEGCOLOR(1)); uint8_t bglight = bg.getAverageLight(); if (bglight > 64) { bg.nscale8_video(16); // very bright, so scale to 1/16th @@ -2432,14 +2566,14 @@ uint16_t WS2812FX::twinklefox_base(bool cat) uint8_t backgroundBrightness = bg.getAverageLight(); - for (uint16_t i = 0; i < SEGLEN; i++) { - + for (int i = 0; i < SEGLEN; i++) { + PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number uint16_t myclockoffset16= PRNG16; // use that number as clock offset PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number // use that number as clock speed adjustment factor (in 8ths, from 8/8ths to 23/8ths) uint8_t myspeedmultiplierQ5_3 = ((((PRNG16 & 0xFF)>>4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08; - uint32_t myclock30 = (uint32_t)((now * myspeedmultiplierQ5_3) >> 3) + myclockoffset16; + uint32_t myclock30 = (uint32_t)((strip.now * myspeedmultiplierQ5_3) >> 3) + myclockoffset16; uint8_t myunique8 = PRNG16 >> 8; // get 'salt' value for this pixel // We now have the adjusted 'clock' for this pixel, now we call @@ -2452,122 +2586,133 @@ uint16_t WS2812FX::twinklefox_base(bool cat) if (deltabright >= 32 || (!bg)) { // If the new pixel is significantly brighter than the background color, // use the new color. - setPixelColor(i, c.red, c.green, c.blue); + SEGMENT.setPixelColor(i, c.red, c.green, c.blue); } else if (deltabright > 0) { // If the new pixel is just slightly brighter than the background color, // mix a blend of the new color and the background color - setPixelColor(i, color_blend(crgb_to_col(bg), crgb_to_col(c), deltabright * 8)); + SEGMENT.setPixelColor(i, color_blend(RGBW32(bg.r,bg.g,bg.b,0), RGBW32(c.r,c.g,c.b,0), deltabright * 8)); } else { // if the new pixel is not at all brighter than the background color, // just use the background color. - setPixelColor(i, bg.r, bg.g, bg.b); + SEGMENT.setPixelColor(i, bg.r, bg.g, bg.b); } } return FRAMETIME; } -uint16_t WS2812FX::mode_twinklefox() + +uint16_t mode_twinklefox() { return twinklefox_base(false); } +static const char _data_FX_MODE_TWINKLEFOX[] PROGMEM = "Twinklefox@!,Twinkle rate,,,,Cool;!,!;!"; + -uint16_t WS2812FX::mode_twinklecat() +uint16_t mode_twinklecat() { return twinklefox_base(true); } +static const char _data_FX_MODE_TWINKLECAT[] PROGMEM = "Twinklecat@!,Twinkle rate,,,,Cool;!,!;!"; //inspired by https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectBlinkingHalloweenEyes -#define HALLOWEEN_EYE_SPACE 3 -#define HALLOWEEN_EYE_WIDTH 1 - -uint16_t WS2812FX::mode_halloween_eyes() -{ +uint16_t mode_halloween_eyes() +{ + if (SEGLEN == 1) return mode_static(); + const uint16_t maxWidth = strip.isMatrix ? SEGMENT.virtualWidth() : SEGLEN; + const uint16_t HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEGMENT.virtualWidth()>>4: SEGLEN>>5); + const uint16_t HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2; uint16_t eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE; - if (eyeLength > SEGLEN) return mode_static(); //bail if segment too short + if (eyeLength >= maxWidth) return mode_static(); //bail if segment too short - fill(SEGCOLOR(1)); //fill background + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); //fill background uint8_t state = SEGENV.aux1 >> 8; uint16_t stateTime = SEGENV.call; if (stateTime == 0) stateTime = 2000; if (state == 0) { //spawn eyes - SEGENV.aux0 = random16(0, SEGLEN - eyeLength); //start pos + SEGENV.aux0 = random16(0, maxWidth - eyeLength - 1); //start pos SEGENV.aux1 = random8(); //color + if (strip.isMatrix) SEGMENT.offset = random16(SEGMENT.virtualHeight()-1); // a hack: reuse offset since it is not used in matrices state = 1; } - + if (state < 2) { //fade eyes uint16_t startPos = SEGENV.aux0; uint16_t start2ndEye = startPos + HALLOWEEN_EYE_WIDTH + HALLOWEEN_EYE_SPACE; - - uint32_t fadestage = (now - SEGENV.step)*255 / stateTime; + + uint32_t fadestage = (strip.now - SEGENV.step)*255 / stateTime; if (fadestage > 255) fadestage = 255; - uint32_t c = color_blend(color_from_palette(SEGENV.aux1 & 0xFF, false, false, 0), SEGCOLOR(1), fadestage); - - for (uint16_t i = 0; i < HALLOWEEN_EYE_WIDTH; i++) - { - setPixelColor(startPos + i, c); - setPixelColor(start2ndEye + i, c); + uint32_t c = color_blend(SEGMENT.color_from_palette(SEGENV.aux1 & 0xFF, false, false, 0), SEGCOLOR(1), fadestage); + + for (int i = 0; i < HALLOWEEN_EYE_WIDTH; i++) { + if (strip.isMatrix) { + SEGMENT.setPixelColorXY(startPos + i, SEGMENT.offset, c); + SEGMENT.setPixelColorXY(start2ndEye + i, SEGMENT.offset, c); + } else { + SEGMENT.setPixelColor(startPos + i, c); + SEGMENT.setPixelColor(start2ndEye + i, c); + } } } - if (now - SEGENV.step > stateTime) - { + if (strip.now - SEGENV.step > stateTime) { state++; if (state > 2) state = 0; - - if (state < 2) - { - stateTime = 100 + (255 - SEGMENT.intensity)*10; //eye fade time + + if (state < 2) { + stateTime = 100 + SEGMENT.intensity*10; //eye fade time } else { - uint16_t eyeOffTimeBase = (255 - SEGMENT.speed)*10; + uint16_t eyeOffTimeBase = (256 - SEGMENT.speed)*10; stateTime = eyeOffTimeBase + random16(eyeOffTimeBase); } - SEGENV.step = now; + SEGENV.step = strip.now; SEGENV.call = stateTime; } SEGENV.aux1 = (SEGENV.aux1 & 0xFF) + (state << 8); //save state - + return FRAMETIME; } +static const char _data_FX_MODE_HALLOWEEN_EYES[] PROGMEM = "Halloween Eyes@Duration,Eye fade time,,,,,Overlay;!,!;!;12"; //Speed slider sets amount of LEDs lit, intensity sets unlit -uint16_t WS2812FX::mode_static_pattern() +uint16_t mode_static_pattern() { uint16_t lit = 1 + SEGMENT.speed; uint16_t unlit = 1 + SEGMENT.intensity; bool drawingLit = true; uint16_t cnt = 0; - for (uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, (drawingLit) ? color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(1)); + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, (drawingLit) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(1)); cnt++; if (cnt >= ((drawingLit) ? lit : unlit)) { cnt = 0; drawingLit = !drawingLit; } } - + return FRAMETIME; } +static const char _data_FX_MODE_STATIC_PATTERN[] PROGMEM = "Solid Pattern@Fg size,Bg size;Fg,!;!;;pal=0"; -uint16_t WS2812FX::mode_tri_static_pattern() + +uint16_t mode_tri_static_pattern() { uint8_t segSize = (SEGMENT.intensity >> 5) +1; uint8_t currSeg = 0; uint16_t currSegCount = 0; - for (uint16_t i = 0; i < SEGLEN; i++) { + for (int i = 0; i < SEGLEN; i++) { if ( currSeg % 3 == 0 ) { - setPixelColor(i, SEGCOLOR(0)); + SEGMENT.setPixelColor(i, SEGCOLOR(0)); } else if( currSeg % 3 == 1) { - setPixelColor(i, SEGCOLOR(1)); + SEGMENT.setPixelColor(i, SEGCOLOR(1)); } else { - setPixelColor(i, (SEGCOLOR(2) > 0 ? SEGCOLOR(2) : WHITE)); + SEGMENT.setPixelColor(i, (SEGCOLOR(2) > 0 ? SEGCOLOR(2) : WHITE)); } currSegCount += 1; if (currSegCount >= segSize) { @@ -2578,50 +2723,54 @@ uint16_t WS2812FX::mode_tri_static_pattern() return FRAMETIME; } +static const char _data_FX_MODE_TRI_STATIC_PATTERN[] PROGMEM = "Solid Pattern Tri@,Size;1,2,3;;;pal=0"; -uint16_t WS2812FX::spots_base(uint16_t threshold) +uint16_t spots_base(uint16_t threshold) { - fill(SEGCOLOR(1)); - + if (SEGLEN == 1) return mode_static(); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); + uint16_t maxZones = SEGLEN >> 2; uint16_t zones = 1 + ((SEGMENT.intensity * maxZones) >> 8); uint16_t zoneLen = SEGLEN / zones; uint16_t offset = (SEGLEN - zones * zoneLen) >> 1; - for (uint16_t z = 0; z < zones; z++) + for (int z = 0; z < zones; z++) { uint16_t pos = offset + z * zoneLen; - for (uint16_t i = 0; i < zoneLen; i++) + for (int i = 0; i < zoneLen; i++) { uint16_t wave = triwave16((i * 0xFFFF) / zoneLen); if (wave > threshold) { uint16_t index = 0 + pos + i; uint8_t s = (wave - threshold)*255 / (0xFFFF - threshold); - setPixelColor(index, color_blend(color_from_palette(index, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255-s)); + SEGMENT.setPixelColor(index, color_blend(SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255-s)); } } } - + return FRAMETIME; } //Intensity slider sets number of "lights", speed sets LEDs per light -uint16_t WS2812FX::mode_spots() +uint16_t mode_spots() { return spots_base((255 - SEGMENT.speed) << 8); } +static const char _data_FX_MODE_SPOTS[] PROGMEM = "Spots@Spread,Width,,,,,Overlay;!,!;!"; //Intensity slider sets number of "lights", LEDs per light fade in and out -uint16_t WS2812FX::mode_spots_fade() +uint16_t mode_spots_fade() { - uint16_t counter = now * ((SEGMENT.speed >> 2) +8); + uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); uint16_t t = triwave16(counter); uint16_t tr = (t >> 1) + (t >> 2); return spots_base(tr); } +static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; //each needs 12 bytes @@ -2634,89 +2783,206 @@ typedef struct Ball { /* * Bouncing Balls Effect */ -uint16_t WS2812FX::mode_bouncing_balls(void) { +uint16_t mode_bouncing_balls(void) { + if (SEGLEN == 1) return mode_static(); //allocate segment data - uint16_t maxNumBalls = 16; + const uint16_t strips = SEGMENT.nrOfVStrips(); // adapt for 2D + const size_t maxNumBalls = 16; uint16_t dataSize = sizeof(ball) * maxNumBalls; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - + if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed + Ball* balls = reinterpret_cast(SEGENV.data); - - // number of balls based on intensity setting to max of 7 (cycles colors) - // non-chosen color is a random color - uint8_t numBalls = int(((SEGMENT.intensity * (maxNumBalls - 0.8f)) / 255) + 1); - - float gravity = -9.81; // standard value of gravity - float impactVelocityStart = sqrt( -2 * gravity); - unsigned long time = millis(); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2) ? BLACK : SEGCOLOR(1)); + + // virtualStrip idea by @ewowi (Ewoud Wijma) + // requires virtual strip # to be embedded into upper 16 bits of index in setPixelColor() + // the following functions will not work on virtual strips: fill(), fade_out(), fadeToBlack(), blur() + struct virtualStrip { + static void runStrip(size_t stripNr, Ball* balls) { + // number of balls based on intensity setting to max of 7 (cycles colors) + // non-chosen color is a random color + uint16_t numBalls = (SEGMENT.intensity * (maxNumBalls - 1)) / 255 + 1; // minimum 1 ball + const float gravity = -9.81f; // standard value of gravity + const bool hasCol2 = SEGCOLOR(2); + const unsigned long time = millis(); + + if (SEGENV.call == 0) { + for (size_t i = 0; i < maxNumBalls; i++) balls[i].lastBounceTime = time; + } + + for (size_t i = 0; i < numBalls; i++) { + float timeSinceLastBounce = (time - balls[i].lastBounceTime)/((255-SEGMENT.speed)/64 +1); + float timeSec = timeSinceLastBounce/1000.0f; + balls[i].height = (0.5f * gravity * timeSec + balls[i].impactVelocity) * timeSec; // avoid use pow(x, 2) - its extremely slow ! + + if (balls[i].height <= 0.0f) { + balls[i].height = 0.0f; + //damping for better effect using multiple balls + float dampening = 0.9f - float(i)/float(numBalls * numBalls); // avoid use pow(x, 2) - its extremely slow ! + balls[i].impactVelocity = dampening * balls[i].impactVelocity; + balls[i].lastBounceTime = time; + + if (balls[i].impactVelocity < 0.015f) { + float impactVelocityStart = sqrtf(-2.0f * gravity) * random8(5,11)/10.0f; // randomize impact velocity + balls[i].impactVelocity = impactVelocityStart; + } + } else if (balls[i].height > 1.0f) { + continue; // do not draw OOB ball + } + + uint32_t color = SEGCOLOR(0); + if (SEGMENT.palette) { + color = SEGMENT.color_wheel(i*(256/MAX(numBalls, 8))); + } else if (hasCol2) { + color = SEGCOLOR(i % NUM_COLORS); + } + + int pos = roundf(balls[i].height * (SEGLEN - 1)); + if (SEGLEN<32) SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index + else SEGMENT.setPixelColor(balls[i].height + (stripNr+1)*10.0f, color); + } + } + }; + + for (int stripNr=0; stripNr(SEGENV.data); + + // number of balls based on intensity setting to max of 16 (cycles colors) + // non-chosen color is a random color + uint8_t numBalls = SEGMENT.intensity/16 + 1; + bool hasCol2 = SEGCOLOR(2); if (SEGENV.call == 0) { - for (uint8_t i = 0; i < maxNumBalls; i++) balls[i].lastBounceTime = time; + SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); // start clean + for (int i = 0; i < maxNumBalls; i++) { + balls[i].lastBounceUpdate = strip.now; + balls[i].velocity = 20.0f * float(random16(1000, 10000))/10000.0f; // number from 1 to 10 + if (random8()<128) balls[i].velocity = -balls[i].velocity; // 50% chance of reverse direction + balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. + balls[i].mass = (float(random16(1000, 10000)) / 10000.0f); // from .1 to 1. + } } - - bool hasCol2 = SEGCOLOR(2); - fill(hasCol2 ? BLACK : SEGCOLOR(1)); - - for (uint8_t i = 0; i < numBalls; i++) { - float timeSinceLastBounce = (time - balls[i].lastBounceTime)/((255-SEGMENT.speed)*8/256 +1); - balls[i].height = 0.5 * gravity * pow(timeSinceLastBounce/1000 , 2.0) + balls[i].impactVelocity * timeSinceLastBounce/1000; - - if (balls[i].height < 0) { //start bounce - balls[i].height = 0; - //damping for better effect using multiple balls - float dampening = 0.90 - float(i)/pow(numBalls,2); - balls[i].impactVelocity = dampening * balls[i].impactVelocity; - balls[i].lastBounceTime = time; - - if (balls[i].impactVelocity < 0.015) { - balls[i].impactVelocity = impactVelocityStart; + + float cfac = float(scale8(8, 255-SEGMENT.speed) +1)*20000.0f; // this uses the Aircoookie conversion factor for scaling time using speed slider + + if (SEGMENT.check3) SEGMENT.fade_out(250); // 2-8 pixel trails (optional) + else { + if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); // don't fill with background color if user wants to see trails + } + + for (int i = 0; i < numBalls; i++) { + float timeSinceLastUpdate = float((strip.now - balls[i].lastBounceUpdate))/cfac; + float thisHeight = balls[i].height + balls[i].velocity * timeSinceLastUpdate; // this method keeps higher resolution + // test if intensity level was increased and some balls are way off the track then put them back + if (thisHeight < -0.5f || thisHeight > 1.5f) { + thisHeight = balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. + balls[i].lastBounceUpdate = strip.now; + } + // check if reached ends of the strip + if ((thisHeight <= 0.0f && balls[i].velocity < 0.0f) || (thisHeight >= 1.0f && balls[i].velocity > 0.0f)) { + balls[i].velocity = -balls[i].velocity; // reverse velocity + balls[i].lastBounceUpdate = strip.now; + balls[i].height = thisHeight; + } + // check for collisions + if (SEGMENT.check1) { + for (int j = i+1; j < numBalls; j++) { + if (balls[j].velocity != balls[i].velocity) { + // tcollided + balls[j].lastBounceUpdate is acutal time of collision (this keeps precision with long to float conversions) + float tcollided = (cfac*(balls[i].height - balls[j].height) + + balls[i].velocity*float(balls[j].lastBounceUpdate - balls[i].lastBounceUpdate))/(balls[j].velocity - balls[i].velocity); + + if ((tcollided > 2.0f) && (tcollided < float(strip.now - balls[j].lastBounceUpdate))) { // 2ms minimum to avoid duplicate bounces + balls[i].height = balls[i].height + balls[i].velocity*(tcollided + float(balls[j].lastBounceUpdate - balls[i].lastBounceUpdate))/cfac; + balls[j].height = balls[i].height; + balls[i].lastBounceUpdate = (unsigned long)(tcollided + 0.5f) + balls[j].lastBounceUpdate; + balls[j].lastBounceUpdate = balls[i].lastBounceUpdate; + float vtmp = balls[i].velocity; + balls[i].velocity = ((balls[i].mass - balls[j].mass)*vtmp + 2.0f*balls[j].mass*balls[j].velocity)/(balls[i].mass + balls[j].mass); + balls[j].velocity = ((balls[j].mass - balls[i].mass)*balls[j].velocity + 2.0f*balls[i].mass*vtmp) /(balls[i].mass + balls[j].mass); + thisHeight = balls[i].height + balls[i].velocity*(strip.now - balls[i].lastBounceUpdate)/cfac; + } + } } } - + uint32_t color = SEGCOLOR(0); if (SEGMENT.palette) { - color = color_wheel(i*(256/MAX(numBalls, 8))); + //color = SEGMENT.color_wheel(i*(256/MAX(numBalls, 8))); + color = SEGMENT.color_from_palette(i*255/numBalls, false, PALETTE_SOLID_WRAP, 0); } else if (hasCol2) { color = SEGCOLOR(i % NUM_COLORS); } - uint16_t pos = round(balls[i].height * (SEGLEN - 1)); - setPixelColor(pos, color); + if (thisHeight < 0.0f) thisHeight = 0.0f; + if (thisHeight > 1.0f) thisHeight = 1.0f; + uint16_t pos = round(thisHeight * (SEGLEN - 1)); + SEGMENT.setPixelColor(pos, color); + balls[i].lastBounceUpdate = strip.now; + balls[i].height = thisHeight; } return FRAMETIME; } +static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar /* * Sinelon stolen from FASTLED examples */ -uint16_t WS2812FX::sinelon_base(bool dual, bool rainbow=false) { - fade_out(SEGMENT.intensity); +uint16_t sinelon_base(bool dual, bool rainbow=false) { + if (SEGLEN == 1) return mode_static(); + SEGMENT.fade_out(SEGMENT.intensity); uint16_t pos = beatsin16(SEGMENT.speed/10,0,SEGLEN-1); if (SEGENV.call == 0) SEGENV.aux0 = pos; - uint32_t color1 = color_from_palette(pos, true, false, 0); + uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0); uint32_t color2 = SEGCOLOR(2); if (rainbow) { - color1 = color_wheel((pos & 0x07) * 32); + color1 = SEGMENT.color_wheel((pos & 0x07) * 32); } - setPixelColor(pos, color1); + SEGMENT.setPixelColor(pos, color1); if (dual) { - if (!color2) color2 = color_from_palette(pos, true, false, 0); + if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0); if (rainbow) color2 = color1; //rainbow - setPixelColor(SEGLEN-1-pos, color2); + SEGMENT.setPixelColor(SEGLEN-1-pos, color2); } - if (SEGENV.aux0 != pos) { + if (SEGENV.aux0 != pos) { if (SEGENV.aux0 < pos) { - for (uint16_t i = SEGENV.aux0; i < pos ; i++) { - setPixelColor(i, color1); - if (dual) setPixelColor(SEGLEN-1-i, color2); + for (int i = SEGENV.aux0; i < pos ; i++) { + SEGMENT.setPixelColor(i, color1); + if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); } } else { - for (uint16_t i = SEGENV.aux0; i > pos ; i--) { - setPixelColor(i, color1); - if (dual) setPixelColor(SEGLEN-1-i, color2); + for (int i = SEGENV.aux0; i > pos ; i--) { + SEGMENT.setPixelColor(i, color1); + if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); } } SEGENV.aux0 = pos; @@ -2725,109 +2991,139 @@ uint16_t WS2812FX::sinelon_base(bool dual, bool rainbow=false) { return FRAMETIME; } -uint16_t WS2812FX::mode_sinelon(void) { + +uint16_t mode_sinelon(void) { return sinelon_base(false); } +static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!"; + -uint16_t WS2812FX::mode_sinelon_dual(void) { +uint16_t mode_sinelon_dual(void) { return sinelon_base(true); } +static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!"; -uint16_t WS2812FX::mode_sinelon_rainbow(void) { + +uint16_t mode_sinelon_rainbow(void) { return sinelon_base(false, true); } +static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!"; -//Rainbow with glitter, inspired by https://gist.github.com/kriegsman/062e10f7f07ba8518af6 -uint16_t WS2812FX::mode_glitter() -{ - mode_palette(); - - if (SEGMENT.intensity > random8()) - { - setPixelColor(random16(SEGLEN), ULTRAWHITE); +// utility function that will add random glitter to SEGMENT +void glitter_base(uint8_t intensity, uint32_t col = ULTRAWHITE) { + if (intensity > random8()) { + if (SEGMENT.is2D()) { + SEGMENT.setPixelColorXY(random16(SEGMENT.virtualWidth()),random16(SEGMENT.virtualHeight()), col); + } else { + SEGMENT.setPixelColor(random16(SEGLEN), col); + } } - +} + +//Glitter with palette background, inspired by https://gist.github.com/kriegsman/062e10f7f07ba8518af6 +uint16_t mode_glitter() +{ + if (!SEGMENT.check2) mode_palette(); // use "* Color 1" palette for solid background (replacing "Solid glitter") + glitter_base(SEGMENT.intensity, SEGCOLOR(2) ? SEGCOLOR(2) : ULTRAWHITE); return FRAMETIME; } +static const char _data_FX_MODE_GLITTER[] PROGMEM = "Glitter@!,!,,,,,Overlay;1,2,Glitter color;!;;pal=0,m12=0"; //pixels +//Solid colour background with glitter (can be replaced by Glitter) +uint16_t mode_solid_glitter() +{ + SEGMENT.fill(SEGCOLOR(0)); + glitter_base(SEGMENT.intensity, SEGCOLOR(2) ? SEGCOLOR(2) : ULTRAWHITE); + return FRAMETIME; +} +static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,,Glitter color;;;m12=0"; -//each needs 12 bytes + +//each needs 19 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { - float pos; - float vel; + float pos, posX; + float vel, velX; uint16_t col; uint8_t colIndex; } spark; +#define maxNumPopcorn 21 // max 21 on 16 segment ESP8266 /* * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ -uint16_t WS2812FX::mode_popcorn(void) { +uint16_t mode_popcorn(void) { + if (SEGLEN == 1) return mode_static(); //allocate segment data - uint16_t maxNumPopcorn = 21; // max 21 on 16 segment ESP8266 + uint16_t strips = SEGMENT.nrOfVStrips(); uint16_t dataSize = sizeof(spark) * maxNumPopcorn; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - - Spark* popcorn = reinterpret_cast(SEGENV.data); + if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed - float gravity = -0.0001 - (SEGMENT.speed/200000.0); // m/s/s - gravity *= SEGLEN; + Spark* popcorn = reinterpret_cast(SEGENV.data); bool hasCol2 = SEGCOLOR(2); - fill(hasCol2 ? BLACK : SEGCOLOR(1)); - - uint8_t numPopcorn = SEGMENT.intensity*maxNumPopcorn/255; - if (numPopcorn == 0) numPopcorn = 1; - - for(uint8_t i = 0; i < numPopcorn; i++) { - if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position - popcorn[i].pos += popcorn[i].vel; - popcorn[i].vel += gravity; - } else { // if kernel is inactive, randomly pop it - if (random8() < 2) { // POP!!! - popcorn[i].pos = 0.01f; - - uint16_t peakHeight = 128 + random8(128); //0-255 - peakHeight = (peakHeight * (SEGLEN -1)) >> 8; - popcorn[i].vel = sqrt(-2.0 * gravity * peakHeight); - - if (SEGMENT.palette) - { - popcorn[i].colIndex = random8(); - } else { - byte col = random8(0, NUM_COLORS); - if (!hasCol2 || !SEGCOLOR(col)) col = 0; - popcorn[i].colIndex = col; + if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); + + struct virtualStrip { + static void runStrip(uint16_t stripNr, Spark* popcorn) { + float gravity = -0.0001 - (SEGMENT.speed/200000.0); // m/s/s + gravity *= SEGLEN; + + uint8_t numPopcorn = SEGMENT.intensity*maxNumPopcorn/255; + if (numPopcorn == 0) numPopcorn = 1; + + for(int i = 0; i < numPopcorn; i++) { + if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position + popcorn[i].pos += popcorn[i].vel; + popcorn[i].vel += gravity; + } else { // if kernel is inactive, randomly pop it + if (random8() < 2) { // POP!!! + popcorn[i].pos = 0.01f; + + uint16_t peakHeight = 128 + random8(128); //0-255 + peakHeight = (peakHeight * (SEGLEN -1)) >> 8; + popcorn[i].vel = sqrtf(-2.0f * gravity * peakHeight); + + if (SEGMENT.palette) + { + popcorn[i].colIndex = random8(); + } else { + byte col = random8(0, NUM_COLORS); + if (!SEGCOLOR(2) || !SEGCOLOR(col)) col = 0; + popcorn[i].colIndex = col; + } + } + } + if (popcorn[i].pos >= 0.0f) { // draw now active popcorn (either active before or just popped) + uint32_t col = SEGMENT.color_wheel(popcorn[i].colIndex); + if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); + uint16_t ledIndex = popcorn[i].pos; + if (ledIndex < SEGLEN) SEGMENT.setPixelColor(indexToVStrip(ledIndex, stripNr), col); } } } - if (popcorn[i].pos >= 0.0f) { // draw now active popcorn (either active before or just popped) - uint32_t col = color_wheel(popcorn[i].colIndex); - if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); - - uint16_t ledIndex = popcorn[i].pos; - if (ledIndex < SEGLEN) setPixelColor(ledIndex, col); - } - } + }; + + for (int stripNr=0; stripNr 1) { //allocate segment data - uint16_t dataSize = (SEGLEN -1) *3; //max. 1365 pixels (ESP8266) + uint16_t dataSize = max(1, SEGLEN -1) *3; //max. 1365 pixels (ESP8266) if (!SEGENV.allocateData(dataSize)) return candle(false); //allocation failed } @@ -2847,7 +3143,7 @@ uint16_t WS2812FX::candle(bool multi) uint16_t numCandles = (multi) ? SEGLEN : 1; - for (uint16_t i = 0; i < numCandles; i++) + for (int i = 0; i < numCandles; i++) { uint16_t d = 0; //data location @@ -2876,18 +3172,18 @@ uint16_t WS2812FX::candle(bool multi) s_target += offset; uint8_t dif = (s_target > s) ? s_target - s : s - s_target; - + fadeStep = dif >> speedFactor; if (fadeStep == 0) fadeStep = 1; } if (i > 0) { - setPixelColor(i, color_blend(SEGCOLOR(1), color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s)); SEGENV.data[d] = s; SEGENV.data[d+1] = s_target; SEGENV.data[d+2] = fadeStep; } else { - for (uint16_t j = 0; j < SEGLEN; j++) { - setPixelColor(j, color_blend(SEGCOLOR(1), color_from_palette(j, true, PALETTE_SOLID_WRAP, 0), s)); + for (int j = 0; j < SEGLEN; j++) { + SEGMENT.setPixelColor(j, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0), s)); } SEGENV.aux0 = s; SEGENV.aux1 = s_target; SEGENV.step = fadeStep; @@ -2897,16 +3193,19 @@ uint16_t WS2812FX::candle(bool multi) return FRAMETIME_FIXED; } -uint16_t WS2812FX::mode_candle() + +uint16_t mode_candle() { return candle(false); } +static const char _data_FX_MODE_CANDLE[] PROGMEM = "Candle@!,!;!,!;!;01;sx=96,ix=224,pal=0"; -uint16_t WS2812FX::mode_candle_multi() +uint16_t mode_candle_multi() { return candle(true); } +static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; /* @@ -2929,11 +3228,12 @@ typedef struct particle { float fragment[STARBURST_MAX_FRAG]; } star; -uint16_t WS2812FX::mode_starburst(void) { +uint16_t mode_starburst(void) { + if (SEGLEN == 1) return mode_static(); uint16_t maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 - uint8_t segs = getActiveSegmentsNum(); - if (segs <= (MAX_NUM_SEGMENTS /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs - if (segs <= (MAX_NUM_SEGMENTS /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs + uint8_t segs = strip.getActiveSegmentsNum(); + if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs + if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs uint16_t maxStars = maxData / sizeof(star); //ESP8266: max. 4/9/19 stars/seg, ESP32: max. 10/21/42 stars/seg uint8_t numStars = 1 + (SEGLEN >> 3); @@ -2941,26 +3241,26 @@ uint16_t WS2812FX::mode_starburst(void) { uint16_t dataSize = sizeof(star) * numStars; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - + uint32_t it = millis(); - + star* stars = reinterpret_cast(SEGENV.data); - + float maxSpeed = 375.0f; // Max velocity float particleIgnition = 250.0f; // How long to "flash" float particleFadeTime = 1500.0f; // Fade out time - + for (int j = 0; j < numStars; j++) { // speed to adjust chance of a burst, max is nearly always. if (random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0) { - // Pick a random color and location. - uint16_t startPos = random16(SEGLEN-1); + // Pick a random color and location. + uint16_t startPos = (SEGLEN > 1) ? random16(SEGLEN-1) : 0; float multiplier = (float)(random8())/255.0 * 1.0; - stars[j].color = col_to_crgb(color_wheel(random8())); - stars[j].pos = startPos; + stars[j].color = CRGB(SEGMENT.color_wheel(random8())); + stars[j].pos = startPos; stars[j].vel = maxSpeed * (float)(random8())/255.0 * multiplier; stars[j].birth = it; stars[j].last = it; @@ -2973,9 +3273,9 @@ uint16_t WS2812FX::mode_starburst(void) { } } } - - fill(SEGCOLOR(1)); - + + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); + for (int j=0; j> 1; - + if (stars[j].fragment[i] > 0) { //all fragments travel right, will be mirrored on other side stars[j].fragment[i] += stars[j].vel * dt * (float)var/3.0; @@ -2992,34 +3292,34 @@ uint16_t WS2812FX::mode_starburst(void) { stars[j].last = it; stars[j].vel -= 3*stars[j].vel*dt; } - + CRGB c = stars[j].color; - // If the star is brand new, it flashes white briefly. + // If the star is brand new, it flashes white briefly. // Otherwise it just fades over time. float fade = 0.0f; float age = it-stars[j].birth; if (age < particleIgnition) { - c = col_to_crgb(color_blend(WHITE, crgb_to_col(c), 254.5f*((age / particleIgnition)))); + c = CRGB(color_blend(WHITE, RGBW32(c.r,c.g,c.b,0), 254.5f*((age / particleIgnition)))); } else { - // Figure out how much to fade and shrink the star based on + // Figure out how much to fade and shrink the star based on // its age relative to its lifetime if (age > particleIgnition + particleFadeTime) { fade = 1.0f; // Black hole, all faded out stars[j].birth = 0; - c = col_to_crgb(SEGCOLOR(1)); + c = CRGB(SEGCOLOR(1)); } else { age -= particleIgnition; fade = (age / particleFadeTime); // Fading star byte f = 254.5f*fade; - c = col_to_crgb(color_blend(crgb_to_col(c), SEGCOLOR(1), f)); + c = CRGB(color_blend(RGBW32(c.r,c.g,c.b,0), SEGCOLOR(1), f)); } } - - float particleSize = (1.0 - fade) * 2; - for (uint8_t index=0; index < STARBURST_MAX_FRAG*2; index++) { + float particleSize = (1.0f - fade) * 2.0f; + + for (size_t index=0; index < STARBURST_MAX_FRAG*2; index++) { bool mirrored = index & 0x1; uint8_t i = index >> 1; if (stars[j].fragment[i] > 0) { @@ -3029,9 +3329,9 @@ uint16_t WS2812FX::mode_starburst(void) { int end = loc + particleSize; if (start < 0) start = 0; if (start == end) end++; - if (end > SEGLEN) end = SEGLEN; + if (end > SEGLEN) end = SEGLEN; for (int p = start; p < end; p++) { - setPixelColor(p, c.r, c.g, c.b); + SEGMENT.setPixelColor(p, c.r, c.g, c.b); } } } @@ -3039,113 +3339,129 @@ uint16_t WS2812FX::mode_starburst(void) { return FRAMETIME; } #undef STARBURST_MAX_FRAG +static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; + /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ + * adapted for 2D WLED by blazoncek (Blaz Kristan (AKA blazoncek)) */ -uint16_t WS2812FX::mode_exploding_fireworks(void) +uint16_t mode_exploding_fireworks(void) { + if (SEGLEN == 1) return mode_static(); + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + //allocate segment data uint16_t maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 - uint8_t segs = getActiveSegmentsNum(); - if (segs <= (MAX_NUM_SEGMENTS /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs - if (segs <= (MAX_NUM_SEGMENTS /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs + uint8_t segs = strip.getActiveSegmentsNum(); + if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs + if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs int maxSparks = maxData / sizeof(spark); //ESP8266: max. 21/42/85 sparks/seg, ESP32: max. 53/106/213 sparks/seg - uint16_t numSparks = min(2 + (SEGLEN >> 1), maxSparks); + uint16_t numSparks = min(2 + ((rows*cols) >> 1), maxSparks); uint16_t dataSize = sizeof(spark) * numSparks; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize + sizeof(float))) return mode_static(); //allocation failed + float *dying_gravity = reinterpret_cast(SEGENV.data + dataSize); - if (dataSize != SEGENV.aux1) { //reset to flare if sparks were reallocated + if (dataSize != SEGENV.aux1) { //reset to flare if sparks were reallocated (it may be good idea to reset segment if bounds change) + *dying_gravity = 0.0f; SEGENV.aux0 = 0; SEGENV.aux1 = dataSize; } - fill(BLACK); - - bool actuallyReverse = SEGMENT.getOption(SEG_OPTION_REVERSED); - //have fireworks start in either direction based on intensity - SEGMENT.setOption(SEG_OPTION_REVERSED, SEGENV.step); - + SEGMENT.fade_out(252); + Spark* sparks = reinterpret_cast(SEGENV.data); Spark* flare = sparks; //first spark is flare data - float gravity = -0.0004 - (SEGMENT.speed/800000.0); // m/s/s - gravity *= SEGLEN; - + float gravity = -0.0004f - (SEGMENT.speed/800000.0f); // m/s/s + gravity *= rows; + if (SEGENV.aux0 < 2) { //FLARE if (SEGENV.aux0 == 0) { //init flare flare->pos = 0; + flare->posX = strip.isMatrix ? random16(2,cols-3) : (SEGMENT.intensity > random8()); // will enable random firing side on 1D uint16_t peakHeight = 75 + random8(180); //0-255 - peakHeight = (peakHeight * (SEGLEN -1)) >> 8; - flare->vel = sqrt(-2.0 * gravity * peakHeight); + peakHeight = (peakHeight * (rows -1)) >> 8; + flare->vel = sqrtf(-2.0f * gravity * peakHeight); + flare->velX = strip.isMatrix ? (random8(9)-4)/32.f : 0; // no X velocity on 1D flare->col = 255; //brightness - - SEGENV.aux0 = 1; + SEGENV.aux0 = 1; } - - // launch + + // launch if (flare->vel > 12 * gravity) { // flare - setPixelColor(int(flare->pos),flare->col,flare->col,flare->col); - - flare->pos += flare->vel; - flare->pos = constrain(flare->pos, 0, SEGLEN-1); - flare->vel += gravity; - flare->col -= 2; + if (strip.isMatrix) SEGMENT.setPixelColorXY(int(flare->posX), rows - uint16_t(flare->pos) - 1, flare->col, flare->col, flare->col); + else SEGMENT.setPixelColor(int(flare->posX) ? rows - int(flare->pos) - 1 : int(flare->pos), flare->col, flare->col, flare->col); + flare->pos += flare->vel; + flare->posX += flare->velX; + flare->pos = constrain(flare->pos, 0, rows-1); + flare->posX = constrain(flare->posX, 0, cols-strip.isMatrix); + flare->vel += gravity; + flare->col -= 2; } else { SEGENV.aux0 = 2; // ready to explode } } else if (SEGENV.aux0 < 4) { /* * Explode! - * + * * Explosion happens where the flare ended. * Size is proportional to the height. */ - int nSparks = flare->pos; - nSparks = constrain(nSparks, 0, numSparks); - static float dying_gravity; - + int nSparks = flare->pos + random8(4); + nSparks = constrain(nSparks, 4, numSparks); + // initialize sparks if (SEGENV.aux0 == 2) { - for (int i = 1; i < nSparks; i++) { - sparks[i].pos = flare->pos; - sparks[i].vel = (float(random16(0, 20000)) / 10000.0) - 0.9; // from -0.9 to 1.1 - sparks[i].col = 345;//abs(sparks[i].vel * 750.0); // set colors before scaling velocity to keep them bright - //sparks[i].col = constrain(sparks[i].col, 0, 345); + for (int i = 1; i < nSparks; i++) { + sparks[i].pos = flare->pos; + sparks[i].posX = flare->posX; + sparks[i].vel = (float(random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1 + sparks[i].vel *= rows<32 ? 0.5f : 1; // reduce velocity for smaller strips + sparks[i].velX = strip.isMatrix ? (float(random16(10001)) / 10000.0f) - 0.5f : 0; // from -0.5 to 0.5 + sparks[i].col = 345;//abs(sparks[i].vel * 750.0); // set colors before scaling velocity to keep them bright + //sparks[i].col = constrain(sparks[i].col, 0, 345); sparks[i].colIndex = random8(); - sparks[i].vel *= flare->pos/SEGLEN; // proportional to height - sparks[i].vel *= -gravity *50; - } - //sparks[1].col = 345; // this will be our known spark - dying_gravity = gravity/2; + sparks[i].vel *= flare->pos/rows; // proportional to height + sparks[i].velX *= strip.isMatrix ? flare->posX/cols : 0; // proportional to width + sparks[i].vel *= -gravity *50; + } + //sparks[1].col = 345; // this will be our known spark + *dying_gravity = gravity/2; SEGENV.aux0 = 3; } - - if (sparks[1].col > 4) {//&& sparks[1].pos > 0) { // as long as our known spark is lit, work with all the sparks - for (int i = 1; i < nSparks; i++) { - sparks[i].pos += sparks[i].vel; - sparks[i].vel += dying_gravity; - if (sparks[i].col > 3) sparks[i].col -= 4; - if (sparks[i].pos > 0 && sparks[i].pos < SEGLEN) { + if (sparks[1].col > 4) {//&& sparks[1].pos > 0) { // as long as our known spark is lit, work with all the sparks + for (int i = 1; i < nSparks; i++) { + sparks[i].pos += sparks[i].vel; + sparks[i].posX += sparks[i].velX; + sparks[i].vel += *dying_gravity; + sparks[i].velX += strip.isMatrix ? *dying_gravity : 0; + if (sparks[i].col > 3) sparks[i].col -= 4; + + if (sparks[i].pos > 0 && sparks[i].pos < rows) { + if (strip.isMatrix && !(sparks[i].posX >= 0 && sparks[i].posX < cols)) continue; uint16_t prog = sparks[i].col; - uint32_t spColor = (SEGMENT.palette) ? color_wheel(sparks[i].colIndex) : SEGCOLOR(0); + uint32_t spColor = (SEGMENT.palette) ? SEGMENT.color_wheel(sparks[i].colIndex) : SEGCOLOR(0); CRGB c = CRGB::Black; //HeatColor(sparks[i].col); if (prog > 300) { //fade from white to spark color - c = col_to_crgb(color_blend(spColor, WHITE, (prog - 300)*5)); + c = CRGB(color_blend(spColor, WHITE, (prog - 300)*5)); } else if (prog > 45) { //fade from spark color to black - c = col_to_crgb(color_blend(BLACK, spColor, prog - 45)); + c = CRGB(color_blend(BLACK, spColor, prog - 45)); uint8_t cooling = (300 - prog) >> 5; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } - setPixelColor(int(sparks[i].pos), c.red, c.green, c.blue); + if (strip.isMatrix) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c.red, c.green, c.blue); + else SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c.red, c.green, c.blue); } } - dying_gravity *= .99; // as sparks burn out they fall slower + SEGMENT.blur(16); + *dying_gravity *= .8f; // as sparks burn out they fall slower } else { SEGENV.aux0 = 6 + random8(10); //wait for this many frames } @@ -3153,152 +3469,198 @@ uint16_t WS2812FX::mode_exploding_fireworks(void) SEGENV.aux0--; if (SEGENV.aux0 < 4) { SEGENV.aux0 = 0; //back to flare - SEGENV.step = actuallyReverse ^ (SEGMENT.intensity > random8()); //decide firing side } } - SEGMENT.setOption(SEG_OPTION_REVERSED, actuallyReverse); - - return FRAMETIME; + return FRAMETIME; } #undef MAX_SPARKS +static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; /* * Drip Effect * ported of: https://www.youtube.com/watch?v=sru2fXh4r7k */ -uint16_t WS2812FX::mode_drip(void) +uint16_t mode_drip(void) { + if (SEGLEN == 1) return mode_static(); //allocate segment data - uint8_t numDrops = 4; - uint16_t dataSize = sizeof(spark) * numDrops; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - - fill(SEGCOLOR(1)); - + uint16_t strips = SEGMENT.nrOfVStrips(); + const int maxNumDrops = 4; + uint16_t dataSize = sizeof(spark) * maxNumDrops; + if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Spark* drops = reinterpret_cast(SEGENV.data); - numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 - - float gravity = -0.0005 - (SEGMENT.speed/50000.0); - gravity *= SEGLEN; - int sourcedrop = 12; - - for (uint8_t j=0;j255) drops[j].col=255; - setPixelColor(uint16_t(drops[j].pos),color_blend(BLACK,SEGCOLOR(0),drops[j].col)); - - drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling - - if (random8() < drops[j].col/10) { // random drop - drops[j].colIndex=2; //fall - drops[j].col=255; - } - } - if (drops[j].colIndex > 1) { // falling - if (drops[j].pos > 0) { // fall until end of segment - drops[j].pos += drops[j].vel; - if (drops[j].pos < 0) drops[j].pos = 0; - drops[j].vel += gravity; // gravity is negative - - for (uint16_t i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets - uint16_t pos = constrain(uint16_t(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally - setPixelColor(pos,color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling - } + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - if (drops[j].colIndex > 2) { // during bounce, some water is on the floor - setPixelColor(0,color_blend(SEGCOLOR(0),BLACK,drops[j].col)); + struct virtualStrip { + static void runStrip(uint16_t stripNr, Spark* drops) { + + uint8_t numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 + + float gravity = -0.0005 - (SEGMENT.speed/50000.0); + gravity *= max(1, SEGLEN-1); + int sourcedrop = 12; + + for (int j=0;j 2) { // already hit once, so back to forming - drops[j].colIndex = 0; - drops[j].col = sourcedrop; - - } else { - if (drops[j].colIndex==2) { // init bounce - drops[j].vel = -drops[j].vel/4;// reverse velocity with damping + SEGMENT.setPixelColor(indexToVStrip(SEGLEN-1, stripNr), color_blend(BLACK,SEGCOLOR(0), sourcedrop));// water source + if (drops[j].colIndex==1) { + if (drops[j].col>255) drops[j].col=255; + SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col)); + + drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling + + if (random8() < drops[j].col/10) { // random drop + drops[j].colIndex=2; //fall + drops[j].col=255; + } + } + if (drops[j].colIndex > 1) { // falling + if (drops[j].pos > 0) { // fall until end of segment drops[j].pos += drops[j].vel; - } - drops[j].col = sourcedrop*2; - drops[j].colIndex = 5; // bouncing + if (drops[j].pos < 0) drops[j].pos = 0; + drops[j].vel += gravity; // gravity is negative + + for (int i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets + uint16_t pos = constrain(uint16_t(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally + SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling + } + + if (drops[j].colIndex > 2) { // during bounce, some water is on the floor + SEGMENT.setPixelColor(indexToVStrip(0, stripNr), color_blend(SEGCOLOR(0),BLACK,drops[j].col)); + } + } else { // we hit bottom + if (drops[j].colIndex > 2) { // already hit once, so back to forming + drops[j].colIndex = 0; + drops[j].col = sourcedrop; + + } else { + + if (drops[j].colIndex==2) { // init bounce + drops[j].vel = -drops[j].vel/4;// reverse velocity with damping + drops[j].pos += drops[j].vel; + } + drops[j].col = sourcedrop*2; + drops[j].colIndex = 5; // bouncing + } + } } } } - } - return FRAMETIME; + }; + + for (int stripNr=0; stripNr(SEGENV.data); + if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed + Tetris* drops = reinterpret_cast(SEGENV.data); + + //if (SEGENV.call == 0) SEGMENT.fill(SEGCOLOR(1)); // will fill entire segment (1D or 2D), then use drop->step = 0 below + + // virtualStrip idea by @ewowi (Ewoud Wijma) + // requires virtual strip # to be embedded into upper 16 bits of index in setPixelcolor() + // the following functions will not work on virtual strips: fill(), fade_out(), fadeToBlack(), blur() + struct virtualStrip { + static void runStrip(size_t stripNr, Tetris *drop) { + // initialize dropping on first call or segment full + if (SEGENV.call == 0) { + drop->stack = 0; // reset brick stack size + drop->step = millis() + 2000; // start by fading out strip + if (SEGMENT.check1) drop->col = 0;// use only one color from palette + } - // initialize dropping on first call or segment full - if (SEGENV.call == 0 || SEGENV.aux1 >= SEGLEN) { - SEGENV.aux1 = 0; // reset brick stack size - SEGENV.step = 0; - fill(SEGCOLOR(1)); - return 250; // short wait - } - - if (SEGENV.step == 0) { //init - drop->speed = 0.0238 * (SEGMENT.speed ? (SEGMENT.speed>>2)+1 : random8(6,64)); // set speed - drop->pos = SEGLEN; // start at end of segment (no need to subtract 1) - drop->col = color_from_palette(random8(0,15)<<4,false,false,0); // limit color choices so there is enough HUE gap - SEGENV.step = 1; // drop state (0 init, 1 forming, 2 falling) - SEGENV.aux0 = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : random8(1,5)) * (1+(SEGLEN>>6)); // size of brick - } - - if (SEGENV.step == 1) { // forming - if (random8()>>6) { // random drop - SEGENV.step = 2; // fall - } - } + if (drop->step == 0) { // init brick + // speed calculation: a single brick should reach bottom of strip in X seconds + // if the speed is set to 1 this should take 5s and at 255 it should take 0.25s + // as this is dependant on SEGLEN it should be taken into account and the fact that effect runs every FRAMETIME s + int speed = SEGMENT.speed ? SEGMENT.speed : random8(1,255); + speed = map(speed, 1, 255, 5000, 250); // time taken for full (SEGLEN) drop + drop->speed = float(SEGLEN * FRAMETIME) / float(speed); // set speed + drop->pos = SEGLEN; // start at end of segment (no need to subtract 1) + if (!SEGMENT.check1) drop->col = random8(0,15)<<4; // limit color choices so there is enough HUE gap + drop->step = 1; // drop state (0 init, 1 forming, 2 falling) + drop->brick = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : random8(1,5)) * (1+(SEGLEN>>6)); // size of brick + } + + if (drop->step == 1) { // forming + if (random8()>>6) { // random drop + drop->step = 2; // fall + } + } + + if (drop->step == 2) { // falling + if (drop->pos > drop->stack) { // fall until top of stack + drop->pos -= drop->speed; // may add gravity as: speed += gravity + if (int(drop->pos) < int(drop->stack)) drop->pos = drop->stack; + for (int i = int(drop->pos); i < SEGLEN; i++) { + uint32_t col = ipos)+drop->brick ? SEGMENT.color_from_palette(drop->col, false, false, 0) : SEGCOLOR(1); + SEGMENT.setPixelColor(indexToVStrip(i, stripNr), col); + } + } else { // we hit bottom + drop->step = 0; // proceed with next brick, go back to init + drop->stack += drop->brick; // increase the stack size + if (drop->stack >= SEGLEN) drop->step = millis() + 2000; // fade out stack + } + } - if (SEGENV.step > 1) { // falling - if (drop->pos > SEGENV.aux1) { // fall until top of stack - drop->pos -= drop->speed; // may add gravity as: speed += gravity - if (int(drop->pos) < SEGENV.aux1) drop->pos = SEGENV.aux1; - for (uint16_t i=int(drop->pos); ipos)+SEGENV.aux0 ? drop->col : SEGCOLOR(1)); - } else { // we hit bottom - SEGENV.step = 0; // go back to init - SEGENV.aux1 += SEGENV.aux0; // increase the stack size - if (SEGENV.aux1 >= SEGLEN) return 1000; // wait for a second + if (drop->step > 2) { // fade strip + drop->brick = 0; // reset brick size (no more growing) + if (drop->step > millis()) { + // allow fading of virtual strip + for (int i = 0; i < SEGLEN; i++) SEGMENT.blendPixelColor(indexToVStrip(i, stripNr), SEGCOLOR(1), 25); // 10% blend + } else { + drop->stack = 0; // reset brick stack size + drop->step = 0; // proceed with next brick + if (SEGMENT.check1) drop->col += 8; // gradually increase palette index + } + } } - } - return FRAMETIME; + }; + + for (int stripNr=0; stripNr> 5))+thisPhase) & 0xFF)/2 // factor=23 // Create a wave and add a phase change and add another wave with its own phase change. + cos8((i*(1+ 2*(SEGMENT.speed >> 5))+thatPhase) & 0xFF)/2; // factor=15 // Hey, you can even change the frequencies if you wish. uint8_t thisBright = qsub8(colorIndex, beatsin8(7,0, (128 - (SEGMENT.intensity>>1)))); - CRGB color = ColorFromPalette(currentPalette, colorIndex, thisBright, LINEARBLEND); - setPixelColor(i, color.red, color.green, color.blue); + //CRGB color = ColorFromPalette(SEGPALETTE, colorIndex, thisBright, LINEARBLEND); + //SEGMENT.setPixelColor(i, color.red, color.green, color.blue); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0, thisBright)); } return FRAMETIME; -} +} +static const char _data_FX_MODE_PLASMA[] PROGMEM = "Plasma@Phase,!;!;!"; /* * Percentage display - * Intesity values from 0-100 turn on the leds. + * Intensity values from 0-100 turn on the leds. */ -uint16_t WS2812FX::mode_percent(void) { +uint16_t mode_percent(void) { + + uint8_t percent = SEGMENT.intensity; + percent = constrain(percent, 0, 200); + uint16_t active_leds = (percent < 100) ? roundf(SEGLEN * percent / 100.0f) + : roundf(SEGLEN * (200 - percent) / 100.0f); - uint8_t percent = MAX(0, MIN(200, SEGMENT.intensity)); - uint16_t active_leds = (percent < 100) ? SEGLEN * percent / 100.0 - : SEGLEN * (200 - percent) / 100.0; - uint8_t size = (1 + ((SEGMENT.speed * SEGLEN) >> 11)); if (SEGMENT.speed == 255) size = 255; - - if (percent < 100) { - for (uint16_t i = 0; i < SEGLEN; i++) { - if (i < SEGENV.step) { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); - } - else { - setPixelColor(i, SEGCOLOR(1)); - } - } + + if (percent <= 100) { + for (int i = 0; i < SEGLEN; i++) { + if (i < SEGENV.aux1) { + if (SEGMENT.check1) + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(map(percent,0,100,0,255), false, false, 0)); + else + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); + } + else { + SEGMENT.setPixelColor(i, SEGCOLOR(1)); + } + } } else { - for (uint16_t i = 0; i < SEGLEN; i++) { - if (i < (SEGLEN - SEGENV.step)) { - setPixelColor(i, SEGCOLOR(1)); - } - else { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); - } - } + for (int i = 0; i < SEGLEN; i++) { + if (i < (SEGLEN - SEGENV.aux1)) { + SEGMENT.setPixelColor(i, SEGCOLOR(1)); + } + else { + if (SEGMENT.check1) + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(map(percent,100,200,255,0), false, false, 0)); + else + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); + } + } } - if(active_leds > SEGENV.step) { // smooth transition to the target value - SEGENV.step += size; - if (SEGENV.step > active_leds) SEGENV.step = active_leds; - } else if (active_leds < SEGENV.step) { - if (SEGENV.step > size) SEGENV.step -= size; else SEGENV.step = 0; - if (SEGENV.step < active_leds) SEGENV.step = active_leds; + if(active_leds > SEGENV.aux1) { // smooth transition to the target value + SEGENV.aux1 += size; + if (SEGENV.aux1 > active_leds) SEGENV.aux1 = active_leds; + } else if (active_leds < SEGENV.aux1) { + if (SEGENV.aux1 > size) SEGENV.aux1 -= size; else SEGENV.aux1 = 0; + if (SEGENV.aux1 < active_leds) SEGENV.aux1 = active_leds; } return FRAMETIME; } +static const char _data_FX_MODE_PERCENT[] PROGMEM = "Percent@,% of fill,,,,One color;!,!;!"; + /* -/ Modulates the brightness similar to a heartbeat -*/ -uint16_t WS2812FX::mode_heartbeat(void) { - uint8_t bpm = 40 + (SEGMENT.speed >> 4); - uint32_t msPerBeat = (60000 / bpm); + * Modulates the brightness similar to a heartbeat + * (unimplemented?) tries to draw an ECG approximation on a 2D matrix + */ +uint16_t mode_heartbeat(void) { + uint8_t bpm = 40 + (SEGMENT.speed >> 3); + uint32_t msPerBeat = (60000L / bpm); uint32_t secondBeat = (msPerBeat / 3); - uint32_t bri_lower = SEGENV.aux1; + unsigned long beatTimer = strip.now - SEGENV.step; + bri_lower = bri_lower * 2042 / (2048 + SEGMENT.intensity); SEGENV.aux1 = bri_lower; - unsigned long beatTimer = millis() - SEGENV.step; - if((beatTimer > secondBeat) && !SEGENV.aux0) { // time for the second beat? - SEGENV.aux1 = UINT16_MAX; //full bri + if ((beatTimer > secondBeat) && !SEGENV.aux0) { // time for the second beat? + SEGENV.aux1 = UINT16_MAX; //3/4 bri SEGENV.aux0 = 1; } - if(beatTimer > msPerBeat) { // time to reset the beat timer? + if (beatTimer > msPerBeat) { // time to reset the beat timer? SEGENV.aux1 = UINT16_MAX; //full bri SEGENV.aux0 = 0; - SEGENV.step = millis(); + SEGENV.step = strip.now; } - for (uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_blend(color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255 - (SEGENV.aux1 >> 8))); + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255 - (SEGENV.aux1 >> 8))); } return FRAMETIME; } +static const char _data_FX_MODE_HEARTBEAT[] PROGMEM = "Heartbeat@!,!;!,!;!;01;m12=1"; // "Pacifica" @@ -3399,16 +3774,16 @@ uint16_t WS2812FX::mode_heartbeat(void) { // For Dan. // // -// In this animation, there are four "layers" of waves of light. +// In this animation, there are four "layers" of waves of light. // // Each layer moves independently, and each is scaled separately. // -// All four wave layers are added together on top of each other, and then -// another filter is applied that adds "whitecaps" of brightness where the +// All four wave layers are added together on top of each other, and then +// another filter is applied that adds "whitecaps" of brightness where the // waves line up with each other more. Finally, another pass is taken // over the led array to 'deepen' (dim) the blues and greens. // -// The speed and scale and motion each layer varies slowly within independent +// The speed and scale and motion each layer varies slowly within independent // hand-chosen ranges, which is why the code has a lot of low-speed 'beatsin8' functions // with a lot of oddly specific numeric ranges. // @@ -3417,34 +3792,48 @@ uint16_t WS2812FX::mode_heartbeat(void) { // // Modified for WLED, based on https://github.com/FastLED/FastLED/blob/master/examples/Pacifica/Pacifica.ino // -uint16_t WS2812FX::mode_pacifica() +// Add one layer of waves into the led array +CRGB pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) +{ + uint16_t ci = cistart; + uint16_t waveangle = ioff; + uint16_t wavescale_half = (wavescale >> 1) + 20; + + waveangle += ((120 + SEGMENT.intensity) * i); //original 250 * i + uint16_t s16 = sin16(waveangle) + 32768; + uint16_t cs = scale16(s16, wavescale_half) + wavescale_half; + ci += (cs * i); + uint16_t sindex16 = sin16(ci) + 32768; + uint8_t sindex8 = scale16(sindex16, 240); + return ColorFromPalette(p, sindex8, bri, LINEARBLEND); +} + +uint16_t mode_pacifica() { - uint32_t nowOld = now; + uint32_t nowOld = strip.now; - CRGBPalette16 pacifica_palette_1 = - { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117, + CRGBPalette16 pacifica_palette_1 = + { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117, 0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x14554B, 0x28AA50 }; - CRGBPalette16 pacifica_palette_2 = - { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117, + CRGBPalette16 pacifica_palette_2 = + { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117, 0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x0C5F52, 0x19BE5F }; - CRGBPalette16 pacifica_palette_3 = - { 0x000208, 0x00030E, 0x000514, 0x00061A, 0x000820, 0x000927, 0x000B2D, 0x000C33, + CRGBPalette16 pacifica_palette_3 = + { 0x000208, 0x00030E, 0x000514, 0x00061A, 0x000820, 0x000927, 0x000B2D, 0x000C33, 0x000E39, 0x001040, 0x001450, 0x001860, 0x001C70, 0x002080, 0x1040BF, 0x2060FF }; if (SEGMENT.palette) { - pacifica_palette_1 = currentPalette; - pacifica_palette_2 = currentPalette; - pacifica_palette_3 = currentPalette; + pacifica_palette_1 = SEGPALETTE; + pacifica_palette_2 = SEGPALETTE; + pacifica_palette_3 = SEGPALETTE; } // Increment the four "color index start" counters, one for each wave layer. // Each is incremented at a different speed, and the speeds vary over time. uint16_t sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step, sCIStart4 = SEGENV.step >> 16; - //static uint16_t sCIStart1, sCIStart2, sCIStart3, sCIStart4; - //uint32_t deltams = 26 + (SEGMENT.speed >> 3); uint32_t deltams = (FRAMETIME >> 2) + ((FRAMETIME * SEGMENT.speed) >> 7); - uint64_t deltat = (now >> 2) + ((now * SEGMENT.speed) >> 7); - now = deltat; + uint64_t deltat = (strip.now >> 2) + ((strip.now * SEGMENT.speed) >> 7); + strip.now = deltat; uint16_t speedfactor1 = beatsin16(3, 179, 269); uint16_t speedfactor2 = beatsin16(4, 179, 269); @@ -3459,19 +3848,19 @@ uint16_t WS2812FX::mode_pacifica() SEGENV.step = sCIStart4; SEGENV.step = (SEGENV.step << 16) + sCIStart3; // Clear out the LED array to a dim background blue-green - //fill(132618); + //SEGMENT.fill(132618); uint8_t basethreshold = beatsin8( 9, 55, 65); uint8_t wave = beat8( 7 ); - - for( uint16_t i = 0; i < SEGLEN; i++) { + + for (int i = 0; i < SEGLEN; i++) { CRGB c = CRGB(2, 6, 10); // Render each of four layers, with different scales and speeds, that vary over time c += pacifica_one_layer(i, pacifica_palette_1, sCIStart1, beatsin16(3, 11 * 256, 14 * 256), beatsin8(10, 70, 130), 0-beat16(301)); c += pacifica_one_layer(i, pacifica_palette_2, sCIStart2, beatsin16(4, 6 * 256, 9 * 256), beatsin8(17, 40, 80), beat16(401)); c += pacifica_one_layer(i, pacifica_palette_3, sCIStart3, 6 * 256 , beatsin8(9, 10,38) , 0-beat16(503)); c += pacifica_one_layer(i, pacifica_palette_3, sCIStart4, 5 * 256 , beatsin8(8, 10,28) , beat16(601)); - + // Add extra 'white' to areas where the four layers of light have lined up brightly uint8_t threshold = scale8( sin8( wave), 20) + basethreshold; wave += 7; @@ -3483,121 +3872,94 @@ uint16_t WS2812FX::mode_pacifica() } //deepen the blues and greens - c.blue = scale8(c.blue, 145); - c.green = scale8(c.green, 200); + c.blue = scale8(c.blue, 145); + c.green = scale8(c.green, 200); c |= CRGB( 2, 5, 7); - setPixelColor(i, c.red, c.green, c.blue); + SEGMENT.setPixelColor(i, c.red, c.green, c.blue); } - now = nowOld; - return FRAMETIME; -} - -// Add one layer of waves into the led array -CRGB WS2812FX::pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) -{ - uint16_t ci = cistart; - uint16_t waveangle = ioff; - uint16_t wavescale_half = (wavescale >> 1) + 20; - - waveangle += ((120 + SEGMENT.intensity) * i); //original 250 * i - uint16_t s16 = sin16(waveangle) + 32768; - uint16_t cs = scale16(s16, wavescale_half) + wavescale_half; - ci += (cs * i); - uint16_t sindex16 = sin16(ci) + 32768; - uint8_t sindex8 = scale16(sindex16, 240); - return ColorFromPalette(p, sindex8, bri, LINEARBLEND); -} - -//Solid colour background with glitter -uint16_t WS2812FX::mode_solid_glitter() -{ - fill(SEGCOLOR(0)); - - if (SEGMENT.intensity > random8()) - { - setPixelColor(random16(SEGLEN), ULTRAWHITE); - } + strip.now = nowOld; return FRAMETIME; } +static const char _data_FX_MODE_PACIFICA[] PROGMEM = "Pacifica@!,Angle;;!;;pal=51"; /* * Mode simulates a gradual sunrise */ -uint16_t WS2812FX::mode_sunrise() { +uint16_t mode_sunrise() { + if (SEGLEN == 1) return mode_static(); //speed 0 - static sun //speed 1 - 60: sunrise time in minutes //speed 60 - 120 : sunset time in minutes - 60; //speed above: "breathing" rise and set if (SEGENV.call == 0 || SEGMENT.speed != SEGENV.aux0) { - SEGENV.step = millis(); //save starting time, millis() because now can change from sync + SEGENV.step = millis(); //save starting time, millis() because now can change from sync SEGENV.aux0 = SEGMENT.speed; } - - fill(0); + + SEGMENT.fill(BLACK); uint16_t stage = 0xFFFF; - + uint32_t s10SinceStart = (millis() - SEGENV.step) /100; //tenths of seconds - + if (SEGMENT.speed > 120) { //quick sunrise and sunset - uint16_t counter = (now >> 1) * (((SEGMENT.speed -120) >> 1) +1); - stage = triwave16(counter); + uint16_t counter = (strip.now >> 1) * (((SEGMENT.speed -120) >> 1) +1); + stage = triwave16(counter); } else if (SEGMENT.speed) { //sunrise - uint8_t durMins = SEGMENT.speed; - if (durMins > 60) durMins -= 60; - uint32_t s10Target = durMins * 600; - if (s10SinceStart > s10Target) s10SinceStart = s10Target; - stage = map(s10SinceStart, 0, s10Target, 0, 0xFFFF); - if (SEGMENT.speed > 60) stage = 0xFFFF - stage; //sunset + uint8_t durMins = SEGMENT.speed; + if (durMins > 60) durMins -= 60; + uint32_t s10Target = durMins * 600; + if (s10SinceStart > s10Target) s10SinceStart = s10Target; + stage = map(s10SinceStart, 0, s10Target, 0, 0xFFFF); + if (SEGMENT.speed > 60) stage = 0xFFFF - stage; //sunset } - - for (uint16_t i = 0; i <= SEGLEN/2; i++) + + for (int i = 0; i <= SEGLEN/2; i++) { //default palette is Fire - uint32_t c = color_from_palette(0, false, true, 255); //background + uint32_t c = SEGMENT.color_from_palette(0, false, true, 255); //background uint16_t wave = triwave16((i * stage) / SEGLEN); wave = (wave >> 8) + ((wave * SEGMENT.intensity) >> 15); if (wave > 240) { //clipped, full white sun - c = color_from_palette( 240, false, true, 255); + c = SEGMENT.color_from_palette( 240, false, true, 255); } else { //transition - c = color_from_palette(wave, false, true, 255); + c = SEGMENT.color_from_palette(wave, false, true, 255); } - setPixelColor(i, c); - setPixelColor(SEGLEN - i - 1, c); + SEGMENT.setPixelColor(i, c); + SEGMENT.setPixelColor(SEGLEN - i - 1, c); } return FRAMETIME; } +static const char _data_FX_MODE_SUNRISE[] PROGMEM = "Sunrise@Time [min],Width;;!;;sx=60"; /* * Effects by Andrew Tuline */ -uint16_t WS2812FX::phased_base(uint8_t moder) { // We're making sine waves here. By Andrew Tuline. +uint16_t phased_base(uint8_t moder) { // We're making sine waves here. By Andrew Tuline. uint8_t allfreq = 16; // Base frequency. - //float* phasePtr = reinterpret_cast(SEGENV.step); // Phase change value gets calculated. - static float phase = 0;//phasePtr[0]; + float *phase = reinterpret_cast(&SEGENV.step); // Phase change value gets calculated (float fits into unsigned long). uint8_t cutOff = (255-SEGMENT.intensity); // You can change the number of pixels. AKA INTENSITY (was 192). uint8_t modVal = 5;//SEGMENT.fft1/8+1; // You can change the modulus. AKA FFT1 (was 5). - uint8_t index = now/64; // Set color rotation speed - phase += SEGMENT.speed/32.0; // You can change the speed of the wave. AKA SPEED (was .4) - //phasePtr[0] = phase; + uint8_t index = strip.now/64; // Set color rotation speed + *phase += SEGMENT.speed/32.0; // You can change the speed of the wave. AKA SPEED (was .4) for (int i = 0; i < SEGLEN; i++) { if (moder == 1) modVal = (inoise8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. - uint16_t val = (i+1) * allfreq; // This sets the frequency of the waves. The +1 makes sure that leds[0] is used. + uint16_t val = (i+1) * allfreq; // This sets the frequency of the waves. The +1 makes sure that led 0 is used. if (modVal == 0) modVal = 1; - val += phase * (i % modVal +1) /2; // This sets the varying phase change of the waves. By Andrew Tuline. + val += *phase * (i % modVal +1) /2; // This sets the varying phase change of the waves. By Andrew Tuline. uint8_t b = cubicwave8(val); // Now we make an 8 bit sinewave. b = (b > cutOff) ? (b - cutOff) : 0; // A ternary operator to cutoff the light. - setPixelColor(i, color_blend(SEGCOLOR(1), color_from_palette(index, false, false, 0), b)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), b)); index += 256 / SEGLEN; if (SEGLEN > 256) index ++; // Correction for segments longer than 256 LEDs } @@ -3606,35 +3968,35 @@ uint16_t WS2812FX::phased_base(uint8_t moder) { // We're making } - -uint16_t WS2812FX::mode_phased(void) { +uint16_t mode_phased(void) { return phased_base(0); } +static const char _data_FX_MODE_PHASED[] PROGMEM = "Phased@!,!;!,!;!"; - -uint16_t WS2812FX::mode_phased_noise(void) { +uint16_t mode_phased_noise(void) { return phased_base(1); } +static const char _data_FX_MODE_PHASEDNOISE[] PROGMEM = "Phased Noise@!,!;!,!;!"; +uint16_t mode_twinkleup(void) { // A very short twinkle routine with fade-in and dual controls. By Andrew Tuline. + random16_set_seed(535); // The randomizer needs to be re-set each time through the loop in order for the same 'random' numbers to be the same each time through. -uint16_t WS2812FX::mode_twinkleup(void) { // A very short twinkle routine with fade-in and dual controls. By Andrew Tuline. - random16_set_seed(535); // The randomizer needs to be re-set each time through the loop in order for the same 'random' numbers to be the same each time through. - - for (int i = 0; i SEGMENT.intensity) pixBri = 0; - setPixelColor(i, color_blend(SEGCOLOR(1), color_from_palette(random8()+now/100, false, PALETTE_SOLID_WRAP, 0), pixBri)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(random8()+strip.now/100, false, PALETTE_SOLID_WRAP, 0), pixBri)); } return FRAMETIME; } +static const char _data_FX_MODE_TWINKLEUP[] PROGMEM = "Twinkleup@!,Intensity;!,!;!;;m12=0"; // Peaceful noise that's slow and with gradually changing palettes. Does not support WLED palettes or default colours or controls. -uint16_t WS2812FX::mode_noisepal(void) { // Slow noise palette by Andrew Tuline. +uint16_t mode_noisepal(void) { // Slow noise palette by Andrew Tuline. uint16_t scale = 15 + (SEGMENT.intensity >> 2); //default was 30 //#define scale 30 @@ -3657,53 +4019,55 @@ uint16_t WS2812FX::mode_noisepal(void) { // S //EVERY_N_MILLIS(10) { //(don't have to time this, effect function is only called every 24ms) nblendPaletteTowardPalette(palettes[0], palettes[1], 48); // Blend towards the target palette over 48 iterations. - if (SEGMENT.palette > 0) palettes[0] = currentPalette; + if (SEGMENT.palette > 0) palettes[0] = SEGPALETTE; - for(int i = 0; i < SEGLEN; i++) { + for (int i = 0; i < SEGLEN; i++) { uint8_t index = inoise8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. color = ColorFromPalette(palettes[0], index, 255, LINEARBLEND); // Use the my own palette. - setPixelColor(i, color.red, color.green, color.blue); + SEGMENT.setPixelColor(i, color.red, color.green, color.blue); } SEGENV.aux0 += beatsin8(10,1,4); // Moving along the distance. Vary it a bit with a sine wave. return FRAMETIME; } +static const char _data_FX_MODE_NOISEPAL[] PROGMEM = "Noise Pal@!,Scale;;!"; // Sine waves that have controllable phase change speed, frequency and cutoff. By Andrew Tuline. // SEGMENT.speed ->Speed, SEGMENT.intensity -> Frequency (SEGMENT.fft1 -> Color change, SEGMENT.fft2 -> PWM cutoff) // -uint16_t WS2812FX::mode_sinewave(void) { // Adjustable sinewave. By Andrew Tuline +uint16_t mode_sinewave(void) { // Adjustable sinewave. By Andrew Tuline //#define qsuba(x, b) ((x>b)?x-b:0) // Analog Unsigned subtraction macro. if result <0, then => 0 - uint16_t colorIndex = now /32;//(256 - SEGMENT.fft1); // Amount of colour change. + uint16_t colorIndex = strip.now /32;//(256 - SEGMENT.fft1); // Amount of colour change. SEGENV.step += SEGMENT.speed/16; // Speed of animation. uint16_t freq = SEGMENT.intensity/4;//SEGMENT.fft2/8; // Frequency of the signal. - for (int i=0; i> 2) +1); + counter = strip.now * ((SEGMENT.speed >> 2) +1); counter = counter >> 8; } - + uint16_t maxZones = SEGLEN / 6; //only looks good if each zone has at least 6 LEDs uint16_t zones = (SEGMENT.intensity * maxZones) >> 8; if (zones & 0x01) zones++; //zones must be even @@ -3711,45 +4075,50 @@ uint16_t WS2812FX::mode_flow(void) uint16_t zoneLen = SEGLEN / zones; uint16_t offset = (SEGLEN - zones * zoneLen) >> 1; - fill(color_from_palette(-counter, false, true, 255)); + SEGMENT.fill(SEGMENT.color_from_palette(-counter, false, true, 255)); - for (uint16_t z = 0; z < zones; z++) + for (int z = 0; z < zones; z++) { uint16_t pos = offset + z * zoneLen; - for (uint16_t i = 0; i < zoneLen; i++) + for (int i = 0; i < zoneLen; i++) { uint8_t colorIndex = (i * 255 / zoneLen) - counter; uint16_t led = (z & 0x01) ? i : (zoneLen -1) -i; - if (IS_REVERSE) led = (zoneLen -1) -led; - setPixelColor(pos + led, color_from_palette(colorIndex, false, true, 255)); + if (SEGMENT.reverse) led = (zoneLen -1) -led; + SEGMENT.setPixelColor(pos + led, SEGMENT.color_from_palette(colorIndex, false, true, 255)); } } return FRAMETIME; } +static const char _data_FX_MODE_FLOW[] PROGMEM = "Flow@!,Zones;;!;;m12=1"; //vertical /* * Dots waving around in a sine/pendulum motion. * Little pixel birds flying in a circle. By Aircoookie */ -uint16_t WS2812FX::mode_chunchun(void) +uint16_t mode_chunchun(void) { - fill(SEGCOLOR(1)); - uint16_t counter = now*(6 + (SEGMENT.speed >> 4)); + if (SEGLEN == 1) return mode_static(); + SEGMENT.fade_out(254); // add a bit of trail + uint16_t counter = strip.now * (6 + (SEGMENT.speed >> 4)); uint16_t numBirds = 2 + (SEGLEN >> 3); // 2 + 1/8 of a segment uint16_t span = (SEGMENT.intensity << 8) / numBirds; - for (uint16_t i = 0; i < numBirds; i++) + for (int i = 0; i < numBirds; i++) { counter -= span; uint16_t megumin = sin16(counter) + 0x8000; - uint32_t bird = (megumin * SEGLEN) >> 16; - uint32_t c = color_from_palette((i * 255)/ numBirds, false, false, 0); // no palette wrapping - setPixelColor(bird, c); + uint16_t bird = uint32_t(megumin * SEGLEN) >> 16; + uint32_t c = SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0); // no palette wrapping + bird = constrain(bird, 0, SEGLEN-1); + SEGMENT.setPixelColor(bird, c); } return FRAMETIME; } +static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; + //13 bytes typedef struct Spotlight { @@ -3781,8 +4150,9 @@ typedef struct Spotlight { * * By Steve Pomeroy @xxv */ -uint16_t WS2812FX::mode_dancing_shadows(void) +uint16_t mode_dancing_shadows(void) { + if (SEGLEN == 1) return mode_static(); uint8_t numSpotlights = map(SEGMENT.intensity, 0, 255, 2, SPOT_MAX_COUNT); // 49 on 32 segment ESP32, 17 on 16 segment ESP8266 bool initialize = SEGENV.aux0 != numSpotlights; SEGENV.aux0 = numSpotlights; @@ -3791,12 +4161,12 @@ uint16_t WS2812FX::mode_dancing_shadows(void) if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Spotlight* spotlights = reinterpret_cast(SEGENV.data); - fill(BLACK); + SEGMENT.fill(BLACK); unsigned long time = millis(); bool respawn = false; - for (uint8_t i = 0; i < numSpotlights; i++) { + for (size_t i = 0; i < numSpotlights; i++) { if (!initialize) { // advance the position of the spotlight int16_t delta = (float)(time - spotlights[i].lastUpdateTime) * @@ -3833,61 +4203,59 @@ uint16_t WS2812FX::mode_dancing_shadows(void) spotlights[i].type = random8(SPOT_TYPES_COUNT); } - uint32_t color = color_from_palette(spotlights[i].colorIdx, false, false, 0); + uint32_t color = SEGMENT.color_from_palette(spotlights[i].colorIdx, false, false, 255); int start = spotlights[i].position; if (spotlights[i].width <= 1) { if (start >= 0 && start < SEGLEN) { - blendPixelColor(start, color, 128); + SEGMENT.blendPixelColor(start, color, 128); } } else { switch (spotlights[i].type) { case SPOT_TYPE_SOLID: - for (uint8_t j = 0; j < spotlights[i].width; j++) { + for (size_t j = 0; j < spotlights[i].width; j++) { if ((start + j) >= 0 && (start + j) < SEGLEN) { - blendPixelColor(start + j, color, 128); + SEGMENT.blendPixelColor(start + j, color, 128); } } break; case SPOT_TYPE_GRADIENT: - for (uint8_t j = 0; j < spotlights[i].width; j++) { + for (size_t j = 0; j < spotlights[i].width; j++) { if ((start + j) >= 0 && (start + j) < SEGLEN) { - blendPixelColor(start + j, color, - cubicwave8(map(j, 0, spotlights[i].width - 1, 0, 255))); + SEGMENT.blendPixelColor(start + j, color, cubicwave8(map(j, 0, spotlights[i].width - 1, 0, 255))); } } break; case SPOT_TYPE_2X_GRADIENT: - for (uint8_t j = 0; j < spotlights[i].width; j++) { + for (size_t j = 0; j < spotlights[i].width; j++) { if ((start + j) >= 0 && (start + j) < SEGLEN) { - blendPixelColor(start + j, color, - cubicwave8(2 * map(j, 0, spotlights[i].width - 1, 0, 255))); + SEGMENT.blendPixelColor(start + j, color, cubicwave8(2 * map(j, 0, spotlights[i].width - 1, 0, 255))); } } break; case SPOT_TYPE_2X_DOT: - for (uint8_t j = 0; j < spotlights[i].width; j += 2) { + for (size_t j = 0; j < spotlights[i].width; j += 2) { if ((start + j) >= 0 && (start + j) < SEGLEN) { - blendPixelColor(start + j, color, 128); + SEGMENT.blendPixelColor(start + j, color, 128); } } break; case SPOT_TYPE_3X_DOT: - for (uint8_t j = 0; j < spotlights[i].width; j += 3) { + for (size_t j = 0; j < spotlights[i].width; j += 3) { if ((start + j) >= 0 && (start + j) < SEGLEN) { - blendPixelColor(start + j, color, 128); + SEGMENT.blendPixelColor(start + j, color, 128); } } break; case SPOT_TYPE_4X_DOT: - for (uint8_t j = 0; j < spotlights[i].width; j += 4) { + for (size_t j = 0; j < spotlights[i].width; j += 4) { if ((start + j) >= 0 && (start + j) < SEGLEN) { - blendPixelColor(start + j, color, 128); + SEGMENT.blendPixelColor(start + j, color, 128); } } break; @@ -3897,51 +4265,55 @@ uint16_t WS2812FX::mode_dancing_shadows(void) return FRAMETIME; } +static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; + /* Imitates a washing machine, rotating same waves forward, then pause, then backward. By Stefan Seegel */ -uint16_t WS2812FX::mode_washing_machine(void) { - float speed = tristate_square8(now >> 7, 90, 15); - float quot = 32.0f - ((float)SEGMENT.speed / 16.0f); - speed /= quot; +uint16_t mode_washing_machine(void) { + int speed = tristate_square8(strip.now >> 7, 90, 15); - SEGENV.step += (speed * 128.0f); - - for (int i=0; i> 7)); - setPixelColor(i, color_from_palette(col, false, PALETTE_SOLID_WRAP, 3)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(col, false, PALETTE_SOLID_WRAP, 3)); } return FRAMETIME; } +static const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = "Washing Machine@!,!;;!"; + /* Blends random colors across palette Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e */ -uint16_t WS2812FX::mode_blends(void) { +uint16_t mode_blends(void) { uint16_t pixelLen = SEGLEN > UINT8_MAX ? UINT8_MAX : SEGLEN; uint16_t dataSize = sizeof(uint32_t) * (pixelLen + 1); // max segment length of 56 pixels on 16 segment ESP8266 if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed uint32_t* pixels = reinterpret_cast(SEGENV.data); uint8_t blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); - uint8_t shift = (now * ((SEGMENT.speed >> 3) +1)) >> 8; + uint8_t shift = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8; for (int i = 0; i < pixelLen; i++) { - pixels[i] = color_blend(pixels[i], color_from_palette(shift + quadwave8((i + 1) * 16), false, PALETTE_SOLID_WRAP, 255), blendSpeed); + pixels[i] = color_blend(pixels[i], SEGMENT.color_from_palette(shift + quadwave8((i + 1) * 16), false, PALETTE_SOLID_WRAP, 255), blendSpeed); shift += 3; } uint16_t offset = 0; for (int i = 0; i < SEGLEN; i++) { - setPixelColor(i, pixels[offset++]); + SEGMENT.setPixelColor(i, pixels[offset++]); if (offset > pixelLen) offset = 0; } return FRAMETIME; } +static const char _data_FX_MODE_BLENDS[] PROGMEM = "Blends@Shift speed,Blend speed;;!"; + /* TV Simulator @@ -3968,7 +4340,7 @@ typedef struct TvSim { uint16_t pb = 0; } tvSim; -uint16_t WS2812FX::mode_tv_simulator(void) { +uint16_t mode_tv_simulator(void) { uint16_t nr, ng, nb, r, g, b, i, hue; uint8_t sat, bri, j; @@ -3993,19 +4365,19 @@ uint16_t WS2812FX::mode_tv_simulator(void) { tvSimulator->sceeneColorBri = random8 ( 200, 240); // random start color-brightness for the sceene SEGENV.aux1 = 1; SEGENV.aux0 = 0; - } - + } + // slightly change the color-tone in this sceene if ( SEGENV.aux0 == 0) { // hue change in both directions j = random8(4 * colorIntensity); hue = (random8() < 128) ? ((j < tvSimulator->sceeneColorHue) ? tvSimulator->sceeneColorHue - j : 767 - tvSimulator->sceeneColorHue - j) : // negative ((j + tvSimulator->sceeneColorHue) < 767 ? tvSimulator->sceeneColorHue + j : tvSimulator->sceeneColorHue + j - 767) ; // positive - + // saturation j = random8(2 * colorIntensity); sat = (tvSimulator->sceeneColorSat - j) < 0 ? 0 : tvSimulator->sceeneColorSat - j; - + // brightness j = random8(100); bri = (tvSimulator->sceeneColorBri - j) < 0 ? 0 : tvSimulator->sceeneColorBri - j; @@ -4029,7 +4401,7 @@ uint16_t WS2812FX::mode_tv_simulator(void) { ng = (uint8_t)gamma8(tvSimulator->actualColorG) * 257; nb = (uint8_t)gamma8(tvSimulator->actualColorB) * 257; - if (SEGENV.aux0 == 0) { // initialize next iteration + if (SEGENV.aux0 == 0) { // initialize next iteration SEGENV.aux0 = 1; // randomize total duration and fade duration for the actual color @@ -4043,9 +4415,9 @@ uint16_t WS2812FX::mode_tv_simulator(void) { // how much time is elapsed ? tvSimulator->elapsed = millis() - tvSimulator->startTime; - // fade from prev volor to next color + // fade from prev color to next color if (tvSimulator->elapsed < tvSimulator->fadeTime) { - r = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pr, nr); + r = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pr, nr); g = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pg, ng); b = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pb, nb); } else { // Avoid divide-by-zero in map() @@ -4056,7 +4428,7 @@ uint16_t WS2812FX::mode_tv_simulator(void) { // set strip color for (i = 0; i < SEGLEN; i++) { - setPixelColor(i, r >> 8, g >> 8, b >> 8); // Quantize to 8-bit + SEGMENT.setPixelColor(i, r >> 8, g >> 8, b >> 8); // Quantize to 8-bit } // if total duration has passed, remember last color and restart the loop @@ -4066,9 +4438,11 @@ uint16_t WS2812FX::mode_tv_simulator(void) { tvSimulator->pb = nb; SEGENV.aux0 = 0; } - + return FRAMETIME; } +static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;"; + /* Aurora effect @@ -4110,7 +4484,7 @@ class AuroraWave { alive = true; } - CRGB getColorForLED(int ledIndex) { + CRGB getColorForLED(int ledIndex) { if(ledIndex < center - width || ledIndex > center + width) return 0; //Position out of range of this wave CRGB rgb; @@ -4123,7 +4497,7 @@ class AuroraWave { //The age of the wave determines it brightness. //At half its maximum age it will be the brightest. - float ageFactor = 0.1; + float ageFactor = 0.1; if((float)age / ttl < 0.5) { ageFactor = (float)age / (ttl / 2); } else { @@ -4135,7 +4509,7 @@ class AuroraWave { rgb.r = basecolor.r * factor; rgb.g = basecolor.g * factor; rgb.b = basecolor.b * factor; - + return rgb; }; @@ -4170,12 +4544,15 @@ class AuroraWave { }; }; -uint16_t WS2812FX::mode_aurora(void) { +uint16_t mode_aurora(void) { //aux1 = Wavecount //aux2 = Intensity in last loop AuroraWave* waves; +//TODO: I am not sure this is a correct way of handling memory allocation since if it fails on 1st run +// it will display static effect but on second run it may crash ESP since data will be nullptr + if(SEGENV.aux0 != SEGMENT.intensity || SEGENV.call == 0) { //Intensity slider changed or first call SEGENV.aux1 = map(SEGMENT.intensity, 0, 255, 2, W_MAX_COUNT); @@ -4187,20 +4564,20 @@ uint16_t WS2812FX::mode_aurora(void) { waves = reinterpret_cast(SEGENV.data); - for(int i = 0; i < SEGENV.aux1; i++) { - waves[i].init(SEGLEN, col_to_crgb(color_from_palette(random8(), false, false, random(0, 3)))); + for (int i = 0; i < SEGENV.aux1; i++) { + waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random(0, 3)))); } } else { waves = reinterpret_cast(SEGENV.data); } - for(int i = 0; i < SEGENV.aux1; i++) { + for (int i = 0; i < SEGENV.aux1; i++) { //Update values of wave waves[i].update(SEGLEN, SEGMENT.speed); if(!(waves[i].stillAlive())) { //If a wave dies, reinitialize it starts over. - waves[i].init(SEGLEN, col_to_crgb(color_from_palette(random8(), false, false, random(0, 3)))); + waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random(0, 3)))); } } @@ -4209,21 +4586,3318 @@ uint16_t WS2812FX::mode_aurora(void) { if (SEGCOLOR(1)) backlight++; if (SEGCOLOR(2)) backlight++; //Loop through LEDs to determine color - for(int i = 0; i < SEGLEN; i++) { + for (int i = 0; i < SEGLEN; i++) { CRGB mixedRgb = CRGB(backlight, backlight, backlight); //For each LED we must check each wave if it is "active" at this position. //If there are multiple waves active on a LED we multiply their values. - for(int j = 0; j < SEGENV.aux1; j++) { + for (int j = 0; j < SEGENV.aux1; j++) { CRGB rgb = waves[j].getColorForLED(i); - - if(rgb != CRGB(0)) { + + if(rgb != CRGB(0)) { mixedRgb += rgb; } } - setPixelColor(i, mixedRgb[0], mixedRgb[1], mixedRgb[2]); + SEGMENT.setPixelColor(i, mixedRgb[0], mixedRgb[1], mixedRgb[2]); } - + + return FRAMETIME; +} +static const char _data_FX_MODE_AURORA[] PROGMEM = "Aurora@!,!;1,2,3;!;;sx=24,pal=50"; + +// WLED-SR effects + +///////////////////////// +// Perlin Move // +///////////////////////// +// 16 bit perlinmove. Use Perlin Noise instead of sinewaves for movement. By Andrew Tuline. +// Controls are speed, # of pixels, faderate. +uint16_t mode_perlinmove(void) { + if (SEGLEN == 1) return mode_static(); + SEGMENT.fade_out(255-SEGMENT.custom1); + for (int i = 0; i < SEGMENT.intensity/16 + 1; i++) { + uint16_t locn = inoise16(millis()*128/(260-SEGMENT.speed)+i*15000, millis()*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. + uint16_t pixloc = map(locn, 50*256, 192*256, 0, SEGLEN-1); // Map that to the length of the strand, and ensure we don't go over. + SEGMENT.setPixelColor(pixloc, SEGMENT.color_from_palette(pixloc%255, false, PALETTE_SOLID_WRAP, 0)); + } + + return FRAMETIME; +} // mode_perlinmove() +static const char _data_FX_MODE_PERLINMOVE[] PROGMEM = "Perlin Move@!,# of pixels,Fade rate;!,!;!"; + + +///////////////////////// +// Waveins // +///////////////////////// +// Uses beatsin8() + phase shifting. By: Andrew Tuline +uint16_t mode_wavesins(void) { + + for (int i = 0; i < SEGLEN; i++) { + uint8_t bri = sin8(millis()/4 + i * SEGMENT.intensity); + uint8_t index = beatsin8(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * (SEGMENT.custom3<<3)); // custom3 is reduced resolution slider + //SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, index, bri, LINEARBLEND)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, bri)); + } + + return FRAMETIME; +} // mode_waveins() +static const char _data_FX_MODE_WAVESINS[] PROGMEM = "Wavesins@!,Brightness variation,Starting color,Range of colors,Color variation;!;!"; + + +////////////////////////////// +// Flow Stripe // +////////////////////////////// +// By: ldirko https://editor.soulmatelights.com/gallery/392-flow-led-stripe , modifed by: Andrew Tuline +uint16_t mode_FlowStripe(void) { + + const uint16_t hl = SEGLEN * 10 / 13; + uint8_t hue = millis() / (SEGMENT.speed+1); + uint32_t t = millis() / (SEGMENT.intensity/8+1); + + for (int i = 0; i < SEGLEN; i++) { + int c = (abs(i - hl) / hl) * 127; + c = sin8(c); + c = sin8(c / 2 + t); + byte b = sin8(c + t/8); + SEGMENT.setPixelColor(i, CHSV(b + hue, 255, 255)); + } + + return FRAMETIME; +} // mode_FlowStripe() +static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Effect speed;;"; + + +#ifndef WLED_DISABLE_2D +/////////////////////////////////////////////////////////////////////////////// +//*************************** 2D routines *********************************** +#define XY(x,y) SEGMENT.XY(x,y) + + +// Black hole +uint16_t mode_2DBlackHole(void) { // By: Stepko https://editor.soulmatelights.com/gallery/1012 , Modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + uint16_t x, y; + + SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails + unsigned long t = millis()/128; // timebase + // outer stars + for (size_t i = 0; i < 8; i++) { + x = beatsin8(SEGMENT.custom1>>3, 0, cols - 1, 0, ((i % 2) ? 128 : 0) + t * i); + y = beatsin8(SEGMENT.intensity>>3, 0, rows - 1, 0, ((i % 2) ? 192 : 64) + t * i); + SEGMENT.addPixelColorXY(x, y, SEGMENT.color_from_palette(i*32, false, PALETTE_SOLID_WRAP, SEGMENT.check1?0:255)); + } + // inner stars + for (size_t i = 0; i < 4; i++) { + x = beatsin8(SEGMENT.custom2>>3, cols/4, cols - 1 - cols/4, 0, ((i % 2) ? 128 : 0) + t * i); + y = beatsin8(SEGMENT.custom3 , rows/4, rows - 1 - rows/4, 0, ((i % 2) ? 192 : 64) + t * i); + SEGMENT.addPixelColorXY(x, y, SEGMENT.color_from_palette(255-i*64, false, PALETTE_SOLID_WRAP, SEGMENT.check1?0:255)); + } + // central white dot + SEGMENT.setPixelColorXY(cols/2, rows/2, WHITE); + // blur everything a bit + SEGMENT.blur(16); + + return FRAMETIME; +} // mode_2DBlackHole() +static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Outer Y freq.,Outer X freq.,Inner X freq.,Inner Y freq.,Solid;!;!;2;pal=11"; + + +//////////////////////////// +// 2D Colored Bursts // +//////////////////////////// +uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.soulmatelights.com/gallery/819-colored-bursts , modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (SEGENV.call == 0) { + SEGENV.aux0 = 0; // start with red hue + } + + bool dot = SEGMENT.check3; + bool grad = SEGMENT.check1; + + byte numLines = SEGMENT.intensity/16 + 1; + + SEGENV.aux0++; // hue + SEGMENT.fadeToBlackBy(40); + for (size_t i = 0; i < numLines; i++) { + byte x1 = beatsin8(2 + SEGMENT.speed/16, 0, (cols - 1)); + byte x2 = beatsin8(1 + SEGMENT.speed/16, 0, (cols - 1)); + byte y1 = beatsin8(5 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 24); + byte y2 = beatsin8(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64); + CRGB color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); + + byte xsteps = abs8(x1 - y1) + 1; + byte ysteps = abs8(x2 - y2) + 1; + byte steps = xsteps >= ysteps ? xsteps : ysteps; + //Draw gradient line + for (size_t j = 1; j <= steps; j++) { + uint8_t rate = j * 255 / steps; + byte dx = lerp8by8(x1, y1, rate); + byte dy = lerp8by8(x2, y2, rate); + //SEGMENT.setPixelColorXY(dx, dy, grad ? color.nscale8_video(255-rate) : color); // use addPixelColorXY for different look + SEGMENT.addPixelColorXY(dx, dy, color); // use setPixelColorXY for different look + if (grad) SEGMENT.fadePixelColorXY(dx, dy, rate); + } + + if (dot) { //add white point at the ends of line + SEGMENT.setPixelColorXY(x1, x2, WHITE); + SEGMENT.setPixelColorXY(y1, y2, DARKSLATEGRAY); + } + } + if (SEGMENT.custom3) SEGMENT.blur(SEGMENT.custom3/2); + + return FRAMETIME; +} // mode_2DColoredBursts() +static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Speed,# of lines,,,Blur,Gradient,,Dots;;!;2;c3=16"; + + +///////////////////// +// 2D DNA // +///////////////////// +uint16_t mode_2Ddna(void) { // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline. + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + SEGMENT.fadeToBlackBy(64); + for (int i = 0; i < cols; i++) { + SEGMENT.setPixelColorXY(i, beatsin8(SEGMENT.speed/8, 0, rows-1, 0, i*4 ), ColorFromPalette(SEGPALETTE, i*5+millis()/17, beatsin8(5, 55, 255, 0, i*10), LINEARBLEND)); + SEGMENT.setPixelColorXY(i, beatsin8(SEGMENT.speed/8, 0, rows-1, 0, i*4+128), ColorFromPalette(SEGPALETTE, i*5+128+millis()/17, beatsin8(5, 55, 255, 0, i*10+128), LINEARBLEND)); + } + SEGMENT.blur(SEGMENT.intensity>>3); + + return FRAMETIME; +} // mode_2Ddna() +static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur;;!;2"; + + +///////////////////////// +// 2D DNA Spiral // +///////////////////////// +uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulmatelights.com/gallery/810 , modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + uint8_t speeds = SEGMENT.speed/2 + 1; + uint8_t freq = SEGMENT.intensity/8; + + uint32_t ms = millis() / 20; + SEGMENT.fadeToBlackBy(135); + + for (int i = 0; i < rows; i++) { + uint16_t x = beatsin8(speeds, 0, cols - 1, 0, i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, i * freq + 128); + uint16_t x1 = beatsin8(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, 128 + 64 + i * freq); + uint8_t hue = (i * 128 / rows) + ms; + // skip every 4th row every now and then (fade it more) + if ((i + ms / 8) & 3) { + // draw a gradient line between x and x1 + x = x / 2; x1 = x1 / 2; + uint8_t steps = abs8(x - x1) + 1; + for (size_t k = 1; k <= steps; k++) { + uint8_t rate = k * 255 / steps; + uint8_t dx = lerp8by8(x, x1, rate); + //SEGMENT.setPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND).nscale8_video(rate)); + SEGMENT.addPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND)); // use setPixelColorXY for different look + SEGMENT.fadePixelColorXY(dx, i, rate); + } + SEGMENT.setPixelColorXY(x, i, DARKSLATEGRAY); + SEGMENT.setPixelColorXY(x1, i, WHITE); + } + } + + return FRAMETIME; +} // mode_2DDNASpiral() +static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed,Y frequency;;!;2"; + + +///////////////////////// +// 2D Drift // +///////////////////////// +uint16_t mode_2DDrift() { // By: Stepko https://editor.soulmatelights.com/gallery/884-drift , Modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + SEGMENT.fadeToBlackBy(128); + const uint16_t maxDim = MAX(cols, rows)/2; + unsigned long t = millis() / (32 - (SEGMENT.speed>>3)); + unsigned long t_20 = t/20; // softhack007: pre-calculating this gives about 10% speedup + for (float i = 1; i < maxDim; i += 0.25) { + float angle = radians(t * (maxDim - i)); + uint16_t myX = (cols>>1) + (uint16_t)(sin_t(angle) * i) + (cols%2); + uint16_t myY = (rows>>1) + (uint16_t)(cos_t(angle) * i) + (rows%2); + SEGMENT.setPixelColorXY(myX, myY, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); + } + SEGMENT.blur(SEGMENT.intensity>>3); + + return FRAMETIME; +} // mode_2DDrift() +static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur amount;;!;2"; + + +////////////////////////// +// 2D Firenoise // +////////////////////////// +uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline. Yet another short routine. + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + unsigned xscale = SEGMENT.intensity*4; + unsigned yscale = SEGMENT.speed*8; + unsigned indexx = 0; + + CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : CRGBPalette16(CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Black, + CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, + CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, + CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); + + for (int j=0; j < cols; j++) { + for (int i=0; i < rows; i++) { + indexx = inoise8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. + SEGMENT.setPixelColorXY(j, i, ColorFromPalette(pal, min(i*(indexx)>>4, 255U), i*255/cols, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. + } // for i + } // for j + + return FRAMETIME; +} // mode_2Dfirenoise() +static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y scale,,,,Palette;;!;2;pal=66"; + + +////////////////////////////// +// 2D Frizzles // +////////////////////////////// +uint16_t mode_2DFrizzles(void) { // By: Stepko https://editor.soulmatelights.com/gallery/640-color-frizzles , Modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + SEGMENT.fadeToBlackBy(16); + for (size_t i = 8; i > 0; i--) { + SEGMENT.addPixelColorXY(beatsin8(SEGMENT.speed/8 + i, 0, cols - 1), + beatsin8(SEGMENT.intensity/8 - i, 0, rows - 1), + ColorFromPalette(SEGPALETTE, beatsin8(12, 0, 255), 255, LINEARBLEND)); + } + SEGMENT.blur(SEGMENT.custom1>>3); + + return FRAMETIME; +} // mode_2DFrizzles() +static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y frequency,Blur;;!;2"; + + +/////////////////////////////////////////// +// 2D Cellular Automata Game of life // +/////////////////////////////////////////// +typedef struct ColorCount { + CRGB color; + int8_t count; +} colorCount; + +uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + const uint16_t dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled + const uint16_t crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) + + if (!SEGENV.allocateData(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed + CRGB *prevLeds = reinterpret_cast(SEGENV.data); + uint16_t *crcBuffer = reinterpret_cast(SEGENV.data + dataSize); + + CRGB backgroundColor = SEGCOLOR(1); + + if (SEGENV.call == 0 || strip.now - SEGMENT.step > 3000) { + SEGENV.step = strip.now; + SEGENV.aux0 = 0; + random16_set_seed(millis()>>2); //seed the random generator + + //give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen) + for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { + uint8_t state = random8()%2; + if (state == 0) + SEGMENT.setPixelColorXY(x,y, backgroundColor); + else + SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 255)); + } + + for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) prevLeds[XY(x,y)] = CRGB::Black; + memset(crcBuffer, 0, sizeof(uint16_t)*crcBufferLen); + } else if (strip.now - SEGENV.step < FRAMETIME_FIXED * (uint32_t)map(SEGMENT.speed,0,255,64,4)) { + // update only when appropriate time passes (in 42 FPS slots) + return FRAMETIME; + } + + //copy previous leds (save previous generation) + //NOTE: using lossy getPixelColor() is a benefit as endlessly repeating patterns will eventually fade out causing a reset + for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) prevLeds[XY(x,y)] = SEGMENT.getPixelColorXY(x,y); + + //calculate new leds + for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { + + colorCount colorsCount[9]; // count the different colors in the 3*3 matrix + for (int i=0; i<9; i++) colorsCount[i] = {backgroundColor, 0}; // init colorsCount + + // iterate through neighbors and count them and their different colors + int neighbors = 0; + for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix + if (i==0 && j==0) continue; // ignore itself + // wrap around segment + int16_t xx = x+i, yy = y+j; + if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0; + if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0; + + uint16_t xy = XY(xx, yy); // previous cell xy to check + // count different neighbours and colors + if (prevLeds[xy] != backgroundColor) { + neighbors++; + bool colorFound = false; + int k; + for (k=0; k<9 && colorsCount[i].count != 0; k++) + if (colorsCount[k].color == prevLeds[xy]) { + colorsCount[k].count++; + colorFound = true; + } + if (!colorFound) colorsCount[k] = {prevLeds[xy], 1}; //add new color found in the array + } + } // i,j + + // Rules of Life + uint32_t col = uint32_t(prevLeds[XY(x,y)]) & 0x00FFFFFF; // uint32_t operator returns RGBA, we want RGBW -> cut off "alpha" byte + uint32_t bgc = RGBW32(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0); + if ((col != bgc) && (neighbors < 2)) SEGMENT.setPixelColorXY(x,y, bgc); // Loneliness + else if ((col != bgc) && (neighbors > 3)) SEGMENT.setPixelColorXY(x,y, bgc); // Overpopulation + else if ((col == bgc) && (neighbors == 3)) { // Reproduction + // find dominant color and assign it to a cell + colorCount dominantColorCount = {backgroundColor, 0}; + for (int i=0; i<9 && colorsCount[i].count != 0; i++) + if (colorsCount[i].count > dominantColorCount.count) dominantColorCount = colorsCount[i]; + // assign the dominant color w/ a bit of randomness to avoid "gliders" + if (dominantColorCount.count > 0 && random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color); + } else if ((col == bgc) && (neighbors == 2) && !random8(128)) { // Mutation + SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 255)); + } + // else do nothing! + } //x,y + + // calculate CRC16 of leds + uint16_t crc = crc16((const unsigned char*)prevLeds, dataSize); + // check if we had same CRC and reset if needed + bool repetition = false; + for (int i=0; i>1)+1); + + for (int x = 0; x < cols; x++) { + for (int y = 0; y < rows; y++) { + SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(sin8(cos8(x * SEGMENT.speed/16 + a / 3) + sin8(y * SEGMENT.intensity/16 + a / 4) + a), false, PALETTE_SOLID_WRAP, 0)); + } + } + return FRAMETIME; -} \ No newline at end of file +} // mode_2DHiphotic() +static const char _data_FX_MODE_2DHIPHOTIC[] PROGMEM = "Hiphotic@X scale,Y scale,,,Speed;!;!;2"; + + +///////////////////////// +// 2D Julia // +///////////////////////// +// Sliders are: +// intensity = Maximum number of iterations per pixel. +// Custom1 = Location of X centerpoint +// Custom2 = Location of Y centerpoint +// Custom3 = Size of the area (small value = smaller area) +typedef struct Julia { + float xcen; + float ycen; + float xymag; +} julia; + +uint16_t mode_2DJulia(void) { // An animated Julia set by Andrew Tuline. + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (!SEGENV.allocateData(sizeof(julia))) return mode_static(); + Julia* julias = reinterpret_cast(SEGENV.data); + + float reAl; + float imAg; + + if (SEGENV.call == 0) { // Reset the center if we've just re-started this animation. + julias->xcen = 0.; + julias->ycen = 0.; + julias->xymag = 1.0; + + SEGMENT.custom1 = 128; // Make sure the location widgets are centered to start. + SEGMENT.custom2 = 128; + SEGMENT.custom3 = 16; + SEGMENT.intensity = 24; + } + + julias->xcen = julias->xcen + (float)(SEGMENT.custom1 - 128)/100000.f; + julias->ycen = julias->ycen + (float)(SEGMENT.custom2 - 128)/100000.f; + julias->xymag = julias->xymag + (float)((SEGMENT.custom3 - 16)<<3)/100000.f; // reduced resolution slider + if (julias->xymag < 0.01f) julias->xymag = 0.01f; + if (julias->xymag > 1.0f) julias->xymag = 1.0f; + + float xmin = julias->xcen - julias->xymag; + float xmax = julias->xcen + julias->xymag; + float ymin = julias->ycen - julias->xymag; + float ymax = julias->ycen + julias->xymag; + + // Whole set should be within -1.2,1.2 to -.8 to 1. + xmin = constrain(xmin, -1.2f, 1.2f); + xmax = constrain(xmax, -1.2f, 1.2f); + ymin = constrain(ymin, -0.8f, 1.0f); + ymax = constrain(ymax, -0.8f, 1.0f); + + float dx; // Delta x is mapped to the matrix size. + float dy; // Delta y is mapped to the matrix size. + + int maxIterations = 15; // How many iterations per pixel before we give up. Make it 8 bits to match our range of colours. + float maxCalc = 16.0; // How big is each calculation allowed to be before we give up. + + maxIterations = SEGMENT.intensity/2; + + + // Resize section on the fly for some animaton. + reAl = -0.94299f; // PixelBlaze example + imAg = 0.3162f; + + reAl += sin_t((float)millis()/305.f)/20.f; + imAg += sin_t((float)millis()/405.f)/20.f; + + dx = (xmax - xmin) / (cols); // Scale the delta x and y values to our matrix size. + dy = (ymax - ymin) / (rows); + + // Start y + float y = ymin; + for (int j = 0; j < rows; j++) { + + // Start x + float x = xmin; + for (int i = 0; i < cols; i++) { + + // Now we test, as we iterate z = z^2 + c does z tend towards infinity? + float a = x; + float b = y; + int iter = 0; + + while (iter < maxIterations) { // Here we determine whether or not we're out of bounds. + float aa = a * a; + float bb = b * b; + float len = aa + bb; + if (len > maxCalc) { // |z| = sqrt(a^2+b^2) OR z^2 = a^2+b^2 to save on having to perform a square root. + break; // Bail + } + + // This operation corresponds to z -> z^2+c where z=a+ib c=(x,y). Remember to use 'foil'. + b = 2*a*b + imAg; + a = aa - bb + reAl; + iter++; + } // while + + // We color each pixel based on how long it takes to get to infinity, or black if it never gets there. + if (iter == maxIterations) { + SEGMENT.setPixelColorXY(i, j, 0); + } else { + SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(iter*255/maxIterations, false, PALETTE_SOLID_WRAP, 0)); + } + x += dx; + } + y += dy; + } +// SEGMENT.blur(64); + + return FRAMETIME; +} // mode_2DJulia() +static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per pixel,X center,Y center,Area size;!;!;2;ix=24,c1=128,c2=128,c3=16"; + + +////////////////////////////// +// 2D Lissajous // +////////////////////////////// +uint16_t mode_2DLissajous(void) { // By: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + SEGMENT.fadeToBlackBy(SEGMENT.intensity); + uint_fast16_t phase = (millis() * (1 + SEGENV.custom3)) /32; // allow user to control rotation speed + + //for (int i=0; i < 4*(cols+rows); i ++) { + for (int i=0; i < 256; i ++) { + //float xlocn = float(sin8(now/4+i*(SEGMENT.speed>>5))) / 255.0f; + //float ylocn = float(cos8(now/4+i*2)) / 255.0f; + uint_fast8_t xlocn = sin8(phase/2 + (i*SEGMENT.speed)/32); + uint_fast8_t ylocn = cos8(phase/2 + i*2); + xlocn = (cols < 2) ? 1 : (map(2*xlocn, 0,511, 0,2*(cols-1)) +1) /2; // softhack007: "(2* ..... +1) /2" for proper rounding + ylocn = (rows < 2) ? 1 : (map(2*ylocn, 0,511, 0,2*(rows-1)) +1) /2; // "rows > 1" is needed to avoid div/0 in map() + SEGMENT.setPixelColorXY((uint8_t)xlocn, (uint8_t)ylocn, SEGMENT.color_from_palette(millis()/100+i, false, PALETTE_SOLID_WRAP, 0)); + } + + return FRAMETIME; +} // mode_2DLissajous() +static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate,,,Speed;!;!;2;;c3=15"; + + +/////////////////////// +// 2D Matrix // +/////////////////////// +uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi, and softhack007. + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + SEGENV.aux0 = SEGENV.aux1 = UINT16_MAX; + SEGENV.step = 0; + } + + uint8_t fade = map(SEGMENT.custom1, 0, 255, 50, 250); // equals trail size + uint8_t speed = (256-SEGMENT.speed) >> map(MIN(rows, 150), 0, 150, 0, 3); // slower speeds for small displays + + CRGB spawnColor; + CRGB trailColor; + if (SEGMENT.check1) { + spawnColor = SEGCOLOR(0); + trailColor = SEGCOLOR(1); + } else { + spawnColor = CRGB(175,255,175); + trailColor = CRGB(27,130,39); + } + + if (strip.now - SEGENV.step >= speed) { + SEGENV.step = strip.now; + // find out what color value is returned by gPC for a "falling code" example pixel + // the color values returned may differ from the previously set values, due to + // - auto brightness limiter (dimming) + // - lossy color buffer (when not using global buffer) + // - color balance correction + // - segment opacity + CRGB oldSpawnColor = spawnColor; + if ((SEGENV.aux0 < cols) && (SEGENV.aux1 < rows)) { // we have a hint from last run + oldSpawnColor = SEGMENT.getPixelColorXY(SEGENV.aux0, SEGENV.aux1); // find color of previous spawns + SEGENV.aux1 ++; // our sample pixel will be one row down the next time + } + if ((oldSpawnColor == CRGB::Black) || (oldSpawnColor == trailColor)) oldSpawnColor = spawnColor; // reject "black", as it would mean that ALL pixels create trails + + // move pixels one row down. Falling codes keep color and add trail pixels; all others pixels are faded + for (int row=rows-1; row>=0; row--) { + for (int col=0; col= rows); // empty screen means that the last falling code has moved out of screen area + + // spawn new falling code + if (random8() <= SEGMENT.intensity || emptyScreen) { + uint8_t spawnX = random8(cols); + SEGMENT.setPixelColorXY(spawnX, 0, spawnColor); + // update hint for next run + SEGENV.aux0 = spawnX; + SEGENV.aux1 = 0; + } + } // if millis + + return FRAMETIME; +} // mode_2Dmatrix() +static const char _data_FX_MODE_2DMATRIX[] PROGMEM = "Matrix@!,Spawning rate,Trail,,,Custom color;Spawn,Trail;;2"; + + +///////////////////////// +// 2D Metaballs // +///////////////////////// +uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have one of the dimensions be 2 or less. Adapted by Andrew Tuline. + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + float speed = 0.25f * (1+(SEGMENT.speed>>6)); + + // get some 2 random moving points + uint8_t x2 = map(inoise8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); + uint8_t y2 = map(inoise8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); + + uint8_t x3 = map(inoise8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); + uint8_t y3 = map(inoise8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); + + // and one Lissajou function + uint8_t x1 = beatsin8(23 * speed, 0, cols-1); + uint8_t y1 = beatsin8(28 * speed, 0, rows-1); + + for (int y = 0; y < rows; y++) { + for (int x = 0; x < cols; x++) { + // calculate distances of the 3 points from actual pixel + // and add them together with weightening + uint16_t dx = abs(x - x1); + uint16_t dy = abs(y - y1); + uint16_t dist = 2 * sqrt16((dx * dx) + (dy * dy)); + + dx = abs(x - x2); + dy = abs(y - y2); + dist += sqrt16((dx * dx) + (dy * dy)); + + dx = abs(x - x3); + dy = abs(y - y3); + dist += sqrt16((dx * dx) + (dy * dy)); + + // inverse result + byte color = dist ? 1000 / dist : 255; + + // map color between thresholds + if (color > 0 and color < 60) { + SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(map(color * 9, 9, 531, 0, 255), false, PALETTE_SOLID_WRAP, 0)); + } else { + SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(0, false, PALETTE_SOLID_WRAP, 0)); + } + // show the 3 points, too + SEGMENT.setPixelColorXY(x1, y1, WHITE); + SEGMENT.setPixelColorXY(x2, y2, WHITE); + SEGMENT.setPixelColorXY(x3, y3, WHITE); + } + } + + return FRAMETIME; +} // mode_2Dmetaballs() +static const char _data_FX_MODE_2DMETABALLS[] PROGMEM = "Metaballs@!;;!;2"; + + +////////////////////// +// 2D Noise // +////////////////////// +uint16_t mode_2Dnoise(void) { // By Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + const uint16_t scale = SEGMENT.intensity+2; + + for (int y = 0; y < rows; y++) { + for (int x = 0; x < cols; x++) { + uint8_t pixelHue8 = inoise8(x * scale, y * scale, millis() / (16 - SEGMENT.speed/16)); + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, pixelHue8)); + } + } + + return FRAMETIME; +} // mode_2Dnoise() +static const char _data_FX_MODE_2DNOISE[] PROGMEM = "Noise2D@!,Scale;;!;2"; + + +////////////////////////////// +// 2D Plasma Ball // +////////////////////////////// +uint16_t mode_2DPlasmaball(void) { // By: Stepko https://editor.soulmatelights.com/gallery/659-plasm-ball , Modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); + uint_fast32_t t = (millis() * 8) / (256 - SEGMENT.speed); // optimized to avoid float + for (int i = 0; i < cols; i++) { + uint16_t thisVal = inoise8(i * 30, t, t); + uint16_t thisMax = map(thisVal, 0, 255, 0, cols-1); + for (int j = 0; j < rows; j++) { + uint16_t thisVal_ = inoise8(t, j * 30, t); + uint16_t thisMax_ = map(thisVal_, 0, 255, 0, rows-1); + uint16_t x = (i + thisMax_ - cols / 2); + uint16_t y = (j + thisMax - cols / 2); + uint16_t cx = (i + thisMax_); + uint16_t cy = (j + thisMax); + + SEGMENT.addPixelColorXY(i, j, ((x - y > -2) && (x - y < 2)) || + ((cols - 1 - x - y) > -2 && (cols - 1 - x - y < 2)) || + (cols - cx == 0) || + (cols - 1 - cx == 0) || + ((rows - cy == 0) || + (rows - 1 - cy == 0)) ? ColorFromPalette(SEGPALETTE, beat8(5), thisVal, LINEARBLEND) : CRGB::Black); + } + } + SEGMENT.blur(SEGMENT.custom2>>5); + + return FRAMETIME; +} // mode_2DPlasmaball() +static const char _data_FX_MODE_2DPLASMABALL[] PROGMEM = "Plasma Ball@Speed,,Fade,Blur;;!;2"; + + +//////////////////////////////// +// 2D Polar Lights // +//////////////////////////////// +//static float fmap(const float x, const float in_min, const float in_max, const float out_min, const float out_max) { +// return (out_max - out_min) * (x - in_min) / (in_max - in_min) + out_min; +//} +uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + CRGBPalette16 auroraPalette = {0x000000, 0x003300, 0x006600, 0x009900, 0x00cc00, 0x00ff00, 0x33ff00, 0x66ff00, 0x99ff00, 0xccff00, 0xffff00, 0xffcc00, 0xff9900, 0xff6600, 0xff3300, 0xff0000}; + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + SEGENV.step = 0; + } + + float adjustHeight = (float)map(rows, 8, 32, 28, 12); // maybe use mapf() ??? + uint16_t adjScale = map(cols, 8, 64, 310, 63); +/* + if (SEGENV.aux1 != SEGMENT.custom1/12) { // Hacky palette rotation. We need that black. + SEGENV.aux1 = SEGMENT.custom1/12; + for (int i = 0; i < 16; i++) { + long ilk; + ilk = (long)currentPalette[i].r << 16; + ilk += (long)currentPalette[i].g << 8; + ilk += (long)currentPalette[i].b; + ilk = (ilk << SEGENV.aux1) | (ilk >> (24 - SEGENV.aux1)); + currentPalette[i].r = ilk >> 16; + currentPalette[i].g = ilk >> 8; + currentPalette[i].b = ilk; + } + } +*/ + uint16_t _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); + byte _speed = map(SEGMENT.speed, 0, 255, 128, 16); + + for (int x = 0; x < cols; x++) { + for (int y = 0; y < rows; y++) { + SEGENV.step++; + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(auroraPalette, + qsub8( + inoise8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), + fabsf((float)rows / 2.0f - (float)y) * adjustHeight))); + } + } + + return FRAMETIME; +} // mode_2DPolarLights() +static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale;;;2"; + + +///////////////////////// +// 2D Pulser // +///////////////////////// +uint16_t mode_2DPulser(void) { // By: ldirko https://editor.soulmatelights.com/gallery/878-pulse-test , modifed by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + SEGMENT.fadeToBlackBy(8 - (SEGMENT.intensity>>5)); + uint32_t a = strip.now / (18 - SEGMENT.speed / 16); + uint16_t x = (a / 14) % cols; + uint16_t y = map((sin8(a * 5) + sin8(a * 4) + sin8(a * 2)), 0, 765, rows-1, 0); + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, map(y, 0, rows-1, 0, 255), 255, LINEARBLEND)); + + SEGMENT.blur(1 + (SEGMENT.intensity>>4)); + + return FRAMETIME; +} // mode_2DPulser() +static const char _data_FX_MODE_2DPULSER[] PROGMEM = "Pulser@!,Blur;;!;2"; + + +///////////////////////// +// 2D Sindots // +///////////////////////// +uint16_t mode_2DSindots(void) { // By: ldirko https://editor.soulmatelights.com/gallery/597-sin-dots , modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + SEGMENT.fadeToBlackBy(SEGMENT.custom1>>3); + + byte t1 = millis() / (257 - SEGMENT.speed); // 20; + byte t2 = sin8(t1) / 4 * 2; + for (int i = 0; i < 13; i++) { + byte x = sin8(t1 + i * SEGMENT.intensity/8)*(cols-1)/255; // max index now 255x15/255=15! + byte y = sin8(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, i * 255 / 13, 255, LINEARBLEND)); + } + SEGMENT.blur(SEGMENT.custom2>>3); + + return FRAMETIME; +} // mode_2DSindots() +static const char _data_FX_MODE_2DSINDOTS[] PROGMEM = "Sindots@!,Dot distance,Fade rate,Blur;;!;2"; + + +////////////////////////////// +// 2D Squared Swirl // +////////////////////////////// +// custom3 affects the blur amount. +uint16_t mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://gist.github.com/kriegsman/368b316c55221134b160 + // Modifed by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + const uint8_t kBorderWidth = 2; + + SEGMENT.fadeToBlackBy(24); + uint8_t blurAmount = SEGMENT.custom3>>1; // reduced resolution slider + SEGMENT.blur(blurAmount); + + // Use two out-of-sync sine waves + uint8_t i = beatsin8(19, kBorderWidth, cols-kBorderWidth); + uint8_t j = beatsin8(22, kBorderWidth, cols-kBorderWidth); + uint8_t k = beatsin8(17, kBorderWidth, cols-kBorderWidth); + uint8_t m = beatsin8(18, kBorderWidth, rows-kBorderWidth); + uint8_t n = beatsin8(15, kBorderWidth, rows-kBorderWidth); + uint8_t p = beatsin8(20, kBorderWidth, rows-kBorderWidth); + + uint16_t ms = millis(); + + SEGMENT.addPixelColorXY(i, m, ColorFromPalette(SEGPALETTE, ms/29, 255, LINEARBLEND)); + SEGMENT.addPixelColorXY(j, n, ColorFromPalette(SEGPALETTE, ms/41, 255, LINEARBLEND)); + SEGMENT.addPixelColorXY(k, p, ColorFromPalette(SEGPALETTE, ms/73, 255, LINEARBLEND)); + + return FRAMETIME; +} // mode_2Dsquaredswirl() +static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,,,,Blur;;!;2"; + + +////////////////////////////// +// 2D Sun Radiation // +////////////////////////////// +uint16_t mode_2DSunradiation(void) { // By: ldirko https://editor.soulmatelights.com/gallery/599-sun-radiation , modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (!SEGENV.allocateData(sizeof(byte)*(cols+2)*(rows+2))) return mode_static(); //allocation failed + byte *bump = reinterpret_cast(SEGENV.data); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + unsigned long t = millis() / 4; + int index = 0; + uint8_t someVal = SEGMENT.speed/4; // Was 25. + for (int j = 0; j < (rows + 2); j++) { + for (int i = 0; i < (cols + 2); i++) { + byte col = (inoise8_raw(i * someVal, j * someVal, t)) / 2; + bump[index++] = col; + } + } + + int yindex = cols + 3; + int16_t vly = -(rows / 2 + 1); + for (int y = 0; y < rows; y++) { + ++vly; + int16_t vlx = -(cols / 2 + 1); + for (int x = 0; x < cols; x++) { + ++vlx; + int8_t nx = bump[x + yindex + 1] - bump[x + yindex - 1]; + int8_t ny = bump[x + yindex + (cols + 2)] - bump[x + yindex - (cols + 2)]; + byte difx = abs8(vlx * 7 - nx); + byte dify = abs8(vly * 7 - ny); + int temp = difx * difx + dify * dify; + int col = 255 - temp / 8; //8 its a size of effect + if (col < 0) col = 0; + SEGMENT.setPixelColorXY(x, y, HeatColor(col / (3.0f-(float)(SEGMENT.intensity)/128.f))); + } + yindex += (cols + 2); + } + + return FRAMETIME; +} // mode_2DSunradiation() +static const char _data_FX_MODE_2DSUNRADIATION[] PROGMEM = "Sun Radiation@Variance,Brightness;;;2"; + + +///////////////////////// +// 2D Tartan // +///////////////////////// +uint16_t mode_2Dtartan(void) { // By: Elliott Kember https://editor.soulmatelights.com/gallery/3-tartan , Modified by: Andrew Tuline + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + uint8_t hue, bri; + size_t intensity; + int offsetX = beatsin16(3, -360, 360); + int offsetY = beatsin16(2, -360, 360); + int sharpness = SEGMENT.custom3 / 8; // 0-3 + + for (int x = 0; x < cols; x++) { + for (int y = 0; y < rows; y++) { + hue = x * beatsin16(10, 1, 10) + offsetY; + intensity = bri = sin8(x * SEGMENT.speed/2 + offsetX); + for (int i=0; i>= 8*sharpness; + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND)); + hue = y * 3 + offsetX; + intensity = bri = sin8(y * SEGMENT.intensity/2 + offsetY); + for (int i=0; i>= 8*sharpness; + SEGMENT.addPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND)); + } + } + + return FRAMETIME; +} // mode_2DTartan() +static const char _data_FX_MODE_2DTARTAN[] PROGMEM = "Tartan@X scale,Y scale,,,Sharpness;;!;2"; + + +///////////////////////// +// 2D spaceships // +///////////////////////// +uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [https://editor.soulmatelights.com/gallery/639-space-ships], adapted by Blaz Kristan (AKA blazoncek) + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + uint32_t tb = strip.now >> 12; // every ~4s + if (tb > SEGENV.step) { + int8_t dir = ++SEGENV.aux0; + dir += (int)random8(3)-1; + if (dir > 7) SEGENV.aux0 = 0; + else if (dir < 0) SEGENV.aux0 = 7; + else SEGENV.aux0 = dir; + SEGENV.step = tb + random8(4); + } + + SEGMENT.fadeToBlackBy(map(SEGMENT.speed, 0, 255, 248, 16)); + SEGMENT.move(SEGENV.aux0, 1); + + for (size_t i = 0; i < 8; i++) { + byte x = beatsin8(12 + i, 2, cols - 3); + byte y = beatsin8(15 + i, 2, rows - 3); + CRGB color = ColorFromPalette(SEGPALETTE, beatsin8(12 + i, 0, 255), 255); + SEGMENT.addPixelColorXY(x, y, color); + if (cols > 24 || rows > 24) { + SEGMENT.addPixelColorXY(x+1, y, color); + SEGMENT.addPixelColorXY(x-1, y, color); + SEGMENT.addPixelColorXY(x, y+1, color); + SEGMENT.addPixelColorXY(x, y-1, color); + } + } + SEGMENT.blur(SEGMENT.intensity>>3); + + return FRAMETIME; +} +static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur;;!;2"; + + +///////////////////////// +// 2D Crazy Bees // +///////////////////////// +//// Crazy bees by stepko (c)12.02.21 [https://editor.soulmatelights.com/gallery/651-crazy-bees], adapted by Blaz Kristan (AKA blazoncek) +#define MAX_BEES 5 +uint16_t mode_2Dcrazybees(void) { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + byte n = MIN(MAX_BEES, (rows * cols) / 256 + 1); + + typedef struct Bee { + uint8_t posX, posY, aimX, aimY, hue; + int8_t deltaX, deltaY, signX, signY, error; + void aimed(uint16_t w, uint16_t h) { + random16_set_seed(millis()); + aimX = random8(0, w); + aimY = random8(0, h); + hue = random8(); + deltaX = abs(aimX - posX); + deltaY = abs(aimY - posY); + signX = posX < aimX ? 1 : -1; + signY = posY < aimY ? 1 : -1; + error = deltaX - deltaY; + }; + } bee_t; + + if (!SEGENV.allocateData(sizeof(bee_t)*MAX_BEES)) return mode_static(); //allocation failed + bee_t *bee = reinterpret_cast(SEGENV.data); + + if (SEGENV.call == 0) { + for (size_t i = 0; i < n; i++) { + bee[i].posX = random8(0, cols); + bee[i].posY = random8(0, rows); + bee[i].aimed(cols, rows); + } + } + + if (millis() > SEGENV.step) { + SEGENV.step = millis() + (FRAMETIME * 16 / ((SEGMENT.speed>>4)+1)); + + SEGMENT.fadeToBlackBy(32); + + for (size_t i = 0; i < n; i++) { + SEGMENT.addPixelColorXY(bee[i].aimX + 1, bee[i].aimY, CHSV(bee[i].hue, 255, 255)); + SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY + 1, CHSV(bee[i].hue, 255, 255)); + SEGMENT.addPixelColorXY(bee[i].aimX - 1, bee[i].aimY, CHSV(bee[i].hue, 255, 255)); + SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, CHSV(bee[i].hue, 255, 255)); + if (bee[i].posX != bee[i].aimX || bee[i].posY != bee[i].aimY) { + SEGMENT.setPixelColorXY(bee[i].posX, bee[i].posY, CRGB(CHSV(bee[i].hue, 60, 255))); + int8_t error2 = bee[i].error * 2; + if (error2 > -bee[i].deltaY) { + bee[i].error -= bee[i].deltaY; + bee[i].posX += bee[i].signX; + } + if (error2 < bee[i].deltaX) { + bee[i].error += bee[i].deltaX; + bee[i].posY += bee[i].signY; + } + } else { + bee[i].aimed(cols, rows); + } + } + SEGMENT.blur(SEGMENT.intensity>>4); + } + return FRAMETIME; +} +static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; + + +///////////////////////// +// 2D Ghost Rider // +///////////////////////// +//// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek) +#define LIGHTERS_AM 64 // max lighters (adequate for 32x32 matrix) +uint16_t mode_2Dghostrider(void) { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + typedef struct Lighter { + int16_t gPosX; + int16_t gPosY; + uint16_t gAngle; + int8_t angleSpeed; + uint16_t lightersPosX[LIGHTERS_AM]; + uint16_t lightersPosY[LIGHTERS_AM]; + uint16_t Angle[LIGHTERS_AM]; + uint16_t time[LIGHTERS_AM]; + bool reg[LIGHTERS_AM]; + int8_t Vspeed; + } lighter_t; + + if (!SEGENV.allocateData(sizeof(lighter_t))) return mode_static(); //allocation failed + lighter_t *lighter = reinterpret_cast(SEGENV.data); + + const size_t maxLighters = min(cols + rows, LIGHTERS_AM); + + if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { + SEGENV.aux0 = cols; + SEGENV.aux1 = rows; + random16_set_seed(strip.now); + lighter->angleSpeed = random8(0,20) - 10; + lighter->gAngle = random16(); + lighter->Vspeed = 5; + lighter->gPosX = (cols/2) * 10; + lighter->gPosY = (rows/2) * 10; + for (size_t i = 0; i < maxLighters; i++) { + lighter->lightersPosX[i] = lighter->gPosX; + lighter->lightersPosY[i] = lighter->gPosY + i; + lighter->time[i] = i * 2; + lighter->reg[i] = false; + } + } + + if (millis() > SEGENV.step) { + SEGENV.step = millis() + 1024 / (cols+rows); + + SEGMENT.fadeToBlackBy((SEGMENT.speed>>2)+64); + + CRGB color = CRGB::White; + SEGMENT.wu_pixel(lighter->gPosX * 256 / 10, lighter->gPosY * 256 / 10, color); + + lighter->gPosX += lighter->Vspeed * sin_t(radians(lighter->gAngle)); + lighter->gPosY += lighter->Vspeed * cos_t(radians(lighter->gAngle)); + lighter->gAngle += lighter->angleSpeed; + if (lighter->gPosX < 0) lighter->gPosX = (cols - 1) * 10; + if (lighter->gPosX > (cols - 1) * 10) lighter->gPosX = 0; + if (lighter->gPosY < 0) lighter->gPosY = (rows - 1) * 10; + if (lighter->gPosY > (rows - 1) * 10) lighter->gPosY = 0; + for (size_t i = 0; i < maxLighters; i++) { + lighter->time[i] += random8(5, 20); + if (lighter->time[i] >= 255 || + (lighter->lightersPosX[i] <= 0) || + (lighter->lightersPosX[i] >= (cols - 1) * 10) || + (lighter->lightersPosY[i] <= 0) || + (lighter->lightersPosY[i] >= (rows - 1) * 10)) { + lighter->reg[i] = true; + } + if (lighter->reg[i]) { + lighter->lightersPosY[i] = lighter->gPosY; + lighter->lightersPosX[i] = lighter->gPosX; + lighter->Angle[i] = lighter->gAngle + random(-10, 10); + lighter->time[i] = 0; + lighter->reg[i] = false; + } else { + lighter->lightersPosX[i] += -7 * sin_t(radians(lighter->Angle[i])); + lighter->lightersPosY[i] += -7 * cos_t(radians(lighter->Angle[i])); + } + SEGMENT.wu_pixel(lighter->lightersPosX[i] * 256 / 10, lighter->lightersPosY[i] * 256 / 10, ColorFromPalette(SEGPALETTE, (256 - lighter->time[i]))); + } + SEGMENT.blur(SEGMENT.intensity>>3); + } + + return FRAMETIME; +} +static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; + + +//////////////////////////// +// 2D Floating Blobs // +//////////////////////////// +//// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek) +#define MAX_BLOBS 8 +uint16_t mode_2Dfloatingblobs(void) { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + typedef struct Blob { + float x[MAX_BLOBS], y[MAX_BLOBS]; + float sX[MAX_BLOBS], sY[MAX_BLOBS]; // speed + float r[MAX_BLOBS]; + bool grow[MAX_BLOBS]; + byte color[MAX_BLOBS]; + } blob_t; + + uint8_t Amount = (SEGMENT.intensity>>5) + 1; // NOTE: be sure to update MAX_BLOBS if you change this + + if (!SEGENV.allocateData(sizeof(blob_t))) return mode_static(); //allocation failed + blob_t *blob = reinterpret_cast(SEGENV.data); + + if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { + SEGENV.aux0 = cols; // re-initialise if virtual size changes + SEGENV.aux1 = rows; + //SEGMENT.fill(BLACK); + for (size_t i = 0; i < MAX_BLOBS; i++) { + blob->r[i] = random8(1, cols>8 ? (cols/4) : 2); + blob->sX[i] = (float) random8(3, cols) / (float)(256 - SEGMENT.speed); // speed x + blob->sY[i] = (float) random8(3, rows) / (float)(256 - SEGMENT.speed); // speed y + blob->x[i] = random8(0, cols-1); + blob->y[i] = random8(0, rows-1); + blob->color[i] = random8(); + blob->grow[i] = (blob->r[i] < 1.f); + if (blob->sX[i] == 0) blob->sX[i] = 1; + if (blob->sY[i] == 0) blob->sY[i] = 1; + } + } + + SEGMENT.fadeToBlackBy((SEGMENT.custom2>>3)+1); + + // Bounce balls around + for (size_t i = 0; i < Amount; i++) { + if (SEGENV.step < millis()) blob->color[i] = add8(blob->color[i], 4); // slowly change color + // change radius if needed + if (blob->grow[i]) { + // enlarge radius until it is >= 4 + blob->r[i] += (fabsf(blob->sX[i]) > fabsf(blob->sY[i]) ? fabsf(blob->sX[i]) : fabsf(blob->sY[i])) * 0.05f; + if (blob->r[i] >= MIN(cols/4.f,2.f)) { + blob->grow[i] = false; + } + } else { + // reduce radius until it is < 1 + blob->r[i] -= (fabsf(blob->sX[i]) > fabsf(blob->sY[i]) ? fabsf(blob->sX[i]) : fabsf(blob->sY[i])) * 0.05f; + if (blob->r[i] < 1.f) { + blob->grow[i] = true; + } + } + uint32_t c = SEGMENT.color_from_palette(blob->color[i], false, false, 0); + if (blob->r[i] > 1.f) SEGMENT.fill_circle(blob->x[i], blob->y[i], roundf(blob->r[i]), c); + else SEGMENT.setPixelColorXY(blob->x[i], blob->y[i], c); + // move x + if (blob->x[i] + blob->r[i] >= cols - 1) blob->x[i] += (blob->sX[i] * ((cols - 1 - blob->x[i]) / blob->r[i] + 0.005f)); + else if (blob->x[i] - blob->r[i] <= 0) blob->x[i] += (blob->sX[i] * (blob->x[i] / blob->r[i] + 0.005f)); + else blob->x[i] += blob->sX[i]; + // move y + if (blob->y[i] + blob->r[i] >= rows - 1) blob->y[i] += (blob->sY[i] * ((rows - 1 - blob->y[i]) / blob->r[i] + 0.005f)); + else if (blob->y[i] - blob->r[i] <= 0) blob->y[i] += (blob->sY[i] * (blob->y[i] / blob->r[i] + 0.005f)); + else blob->y[i] += blob->sY[i]; + // bounce x + if (blob->x[i] < 0.01f) { + blob->sX[i] = (float)random8(3, cols) / (256 - SEGMENT.speed); + blob->x[i] = 0.01f; + } else if (blob->x[i] > (float)cols - 1.01f) { + blob->sX[i] = (float)random8(3, cols) / (256 - SEGMENT.speed); + blob->sX[i] = -blob->sX[i]; + blob->x[i] = (float)cols - 1.01f; + } + // bounce y + if (blob->y[i] < 0.01f) { + blob->sY[i] = (float)random8(3, rows) / (256 - SEGMENT.speed); + blob->y[i] = 0.01f; + } else if (blob->y[i] > (float)rows - 1.01f) { + blob->sY[i] = (float)random8(3, rows) / (256 - SEGMENT.speed); + blob->sY[i] = -blob->sY[i]; + blob->y[i] = (float)rows - 1.01f; + } + } + SEGMENT.blur(SEGMENT.custom1>>2); + + if (SEGENV.step < millis()) SEGENV.step = millis() + 2000; // change colors every 2 seconds + + return FRAMETIME; +} +#undef MAX_BLOBS +static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; + + +//////////////////////////// +// 2D Scrolling text // +//////////////////////////// +uint16_t mode_2Dscrollingtext(void) { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + int letterWidth, rotLW; + int letterHeight, rotLH; + switch (map(SEGMENT.custom2, 0, 255, 1, 5)) { + default: + case 1: letterWidth = 4; letterHeight = 6; break; + case 2: letterWidth = 5; letterHeight = 8; break; + case 3: letterWidth = 6; letterHeight = 8; break; + case 4: letterWidth = 7; letterHeight = 9; break; + case 5: letterWidth = 5; letterHeight = 12; break; + } + // letters are rotated + if (((SEGMENT.custom3+1)>>3) % 2) { + rotLH = letterWidth; + rotLW = letterHeight; + } else { + rotLW = letterWidth; + rotLH = letterHeight; + } + + char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; + if (SEGMENT.name) for (size_t i=0,j=0; i31 && SEGMENT.name[i]<128) text[j++] = SEGMENT.name[i]; + const bool zero = strchr(text, '0') != nullptr; + + char sec[5]; + int AmPmHour = hour(localTime); + bool isitAM = true; + if (useAMPM) { + if (AmPmHour > 11) { AmPmHour -= 12; isitAM = false; } + if (AmPmHour == 0) { AmPmHour = 12; } + sprintf_P(sec, PSTR(" %2s"), (isitAM ? "AM" : "PM")); + } else { + sprintf_P(sec, PSTR(":%02d"), second(localTime)); + } + + if (!strlen(text)) { // fallback if empty segment name: display date and time + sprintf_P(text, PSTR("%s %d, %d %d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec); + } else { + if (!strncmp_P(text,PSTR("#DATE"),5)) sprintf_P(text, zero?PSTR("%02d.%02d.%04d"):PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime)); + else if (!strncmp_P(text,PSTR("#DDMM"),5)) sprintf_P(text, zero?PSTR("%02d.%02d") :PSTR("%d.%d"), day(localTime), month(localTime)); + else if (!strncmp_P(text,PSTR("#MMDD"),5)) sprintf_P(text, zero?PSTR("%02d/%02d") :PSTR("%d/%d"), month(localTime), day(localTime)); + else if (!strncmp_P(text,PSTR("#TIME"),5)) sprintf_P(text, zero?PSTR("%02d:%02d%s") :PSTR("%2d:%02d%s"), AmPmHour, minute(localTime), sec); + else if (!strncmp_P(text,PSTR("#HHMM"),5)) sprintf_P(text, zero?PSTR("%02d:%02d") :PSTR("%d:%02d"), AmPmHour, minute(localTime)); + else if (!strncmp_P(text,PSTR("#HH"),3)) sprintf_P(text, zero?PSTR("%02d") :PSTR("%d"), AmPmHour); + else if (!strncmp_P(text,PSTR("#MM"),3)) sprintf_P(text, zero?PSTR("%02d") :PSTR("%d"), minute(localTime)); + } + + const int numberOfLetters = strlen(text); + const unsigned long now = millis(); // reduce millis() calls + int width = (numberOfLetters * rotLW); + int yoffset = map(SEGMENT.intensity, 0, 255, -rows/2, rows/2) + (rows-rotLH)/2; + if (width <= cols) { + // scroll vertically (e.g. ^^ Way out ^^) if it fits + int speed = map(SEGMENT.speed, 0, 255, 5000, 1000); + int frac = now % speed + 1; + if (SEGMENT.intensity == 255) { + yoffset = (2 * frac * rows)/speed - rows; + } else if (SEGMENT.intensity == 0) { + yoffset = rows - (2 * frac * rows)/speed; + } + } + + if (SEGENV.step < now) { + // calculate start offset + if (width > cols) { + if (SEGMENT.check3) { + if (SEGENV.aux0 == 0) SEGENV.aux0 = width + cols - 1; + else --SEGENV.aux0; + } else ++SEGENV.aux0 %= width + cols; + } else SEGENV.aux0 = (cols + width)/2; + ++SEGENV.aux1 &= 0xFF; // color shift + SEGENV.step = now + map(SEGMENT.speed, 0, 255, 250, 50); // shift letters every ~250ms to ~50ms + } + + if (!SEGMENT.check2) SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail + + for (int i = 0; i < numberOfLetters; i++) { + int xoffset = int(cols) - int(SEGENV.aux0) + rotLW*i; + if (xoffset + rotLW < 0) continue; // don't draw characters off-screen + uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); + uint32_t col2 = BLACK; + if (SEGMENT.check1 && SEGMENT.palette == 0) { + col1 = SEGCOLOR(0); + col2 = SEGCOLOR(2); + } + SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, map(SEGMENT.custom3, 0, 31, -2, 2)); + } + + return FRAMETIME; +} +static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,Overlay,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; + + +//////////////////////////// +// 2D Drift Rose // +//////////////////////////// +//// Drift Rose by stepko (c)2021 [https://editor.soulmatelights.com/gallery/1369-drift-rose-pattern], adapted by Blaz Kristan (AKA blazoncek) +uint16_t mode_2Ddriftrose(void) { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + const float CX = (cols-cols%2)/2.f - .5f; + const float CY = (rows-rows%2)/2.f - .5f; + const float L = min(cols, rows) / 2.f; + + SEGMENT.fadeToBlackBy(32+(SEGMENT.speed>>3)); + for (size_t i = 1; i < 37; i++) { + uint32_t x = (CX + (sin_t(radians(i * 10)) * (beatsin8(i, 0, L*2)-L))) * 255.f; + uint32_t y = (CY + (cos_t(radians(i * 10)) * (beatsin8(i, 0, L*2)-L))) * 255.f; + SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); + } + SEGMENT.blur((SEGMENT.intensity>>4)+1); + + return FRAMETIME; +} +static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;;2"; + +#endif // WLED_DISABLE_2D + + +/////////////////////////////////////////////////////////////////////////////// +/******************** audio enhanced routines ************************/ +/////////////////////////////////////////////////////////////////////////////// + + +/* use the following code to pass AudioReactive usermod variables to effect + + uint8_t *binNum = (uint8_t*)&SEGENV.aux1, *maxVol = (uint8_t*)(&SEGENV.aux1+1); // just in case assignment + bool samplePeak = false; + float FFT_MajorPeak = 1.0; + uint8_t *fftResult = nullptr; + float *fftBin = nullptr; + um_data_t *um_data; + if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + volumeSmth = *(float*) um_data->u_data[0]; + volumeRaw = *(float*) um_data->u_data[1]; + fftResult = (uint8_t*) um_data->u_data[2]; + samplePeak = *(uint8_t*) um_data->u_data[3]; + FFT_MajorPeak = *(float*) um_data->u_data[4]; + my_magnitude = *(float*) um_data->u_data[5]; + maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element + binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element + fftBin = (float*) um_data->u_data[8]; + } else { + // add support for no audio data + um_data = simulateSound(SEGMENT.soundSim); + } +*/ + + +// a few constants needed for AudioReactive effects + +// for 22Khz sampling +#define MAX_FREQUENCY 11025 // sample frequency / 2 (as per Nyquist criterion) +#define MAX_FREQ_LOG10 4.04238f // log10(MAX_FREQUENCY) + +// for 20Khz sampling +//#define MAX_FREQUENCY 10240 +//#define MAX_FREQ_LOG10 4.0103f + +// for 10Khz sampling +//#define MAX_FREQUENCY 5120 +//#define MAX_FREQ_LOG10 3.71f + + +///////////////////////////////// +// * Ripple Peak // +///////////////////////////////// +uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuline. + // This currently has no controls. + #define maxsteps 16 // Case statement wouldn't allow a variable. + + uint16_t maxRipples = 16; + uint16_t dataSize = sizeof(Ripple) * maxRipples; + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + Ripple* ripples = reinterpret_cast(SEGENV.data); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; + #ifdef ESP32 + float FFT_MajorPeak = *(float*) um_data->u_data[4]; + #endif + uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; + uint8_t *binNum = (uint8_t*)um_data->u_data[7]; + + // printUmData(); + + if (SEGENV.call == 0) { + SEGENV.aux0 = 255; + SEGMENT.custom1 = *binNum; + SEGMENT.custom2 = *maxVol * 2; + } + + *binNum = SEGMENT.custom1; // Select a bin. + *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. + + SEGMENT.fade_out(240); // Lower frame rate means less effective fading than FastLED + SEGMENT.fade_out(240); + + for (int i = 0; i < SEGMENT.intensity/16; i++) { // Limit the number of ripples. + if (samplePeak) ripples[i].state = 255; + + switch (ripples[i].state) { + case 254: // Inactive mode + break; + + case 255: // Initialize ripple variables. + ripples[i].pos = random16(SEGLEN); + #ifdef ESP32 + if (FFT_MajorPeak > 1) // log10(0) is "forbidden" (throws exception) + ripples[i].color = (int)(log10f(FFT_MajorPeak)*128); + else ripples[i].color = 0; + #else + ripples[i].color = random8(); + #endif + ripples[i].state = 0; + break; + + case 0: + SEGMENT.setPixelColor(ripples[i].pos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), SEGENV.aux0)); + ripples[i].state++; + break; + + case maxsteps: // At the end of the ripples. 254 is an inactive mode. + ripples[i].state = 254; + break; + + default: // Middle of the ripples. + SEGMENT.setPixelColor((ripples[i].pos + ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), SEGENV.aux0/ripples[i].state*2)); + SEGMENT.setPixelColor((ripples[i].pos - ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), SEGENV.aux0/ripples[i].state*2)); + ripples[i].state++; // Next step. + break; + } // switch step + } // for i + + return FRAMETIME; +} // mode_ripplepeak() +static const char _data_FX_MODE_RIPPLEPEAK[] PROGMEM = "Ripple Peak@Fade rate,Max # of ripples,Select bin,Volume (min);!,!;!;1v;c2=0,m12=0,si=0"; // Pixel, Beatsin + + +#ifndef WLED_DISABLE_2D +///////////////////////// +// * 2D Swirl // +///////////////////////// +// By: Mark Kriegsman https://gist.github.com/kriegsman/5adca44e14ad025e6d3b , modified by Andrew Tuline +uint16_t mode_2DSwirl(void) { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + const uint8_t borderWidth = 2; + + SEGMENT.blur(SEGMENT.custom1); + + uint8_t i = beatsin8( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth); + uint8_t j = beatsin8( 41*SEGMENT.speed/255, borderWidth, rows - borderWidth); + uint8_t ni = (cols - 1) - i; + uint8_t nj = (cols - 1) - j; + uint16_t ms = millis(); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? + int16_t volumeRaw = *(int16_t*) um_data->u_data[1]; + + SEGMENT.addPixelColorXY( i, j, ColorFromPalette(SEGPALETTE, (ms / 11 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 11, 200, 255); + SEGMENT.addPixelColorXY( j, i, ColorFromPalette(SEGPALETTE, (ms / 13 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 13, 200, 255); + SEGMENT.addPixelColorXY(ni,nj, ColorFromPalette(SEGPALETTE, (ms / 17 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 17, 200, 255); + SEGMENT.addPixelColorXY(nj,ni, ColorFromPalette(SEGPALETTE, (ms / 29 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 29, 200, 255); + SEGMENT.addPixelColorXY( i,nj, ColorFromPalette(SEGPALETTE, (ms / 37 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 37, 200, 255); + SEGMENT.addPixelColorXY(ni, j, ColorFromPalette(SEGPALETTE, (ms / 41 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 41, 200, 255); + + return FRAMETIME; +} // mode_2DSwirl() +static const char _data_FX_MODE_2DSWIRL[] PROGMEM = "Swirl@!,Sensitivity,Blur;,Bg Swirl;!;2v;ix=64,si=0"; // Beatsin // TODO: color 1 unused? + + +///////////////////////// +// * 2D Waverly // +///////////////////////// +// By: Stepko, https://editor.soulmatelights.com/gallery/652-wave , modified by Andrew Tuline +uint16_t mode_2DWaverly(void) { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + + SEGMENT.fadeToBlackBy(SEGMENT.speed); + + long t = millis() / 2; + for (int i = 0; i < cols; i++) { + uint16_t thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; + // use audio if available + if (um_data) { + thisVal /= 32; // reduce intensity of inoise8() + thisVal *= volumeSmth; + } + uint16_t thisMax = map(thisVal, 0, 512, 0, rows); + + for (int j = 0; j < thisMax; j++) { + SEGMENT.addPixelColorXY(i, j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); + SEGMENT.addPixelColorXY((cols - 1) - i, (rows - 1) - j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); + } + } + SEGMENT.blur(16); + + return FRAMETIME; +} // mode_2DWaverly() +static const char _data_FX_MODE_2DWAVERLY[] PROGMEM = "Waverly@Amplification,Sensitivity;;!;2v;ix=64,si=0"; // Beatsin + +#endif // WLED_DISABLE_2D + +// float version of map() +static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + +// Gravity struct requited for GRAV* effects +typedef struct Gravity { + int topLED; + int gravityCounter; +} gravity; + +/////////////////////// +// * GRAVCENTER // +/////////////////////// +uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. + if (SEGLEN == 1) return mode_static(); + + const uint16_t dataSize = sizeof(gravity); + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + Gravity* gravcen = reinterpret_cast(SEGENV.data); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + + //SEGMENT.fade_out(240); + SEGMENT.fade_out(251); // 30% + + float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; + segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivity" upscaling + + float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 32, 0, (float)SEGLEN/2.0f); // map to pixels available in current segment + uint16_t tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. + uint8_t gravity = 8 - SEGMENT.speed/32; + + for (int i=0; i= gravcen->topLED) + gravcen->topLED = tempsamp-1; + else if (gravcen->gravityCounter % gravity == 0) + gravcen->topLED--; + + if (gravcen->topLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0)); + } + gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; + + return FRAMETIME; +} // mode_gravcenter() +static const char _data_FX_MODE_GRAVCENTER[] PROGMEM = "Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin + + +/////////////////////// +// * GRAVCENTRIC // +/////////////////////// +uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew Tuline. + if (SEGLEN == 1) return mode_static(); + + uint16_t dataSize = sizeof(gravity); + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + Gravity* gravcen = reinterpret_cast(SEGENV.data); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + + // printUmData(); + + //SEGMENT.fade_out(240); + //SEGMENT.fade_out(240); // twice? really? + SEGMENT.fade_out(253); // 50% + + float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; + segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling + + float mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0f); // map to pixels availeable in current segment + int tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. + uint8_t gravity = 8 - SEGMENT.speed/32; + + for (int i=0; i= gravcen->topLED) + gravcen->topLED = tempsamp-1; + else if (gravcen->gravityCounter % gravity == 0) + gravcen->topLED--; + + if (gravcen->topLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); + } + gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; + + return FRAMETIME; +} // mode_gravcentric() +static const char _data_FX_MODE_GRAVCENTRIC[] PROGMEM = "Gravcentric@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=3,si=0"; // Corner, Beatsin + + +/////////////////////// +// * GRAVIMETER // +/////////////////////// +uint16_t mode_gravimeter(void) { // Gravmeter. By Andrew Tuline. + if (SEGLEN == 1) return mode_static(); + + uint16_t dataSize = sizeof(gravity); + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + Gravity* gravcen = reinterpret_cast(SEGENV.data); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + + //SEGMENT.fade_out(240); + SEGMENT.fade_out(249); // 25% + + float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0; + segmentSampleAvg *= 0.25; // divide by 4, to compensate for later "sensitivity" upscaling + + float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment + int tempsamp = constrain(mySampleAvg,0,SEGLEN-1); // Keep the sample from overflowing. + uint8_t gravity = 8 - SEGMENT.speed/32; + + for (int i=0; i= gravcen->topLED) + gravcen->topLED = tempsamp; + else if (gravcen->gravityCounter % gravity == 0) + gravcen->topLED--; + + if (gravcen->topLED > 0) { + SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0)); + } + gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; + + return FRAMETIME; +} // mode_gravimeter() +static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin + + +////////////////////// +// * JUGGLES // +////////////////////// +uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. + if (SEGLEN == 1) return mode_static(); + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + + SEGMENT.fade_out(224); // 6.25% + uint16_t my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); + + for (size_t i=0; iu_data[1]; + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; + if(SEGENV.aux0 != secondHand) { + SEGENV.aux0 = secondHand; + + int pixBri = volumeRaw * SEGMENT.intensity / 64; + for (int i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left + SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0), pixBri)); + } + + return FRAMETIME; +} // mode_matripix() +static const char _data_FX_MODE_MATRIPIX[] PROGMEM = "Matripix@!,Brightness;!,!;!;1v;ix=64,m12=2,si=1"; //,rev=1,mi=1,rY=1,mY=1 Circle, WeWillRockYou, reverseX + + +////////////////////// +// * MIDNOISE // +////////////////////// +uint16_t mode_midnoise(void) { // Midnoise. By Andrew Tuline. + if (SEGLEN == 1) return mode_static(); +// Changing xdist to SEGENV.aux0 and ydist to SEGENV.aux1. + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + + SEGMENT.fade_out(SEGMENT.speed); + SEGMENT.fade_out(SEGMENT.speed); + + float tmpSound2 = volumeSmth * (float)SEGMENT.intensity / 256.0; // Too sensitive. + tmpSound2 *= (float)SEGMENT.intensity / 128.0; // Reduce sensitivity/length. + + int maxLen = mapf(tmpSound2, 0, 127, 0, SEGLEN/2); + if (maxLen >SEGLEN/2) maxLen = SEGLEN/2; + + for (int i=(SEGLEN/2-maxLen); i<(SEGLEN/2+maxLen); i++) { + uint8_t index = inoise8(i*volumeSmth+SEGENV.aux0, SEGENV.aux1+i*volumeSmth); // Get a value from the noise function. I'm using both x and y axis. + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); + } + + SEGENV.aux0=SEGENV.aux0+beatsin8(5,0,10); + SEGENV.aux1=SEGENV.aux1+beatsin8(4,0,10); + + return FRAMETIME; +} // mode_midnoise() +static const char _data_FX_MODE_MIDNOISE[] PROGMEM = "Midnoise@Fade rate,Max. length;!,!;!;1v;ix=128,m12=1,si=0"; // Bar, Beatsin + + +////////////////////// +// * NOISEFIRE // +////////////////////// +// I am the god of hellfire. . . Volume (only) reactive fire routine. Oh, look how short this is. +uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. + CRGBPalette16 myPal = CRGBPalette16(CHSV(0,255,2), CHSV(0,255,4), CHSV(0,255,8), CHSV(0, 255, 8), // Fire palette definition. Lower value = darker. + CHSV(0, 255, 16), CRGB::Red, CRGB::Red, CRGB::Red, + CRGB::DarkOrange, CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, + CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + + if (SEGENV.call == 0) SEGMENT.fill(BLACK); + + for (int i = 0; i < SEGLEN; i++) { + uint16_t index = inoise8(i*SEGMENT.speed/64,millis()*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline. + index = (255 - i*256/SEGLEN) * index/(256-SEGMENT.intensity); // Now we need to scale index so that it gets blacker as we get close to one of the ends. + // This is a simple y=mx+b equation that's been scaled. index/128 is another scaling. + + CRGB color = ColorFromPalette(myPal, index, volumeSmth*2, LINEARBLEND); // Use the my own palette. + SEGMENT.setPixelColor(i, color); + } + + return FRAMETIME; +} // mode_noisefire() +static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;1v;m12=2,si=0"; // Circle, Beatsin + + +/////////////////////// +// * Noisemeter // +/////////////////////// +uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + int16_t volumeRaw = *(int16_t*)um_data->u_data[1]; + + //uint8_t fadeRate = map(SEGMENT.speed,0,255,224,255); + uint8_t fadeRate = map(SEGMENT.speed,0,255,200,254); + SEGMENT.fade_out(fadeRate); + + float tmpSound2 = volumeRaw * 2.0 * (float)SEGMENT.intensity / 255.0; + int maxLen = mapf(tmpSound2, 0, 255, 0, SEGLEN); // map to pixels availeable in current segment // Still a bit too sensitive. + if (maxLen <0) maxLen = 0; + if (maxLen >SEGLEN) maxLen = SEGLEN; + + for (int i=0; iu_data[1]; + + uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 16; + if (SEGENV.aux0 != secondHand) { + SEGENV.aux0 = secondHand; + + int pixBri = volumeRaw * SEGMENT.intensity / 64; + + SEGMENT.setPixelColor(SEGLEN/2, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0), pixBri)); + for (int i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left + for (int i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right + } + + return FRAMETIME; +} // mode_pixelwave() +static const char _data_FX_MODE_PIXELWAVE[] PROGMEM = "Pixelwave@!,Sensitivity;!,!;!;1v;ix=64,m12=2,si=0"; // Circle, Beatsin + + +////////////////////// +// * PLASMOID // +////////////////////// +typedef struct Plasphase { + int16_t thisphase; + int16_t thatphase; +} plasphase; + +uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. + // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment + if (!SEGENV.allocateData(sizeof(plasphase))) return mode_static(); //allocation failed + Plasphase* plasmoip = reinterpret_cast(SEGENV.data); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + + SEGMENT.fadeToBlackBy(32); + + plasmoip->thisphase += beatsin8(6,-4,4); // You can change direction and speed individually. + plasmoip->thatphase += beatsin8(7,-4,4); // Two phase values to make a complex pattern. By Andrew Tuline. + + for (int i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows. + // updated, similar to "plasma" effect - softhack007 + uint8_t thisbright = cubicwave8(((i*(1 + (3*SEGMENT.speed/32)))+plasmoip->thisphase) & 0xFF)/2; + thisbright += cos8(((i*(97 +(5*SEGMENT.speed/32)))+plasmoip->thatphase) & 0xFF)/2; // Let's munge the brightness a bit and animate it all with the phases. + + uint8_t colorIndex=thisbright; + if (volumeSmth * SEGMENT.intensity / 64 < thisbright) {thisbright = 0;} + + SEGMENT.addPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0), thisbright)); + } + + return FRAMETIME; +} // mode_plasmoid() +static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels;!,!;!;1v;sx=128,ix=128,m12=0,si=0"; // Pixels, Beatsin + + +/////////////////////// +// * PUDDLEPEAK // +/////////////////////// +// Andrew's crappy peak detector. If I were 40+ years younger, I'd learn signal processing. +uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. + if (SEGLEN == 1) return mode_static(); + + uint16_t size = 0; + uint8_t fadeVal = map(SEGMENT.speed,0,255, 224, 254); + uint16_t pos = random(SEGLEN); // Set a random starting position. + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; + uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; + uint8_t *binNum = (uint8_t*)um_data->u_data[7]; + float volumeSmth = *(float*) um_data->u_data[0]; + + if (SEGENV.call == 0) { + SEGMENT.custom1 = *binNum; + SEGMENT.custom2 = *maxVol * 2; + } + + *binNum = SEGMENT.custom1; // Select a bin. + *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. + + SEGMENT.fade_out(fadeVal); + + if (samplePeak == 1) { + size = volumeSmth * SEGMENT.intensity /256 /4 + 1; // Determine size of the flash based on the volume. + if (pos+size>= SEGLEN) size = SEGLEN - pos; + } + + for (int i=0; iu_data[1]; + + if (volumeRaw > 1) { + size = volumeRaw * SEGMENT.intensity /256 /8 + 1; // Determine size of the flash based on the volume. + if (pos+size >= SEGLEN) size = SEGLEN - pos; + } + + for (int i=0; i(SEGENV.data); // Used to store a pile of samples because WLED frame rate and WLED sample rate are not synchronized. Frame rate is too low. + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; + + myVals[millis()%32] = volumeSmth; // filling values semi randomly + + SEGMENT.fade_out(64+(SEGMENT.speed>>1)); + + for (int i=0; i u_data[2]; + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + SEGENV.aux0 = 0; + } + + int fadeoutDelay = (256 - SEGMENT.speed) / 32; + if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(SEGMENT.speed); + + SEGENV.step += FRAMETIME; + if (SEGENV.step > SPEED_FORMULA_L) { + uint16_t segLoc = random16(SEGLEN); + SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/max(1, SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), 2*fftResult[SEGENV.aux0%16])); + ++(SEGENV.aux0) %= 16; // make sure it doesn't cross 16 + + SEGENV.step = 1; + SEGMENT.blur(SEGMENT.intensity); + } + + return FRAMETIME; +} // mode_blurz() +static const char _data_FX_MODE_BLURZ[] PROGMEM = "Blurz@Fade rate,Blur;!,Color mix;!;1f;m12=0,si=0"; // Pixels, Beatsin + + +///////////////////////// +// ** DJLight // +///////////////////////// +uint16_t mode_DJLight(void) { // Written by ??? Adapted by Will Tatam. + if (SEGLEN == 1) return mode_static(); + const int mid = SEGLEN / 2; + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 64; + if (SEGENV.aux0 != secondHand) { // Triggered millis timing. + SEGENV.aux0 = secondHand; + + CRGB color = CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2); // 16-> 15 as 16 is out of bounds + SEGMENT.setPixelColor(mid, color.fadeToBlackBy(map(fftResult[4], 0, 255, 255, 4))); // TODO - Update + + for (int i = SEGLEN - 1; i > mid; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); // move to the left + for (int i = 0; i < mid; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right + } + + return FRAMETIME; +} // mode_DJLight() +static const char _data_FX_MODE_DJLIGHT[] PROGMEM = "DJ Light@Speed;;;1f;m12=2,si=0"; // Circle, Beatsin + + +//////////////////// +// ** Freqmap // +//////////////////// +uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. Would be better if a higher framerate. + if (SEGLEN == 1) return mode_static(); + // Start frequency = 60 Hz and log10(60) = 1.78 + // End frequency = MAX_FREQUENCY in Hz and lo10(MAX_FREQUENCY) = MAX_FREQ_LOG10 + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float FFT_MajorPeak = *(float*)um_data->u_data[4]; + float my_magnitude = *(float*)um_data->u_data[5] / 4.0f; + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) + + if (SEGENV.call == 0) SEGMENT.fill(BLACK); + int fadeoutDelay = (256 - SEGMENT.speed) / 32; + if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(SEGMENT.speed); + + int locn = (log10f((float)FFT_MajorPeak) - 1.78f) * (float)SEGLEN/(MAX_FREQ_LOG10 - 1.78f); // log10 frequency range is from 1.78 to 3.71. Let's scale to SEGLEN. + if (locn < 1) locn = 0; // avoid underflow + + if (locn >=SEGLEN) locn = SEGLEN-1; + uint16_t pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. + if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow + + uint16_t bright = (int)my_magnitude; + + SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), bright)); + + return FRAMETIME; +} // mode_freqmap() +static const char _data_FX_MODE_FREQMAP[] PROGMEM = "Freqmap@Fade rate,Starting color;!,!;!;1f;m12=0,si=0"; // Pixels, Beatsin + + +/////////////////////// +// ** Freqmatrix // +/////////////////////// +uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung. + if (SEGLEN == 1) return mode_static(); + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float FFT_MajorPeak = *(float*)um_data->u_data[4]; + float volumeSmth = *(float*)um_data->u_data[0]; + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; + if(SEGENV.aux0 != secondHand) { + SEGENV.aux0 = secondHand; + + uint8_t sensitivity = map(SEGMENT.custom3, 0, 31, 1, 10); // reduced resolution slider + int pixVal = (volumeSmth * SEGMENT.intensity * sensitivity) / 256.0f; + if (pixVal > 255) pixVal = 255; + + float intensity = map(pixVal, 0, 255, 0, 100) / 100.0f; // make a brightness from the last avg + + CRGB color = CRGB::Black; + + if (FFT_MajorPeak > MAX_FREQUENCY) FFT_MajorPeak = 1; + // MajorPeak holds the freq. value which is most abundant in the last sample. + // With our sampling rate of 10240Hz we have a usable freq range from roughly 80Hz to 10240/2 Hz + // we will treat everything with less than 65Hz as 0 + + if (FFT_MajorPeak < 80) { + color = CRGB::Black; + } else { + int upperLimit = 80 + 42 * SEGMENT.custom2; + int lowerLimit = 80 + 3 * SEGMENT.custom1; + uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t + uint16_t b = 255 * intensity; + if (b > 255) b = 255; + color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED + } + + // shift the pixels one pixel up + SEGMENT.setPixelColor(0, color); + for (int i = SEGLEN - 1; i > 0; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left + } + + return FRAMETIME; +} // mode_freqmatrix() +static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound effect,Low bin,High bin,Sensitivity;;;1f;m12=3,si=0"; // Corner, Beatsin + + +////////////////////// +// ** Freqpixels // +////////////////////// +// Start frequency = 60 Hz and log10(60) = 1.78 +// End frequency = 5120 Hz and lo10(5120) = 3.71 +// SEGMENT.speed select faderate +// SEGMENT.intensity select colour index +uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float FFT_MajorPeak = *(float*)um_data->u_data[4]; + float my_magnitude = *(float*)um_data->u_data[5] / 16.0f; + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1.0f; // log10(0) is "forbidden" (throws exception) + + // this code translates to speed * (2 - speed/255) which is a) speed*2 or b) speed (when speed is 255) + // and since fade_out() can only take 0-255 it will behave incorrectly when speed > 127 + //uint16_t fadeRate = 2*SEGMENT.speed - SEGMENT.speed*SEGMENT.speed/255; // Get to 255 as quick as you can. + uint16_t fadeRate = SEGMENT.speed*SEGMENT.speed; // Get to 255 as quick as you can. + fadeRate = map(fadeRate, 0, 65535, 1, 255); + + int fadeoutDelay = (256 - SEGMENT.speed) / 64; + if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(fadeRate); + + uint8_t pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. + if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow + for (int i=0; i < SEGMENT.intensity/32+1; i++) { + uint16_t locn = random16(0,SEGLEN); + SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); + } + + return FRAMETIME; +} // mode_freqpixels() +static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Starting color and # of pixels;!,!,;!;1f;m12=0,si=0"; // Pixels, Beatsin + + +////////////////////// +// ** Freqwave // +////////////////////// +// Assign a color to the central (starting pixels) based on the predominant frequencies and the volume. The color is being determined by mapping the MajorPeak from the FFT +// and then mapping this to the HSV color circle. Currently we are sampling at 10240 Hz, so the highest frequency we can look at is 5120Hz. +// +// SEGMENT.custom1: the lower cut off point for the FFT. (many, most time the lowest values have very little information since they are FFT conversion artifacts. Suggested value is close to but above 0 +// SEGMENT.custom2: The high cut off point. This depends on your sound profile. Most music looks good when this slider is between 50% and 100%. +// SEGMENT.custom3: "preamp" for the audio signal for audio10. +// +// I suggest that for this effect you turn the brightness to 95%-100% but again it depends on your soundprofile you find yourself in. +// Instead of using colorpalettes, This effect works on the HSV color circle with red being the lowest frequency +// +// As a compromise between speed and accuracy we are currently sampling with 10240Hz, from which we can then determine with a 512bin FFT our max frequency is 5120Hz. +// Depending on the music stream you have you might find it useful to change the frequency mapping. +uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschung. + if (SEGLEN == 1) return mode_static(); + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float FFT_MajorPeak = *(float*)um_data->u_data[4]; + float volumeSmth = *(float*)um_data->u_data[0]; + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; + if(SEGENV.aux0 != secondHand) { + SEGENV.aux0 = secondHand; + + float sensitivity = mapf(SEGMENT.custom3, 1, 31, 1, 10); // reduced resolution slider + float pixVal = volumeSmth * (float)SEGMENT.intensity / 256.0f * sensitivity; + if (pixVal > 255) pixVal = 255; + + float intensity = mapf(pixVal, 0, 255, 0, 100) / 100.0f; // make a brightness from the last avg + + CRGB color = 0; + + if (FFT_MajorPeak > MAX_FREQUENCY) FFT_MajorPeak = 1.0f; + // MajorPeak holds the freq. value which is most abundant in the last sample. + // With our sampling rate of 10240Hz we have a usable freq range from roughly 80Hz to 10240/2 Hz + // we will treat everything with less than 65Hz as 0 + + if (FFT_MajorPeak < 80) { + color = CRGB::Black; + } else { + int upperLimit = 80 + 42 * SEGMENT.custom2; + int lowerLimit = 80 + 3 * SEGMENT.custom1; + uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t + uint16_t b = 255.0 * intensity; + if (b > 255) b=255; + color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED + } + + SEGMENT.setPixelColor(SEGLEN/2, color); + + // shift the pixels one pixel outwards + for (int i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left + for (int i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right + } + + return FRAMETIME; +} // mode_freqwave() +static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Speed,Sound effect,Low bin,High bin,Pre-amp;;;1f;m12=2,si=0"; // Circle, Beatsin + + +/////////////////////// +// ** Gravfreq // +/////////////////////// +uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. + if (SEGLEN == 1) return mode_static(); + uint16_t dataSize = sizeof(gravity); + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + Gravity* gravcen = reinterpret_cast(SEGENV.data); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float FFT_MajorPeak = *(float*)um_data->u_data[4]; + float volumeSmth = *(float*)um_data->u_data[0]; + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) + + SEGMENT.fade_out(250); + + float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; + segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling + + float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0,32, 0, (float)SEGLEN/2.0f); // map to pixels availeable in current segment + int tempsamp = constrain(mySampleAvg,0,SEGLEN/2); // Keep the sample from overflowing. + uint8_t gravity = 8 - SEGMENT.speed/32; + + for (int i=0; i= gravcen->topLED) + gravcen->topLED = tempsamp-1; + else if (gravcen->gravityCounter % gravity == 0) + gravcen->topLED--; + + if (gravcen->topLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); + } + gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; + + return FRAMETIME; +} // mode_gravfreq() +static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin + + +////////////////////// +// ** Noisemove // +////////////////////// +uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuline + if (SEGLEN == 1) return mode_static(); + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + int fadeoutDelay = (256 - SEGMENT.speed) / 96; + if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fadeToBlackBy(4+ SEGMENT.speed/4); + + uint8_t numBins = map(SEGMENT.intensity,0,255,0,16); // Map slider to fftResult bins. + for (int i=0; iu_data[4]; + float my_magnitude = *(float*) um_data->u_data[5] / 16.0f; + + SEGMENT.fadeToBlackBy(16); // Just in case something doesn't get faded. + + float frTemp = FFT_MajorPeak; + uint8_t octCount = 0; // Octave counter. + uint8_t volTemp = 0; + + volTemp = 32.0f + my_magnitude * 1.5f; // brightness = volume (overflows are handled in next lines) + if (my_magnitude < 48) volTemp = 0; // We need to squelch out the background noise. + if (my_magnitude > 144) volTemp = 255; // everything above this is full brightness + + while ( frTemp > 249 ) { + octCount++; // This should go up to 5. + frTemp = frTemp/2; + } + + frTemp -= 132.0f; // This should give us a base musical note of C3 + frTemp = fabsf(frTemp * 2.1f); // Fudge factors to compress octave range starting at 0 and going to 255; + + uint16_t i = map(beatsin8(8+octCount*4, 0, 255, 0, octCount*8), 0, 255, 0, SEGLEN-1); + i = constrain(i, 0, SEGLEN-1); + SEGMENT.addPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette((uint8_t)frTemp, false, PALETTE_SOLID_WRAP, 0), volTemp)); + + return FRAMETIME; +} // mode_rocktaves() +static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;1f;m12=1,si=0"; // Bar, Beatsin + + +/////////////////////// +// ** Waterfall // +/////////////////////// +// Combines peak detection with FFT_MajorPeak and FFT_Magnitude. +uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tuline + if (SEGLEN == 1) return mode_static(); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; + float FFT_MajorPeak = *(float*) um_data->u_data[4]; + uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; + uint8_t *binNum = (uint8_t*)um_data->u_data[7]; + float my_magnitude = *(float*) um_data->u_data[5] / 8.0f; + + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + SEGENV.aux0 = 255; + SEGMENT.custom1 = *binNum; + SEGMENT.custom2 = *maxVol * 2; + } + + *binNum = SEGMENT.custom1; // Select a bin. + *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. + + uint8_t secondHand = micros() / (256-SEGMENT.speed)/500 + 1 % 16; + if (SEGENV.aux0 != secondHand) { // Triggered millis timing. + SEGENV.aux0 = secondHand; + + //uint8_t pixCol = (log10f((float)FFT_MajorPeak) - 2.26f) * 177; // 10Khz sampling - log10 frequency range is from 2.26 (182hz) to 3.7 (5012hz). Let's scale accordingly. + uint8_t pixCol = (log10f(FFT_MajorPeak) - 2.26f) * 150; // 22Khz sampling - log10 frequency range is from 2.26 (182hz) to 3.967 (9260hz). Let's scale accordingly. + if (FFT_MajorPeak < 182.0f) pixCol = 0; // handle underflow + + if (samplePeak) { + SEGMENT.setPixelColor(SEGLEN-1, CHSV(92,92,92)); + } else { + SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); + } + for (int i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left + } + + return FRAMETIME; +} // mode_waterfall() +static const char _data_FX_MODE_WATERFALL[] PROGMEM = "Waterfall@!,Adjust color,Select bin,Volume (min);!,!;!;1f;c2=0,m12=2,si=0"; // Circles, Beatsin + + +#ifndef WLED_DISABLE_2D +///////////////////////// +// ** 2D GEQ // +///////////////////////// +uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const int NUM_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (!SEGENV.allocateData(cols*sizeof(uint16_t))) return mode_static(); //allocation failed + uint16_t *previousBarHeight = reinterpret_cast(SEGENV.data); //array of previous bar heights per frequency band + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + if (SEGENV.call == 0) for (int i=0; i= (256U - SEGMENT.intensity)) { + SEGENV.step = millis(); + rippleTime = true; + } + + int fadeoutDelay = (256 - SEGMENT.speed) / 64; + if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fadeToBlackBy(SEGMENT.speed); + + for (int x=0; x < cols; x++) { + uint8_t band = map(x, 0, cols-1, 0, NUM_BANDS - 1); + if (NUM_BANDS < 16) band = map(band, 0, NUM_BANDS - 1, 0, 15); // always use full range. comment out this line to get the previous behaviour. + band = constrain(band, 0, 15); + uint16_t colorIndex = band * 17; + uint16_t barHeight = map(fftResult[band], 0, 255, 0, rows); // do not subtract -1 from rows here + if (barHeight > previousBarHeight[x]) previousBarHeight[x] = barHeight; //drive the peak up + + uint32_t ledColor = BLACK; + for (int y=0; y < barHeight; y++) { + if (SEGMENT.check1) //color_vertical / color bars toggle + colorIndex = map(y, 0, rows-1, 0, 255); + + ledColor = SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0); + SEGMENT.setPixelColorXY(x, rows-1 - y, ledColor); + } + if (previousBarHeight[x] > 0) + SEGMENT.setPixelColorXY(x, rows - previousBarHeight[x], (SEGCOLOR(2) != BLACK) ? SEGCOLOR(2) : ledColor); + + if (rippleTime && previousBarHeight[x]>0) previousBarHeight[x]--; //delay/ripple effect + } + + return FRAMETIME; +} // mode_2DGEQ() +static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ@Fade speed,Ripple decay,# of bands,,,Color bars;!,,Peaks;!;2f;c1=255,c2=64,pal=11,si=0"; // Beatsin + + +///////////////////////// +// ** 2D Funky plank // +///////////////////////// +uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Will Tatam. + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + int NUMB_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); + int barWidth = (cols / NUMB_BANDS); + int bandInc = 1; + if (barWidth == 0) { + // Matrix narrower than fft bands + barWidth = 1; + bandInc = (NUMB_BANDS / cols); + } + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 64; + if (SEGENV.aux0 != secondHand) { // Triggered millis timing. + SEGENV.aux0 = secondHand; + + // display values of + int b = 0; + for (int band = 0; band < NUMB_BANDS; band += bandInc, b++) { + int hue = fftResult[band % 16]; + int v = map(fftResult[band % 16], 0, 255, 10, 255); + for (int w = 0; w < barWidth; w++) { + int xpos = (barWidth * b) + w; + SEGMENT.setPixelColorXY(xpos, 0, CHSV(hue, 255, v)); + } + } + + // Update the display: + for (int i = (rows - 1); i > 0; i--) { + for (int j = (cols - 1); j >= 0; j--) { + SEGMENT.setPixelColorXY(j, i, SEGMENT.getPixelColorXY(j, i-1)); + } + } + } + + return FRAMETIME; +} // mode_2DFunkyPlank +static const char _data_FX_MODE_2DFUNKYPLANK[] PROGMEM = "Funky Plank@Scroll speed,,# of bands;;;2f;si=0"; // Beatsin + + +///////////////////////// +// 2D Akemi // +///////////////////////// +static uint8_t akemi[] PROGMEM = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,2,2,3,3,3,3,3,3,2,2,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,2,3,3,0,0,0,0,0,0,3,3,2,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,2,3,0,0,0,6,5,5,4,0,0,0,3,2,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,2,3,0,0,6,6,5,5,5,5,4,4,0,0,3,2,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,2,3,0,6,5,5,5,5,5,5,5,5,4,0,3,2,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,2,3,0,6,5,5,5,5,5,5,5,5,5,5,4,0,3,2,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,3,2,0,6,5,5,5,5,5,5,5,5,5,5,4,0,2,3,0,0,0,0,0,0,0, + 0,0,0,0,0,0,3,2,3,6,5,5,7,7,5,5,5,5,7,7,5,5,4,3,2,3,0,0,0,0,0,0, + 0,0,0,0,0,2,3,1,3,6,5,1,7,7,7,5,5,1,7,7,7,5,4,3,1,3,2,0,0,0,0,0, + 0,0,0,0,0,8,3,1,3,6,5,1,7,7,7,5,5,1,7,7,7,5,4,3,1,3,8,0,0,0,0,0, + 0,0,0,0,0,8,3,1,3,6,5,5,1,1,5,5,5,5,1,1,5,5,4,3,1,3,8,0,0,0,0,0, + 0,0,0,0,0,2,3,1,3,6,5,5,5,5,5,5,5,5,5,5,5,5,4,3,1,3,2,0,0,0,0,0, + 0,0,0,0,0,0,3,2,3,6,5,5,5,5,5,5,5,5,5,5,5,5,4,3,2,3,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,6,5,5,5,5,5,7,7,5,5,5,5,5,4,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,0,0,0,0,0,0, + 1,0,0,0,0,0,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,0,0,0,0,0,2, + 0,2,2,2,0,0,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,0,0,2,2,2,0, + 0,0,0,3,2,0,0,0,6,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0,0,0,2,2,0,0,0, + 0,0,0,3,2,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,2,3,0,0,0, + 0,0,0,0,3,2,0,0,0,0,3,3,0,3,3,0,0,3,3,0,3,3,0,0,0,0,2,2,0,0,0,0, + 0,0,0,0,3,2,0,0,0,0,3,2,0,3,2,0,0,3,2,0,3,2,0,0,0,0,2,3,0,0,0,0, + 0,0,0,0,0,3,2,0,0,3,2,0,0,3,2,0,0,3,2,0,0,3,2,0,0,2,3,0,0,0,0,0, + 0,0,0,0,0,3,2,2,2,2,0,0,0,3,2,0,0,3,2,0,0,0,3,2,2,2,3,0,0,0,0,0, + 0,0,0,0,0,0,3,3,3,0,0,0,0,3,2,0,0,3,2,0,0,0,0,3,3,3,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +}; + +uint16_t mode_2DAkemi(void) { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; + counter = counter >> 8; + + const float lightFactor = 0.15f; + const float normalFactor = 0.4f; + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + float base = fftResult[0]/255.0f; + + //draw and color Akemi + for (int y=0; y < rows; y++) for (int x=0; x < cols; x++) { + CRGB color; + CRGB soundColor = ORANGE; + CRGB faceColor = SEGMENT.color_wheel(counter); + CRGB armsAndLegsColor = SEGCOLOR(1) > 0 ? SEGCOLOR(1) : 0xFFE0A0; //default warmish white 0xABA8FF; //0xFF52e5;// + uint8_t ak = pgm_read_byte_near(akemi + ((y * 32)/rows) * 32 + (x * 32)/cols); // akemi[(y * 32)/rows][(x * 32)/cols] + switch (ak) { + case 3: armsAndLegsColor.r *= lightFactor; armsAndLegsColor.g *= lightFactor; armsAndLegsColor.b *= lightFactor; color = armsAndLegsColor; break; //light arms and legs 0x9B9B9B + case 2: armsAndLegsColor.r *= normalFactor; armsAndLegsColor.g *= normalFactor; armsAndLegsColor.b *= normalFactor; color = armsAndLegsColor; break; //normal arms and legs 0x888888 + case 1: color = armsAndLegsColor; break; //dark arms and legs 0x686868 + case 6: faceColor.r *= lightFactor; faceColor.g *= lightFactor; faceColor.b *= lightFactor; color=faceColor; break; //light face 0x31AAFF + case 5: faceColor.r *= normalFactor; faceColor.g *= normalFactor; faceColor.b *= normalFactor; color=faceColor; break; //normal face 0x0094FF + case 4: color = faceColor; break; //dark face 0x007DC6 + case 7: color = SEGCOLOR(2) > 0 ? SEGCOLOR(2) : 0xFFFFFF; break; //eyes and mouth default white + case 8: if (base > 0.4) {soundColor.r *= base; soundColor.g *= base; soundColor.b *= base; color=soundColor;} else color = armsAndLegsColor; break; + default: color = BLACK; break; + } + + if (SEGMENT.intensity > 128 && fftResult && fftResult[0] > 128) { //dance if base is high + SEGMENT.setPixelColorXY(x, 0, BLACK); + SEGMENT.setPixelColorXY(x, y+1, color); + } else + SEGMENT.setPixelColorXY(x, y, color); + } + + //add geq left and right + if (um_data && fftResult) { + for (int x=0; x < cols/8; x++) { + uint16_t band = x * cols/8; + band = constrain(band, 0, 15); + uint16_t barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); + CRGB color = SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0); + + for (int y=0; y < barHeight; y++) { + SEGMENT.setPixelColorXY(x, rows/2-y, color); + SEGMENT.setPixelColorXY(cols-1-x, rows/2-y, color); + } + } + } + + return FRAMETIME; +} // mode_2DAkemi +static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Head palette,Arms & Legs,Eyes & Mouth;Face palette;2f;si=0"; //beatsin + + +// Distortion waves - ldirko +// https://editor.soulmatelights.com/gallery/1089-distorsion-waves +// adapted for WLED by @blazoncek +uint16_t mode_2Ddistortionwaves() { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + uint8_t speed = SEGMENT.speed/32; + uint8_t scale = SEGMENT.intensity/32; + + uint8_t w = 2; + + uint16_t a = millis()/32; + uint16_t a2 = a/2; + uint16_t a3 = a/3; + + uint16_t cx = beatsin8(10-speed,0,cols-1)*scale; + uint16_t cy = beatsin8(12-speed,0,rows-1)*scale; + uint16_t cx1 = beatsin8(13-speed,0,cols-1)*scale; + uint16_t cy1 = beatsin8(15-speed,0,rows-1)*scale; + uint16_t cx2 = beatsin8(17-speed,0,cols-1)*scale; + uint16_t cy2 = beatsin8(14-speed,0,rows-1)*scale; + + uint16_t xoffs = 0; + for (int x = 0; x < cols; x++) { + xoffs += scale; + uint16_t yoffs = 0; + + for (int y = 0; y < rows; y++) { + yoffs += scale; + + byte rdistort = cos8((cos8(((x<<3)+a )&255)+cos8(((y<<3)-a2)&255)+a3 )&255)>>1; + byte gdistort = cos8((cos8(((x<<3)-a2)&255)+cos8(((y<<3)+a3)&255)+a+32 )&255)>>1; + byte bdistort = cos8((cos8(((x<<3)+a3)&255)+cos8(((y<<3)-a) &255)+a2+64)&255)>>1; + + byte valueR = rdistort+ w* (a- ( ((xoffs - cx) * (xoffs - cx) + (yoffs - cy) * (yoffs - cy))>>7 )); + byte valueG = gdistort+ w* (a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 )); + byte valueB = bdistort+ w* (a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 )); + + valueR = gamma8(cos8(valueR)); + valueG = gamma8(cos8(valueG)); + valueB = gamma8(cos8(valueB)); + + SEGMENT.setPixelColorXY(x, y, RGBW32(valueR, valueG, valueB, 0)); + } + } + + return FRAMETIME; +} +static const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = "Distortion Waves@!,Scale;;;2"; + + +//Soap +//@Stepko +//Idea from https://www.youtube.com/watch?v=DiHBgITrZck&ab_channel=StefanPetrick +// adapted for WLED by @blazoncek +uint16_t mode_2Dsoap() { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + const size_t dataSize = SEGMENT.width() * SEGMENT.height() * sizeof(uint8_t); // prevent reallocation if mirrored or grouped + if (!SEGENV.allocateData(dataSize + sizeof(uint32_t)*3)) return mode_static(); //allocation failed + + uint8_t *noise3d = reinterpret_cast(SEGENV.data); + uint32_t *noise32_x = reinterpret_cast(SEGENV.data + dataSize); + uint32_t *noise32_y = reinterpret_cast(SEGENV.data + dataSize + sizeof(uint32_t)); + uint32_t *noise32_z = reinterpret_cast(SEGENV.data + dataSize + sizeof(uint32_t)*2); + const uint32_t scale32_x = 160000U/cols; + const uint32_t scale32_y = 160000U/rows; + const uint32_t mov = MIN(cols,rows)*(SEGMENT.speed+2)/2; + const uint8_t smoothness = MIN(250,SEGMENT.intensity); // limit as >250 produces very little changes + + // init + if (SEGENV.call == 0) { + *noise32_x = random16(); + *noise32_y = random16(); + *noise32_z = random16(); + } else { + *noise32_x += mov; + *noise32_y += mov; + *noise32_z += mov; + } + + for (int i = 0; i < cols; i++) { + int32_t ioffset = scale32_x * (i - cols / 2); + for (int j = 0; j < rows; j++) { + int32_t joffset = scale32_y * (j - rows / 2); + uint8_t data = inoise16(*noise32_x + ioffset, *noise32_y + joffset, *noise32_z) >> 8; + noise3d[XY(i,j)] = scale8(noise3d[XY(i,j)], smoothness) + scale8(data, 255 - smoothness); + } + } + // init also if dimensions changed + if (SEGENV.call == 0 || SEGMENT.aux0 != cols || SEGMENT.aux1 != rows) { + SEGMENT.aux0 = cols; + SEGMENT.aux1 = rows; + for (int i = 0; i < cols; i++) { + for (int j = 0; j < rows; j++) { + SEGMENT.setPixelColorXY(i, j, ColorFromPalette(SEGPALETTE,~noise3d[XY(i,j)]*3)); + } + } + } + + int zD; + int zF; + int amplitude; + int8_t shiftX = 0; //(SEGMENT.custom1 - 128) / 4; + int8_t shiftY = 0; //(SEGMENT.custom2 - 128) / 4; + CRGB ledsbuff[MAX(cols,rows)]; + + amplitude = (cols >= 16) ? (cols-8)/8 : 1; + for (int y = 0; y < rows; y++) { + int amount = ((int)noise3d[XY(0,y)] - 128) * 2 * amplitude + 256*shiftX; + int delta = abs(amount) >> 8; + int fraction = abs(amount) & 255; + for (int x = 0; x < cols; x++) { + if (amount < 0) { + zD = x - delta; + zF = zD - 1; + } else { + zD = x + delta; + zF = zD + 1; + } + CRGB PixelA = CRGB::Black; + if ((zD >= 0) && (zD < cols)) PixelA = SEGMENT.getPixelColorXY(zD, y); + else PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[XY(abs(zD),y)]*3); + CRGB PixelB = CRGB::Black; + if ((zF >= 0) && (zF < cols)) PixelB = SEGMENT.getPixelColorXY(zF, y); + else PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[XY(abs(zF),y)]*3); + ledsbuff[x] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); + } + for (int x = 0; x < cols; x++) SEGMENT.setPixelColorXY(x, y, ledsbuff[x]); + } + + amplitude = (rows >= 16) ? (rows-8)/8 : 1; + for (int x = 0; x < cols; x++) { + int amount = ((int)noise3d[XY(x,0)] - 128) * 2 * amplitude + 256*shiftY; + int delta = abs(amount) >> 8; + int fraction = abs(amount) & 255; + for (int y = 0; y < rows; y++) { + if (amount < 0) { + zD = y - delta; + zF = zD - 1; + } else { + zD = y + delta; + zF = zD + 1; + } + CRGB PixelA = CRGB::Black; + if ((zD >= 0) && (zD < rows)) PixelA = SEGMENT.getPixelColorXY(x, zD); + else PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[XY(x,abs(zD))]*3); + CRGB PixelB = CRGB::Black; + if ((zF >= 0) && (zF < rows)) PixelB = SEGMENT.getPixelColorXY(x, zF); + else PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[XY(x,abs(zF))]*3); + ledsbuff[y] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); + } + for (int y = 0; y < rows; y++) SEGMENT.setPixelColorXY(x, y, ledsbuff[y]); + } + + return FRAMETIME; +} +static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness;;!;2"; + + +//Idea from https://www.youtube.com/watch?v=HsA-6KIbgto&ab_channel=GreatScott%21 +//Octopus (https://editor.soulmatelights.com/gallery/671-octopus) +//Stepko and Sutaburosu +// adapted for WLED by @blazoncek +uint16_t mode_2Doctopus() { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + const uint8_t mapp = 180 / MAX(cols,rows); + + typedef struct { + uint8_t angle; + uint8_t radius; + } map_t; + + const size_t dataSize = SEGMENT.width() * SEGMENT.height() * sizeof(map_t); // prevent reallocation if mirrored or grouped + if (!SEGENV.allocateData(dataSize + 2)) return mode_static(); //allocation failed + + map_t *rMap = reinterpret_cast(SEGENV.data); + uint8_t *offsX = reinterpret_cast(SEGENV.data + dataSize); + uint8_t *offsY = reinterpret_cast(SEGENV.data + dataSize + 1); + + // re-init if SEGMENT dimensions or offset changed + if (SEGENV.call == 0 || SEGENV.aux0 != cols || SEGENV.aux1 != rows || SEGMENT.custom1 != *offsX || SEGMENT.custom2 != *offsY) { + SEGENV.step = 0; // t + SEGENV.aux0 = cols; + SEGENV.aux1 = rows; + *offsX = SEGMENT.custom1; + *offsY = SEGMENT.custom2; + const int C_X = (cols / 2) + ((SEGMENT.custom1 - 128)*cols)/255; + const int C_Y = (rows / 2) + ((SEGMENT.custom2 - 128)*rows)/255; + for (int x = 0; x < cols; x++) { + for (int y = 0; y < rows; y++) { + rMap[XY(x, y)].angle = 40.7436f * atan2f((y - C_Y), (x - C_X)); // avoid 128*atan2()/PI + rMap[XY(x, y)].radius = hypotf((x - C_X), (y - C_Y)) * mapp; //thanks Sutaburosu + } + } + } + + SEGENV.step += SEGMENT.speed / 32 + 1; // 1-4 range + for (int x = 0; x < cols; x++) { + for (int y = 0; y < rows; y++) { + byte angle = rMap[XY(x,y)].angle; + byte radius = rMap[XY(x,y)].radius; + //CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1))); + uint16_t intensity = sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); + intensity = map(intensity*intensity, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display + CRGB c = ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity); + SEGMENT.setPixelColorXY(x, y, c); + } + } + return FRAMETIME; +} +static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offset Y,Legs;;!;2;"; + + +//Waving Cell +//@Stepko (https://editor.soulmatelights.com/gallery/1704-wavingcells) +// adapted for WLED by @blazoncek +uint16_t mode_2Dwavingcell() { + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + uint32_t t = millis()/(257-SEGMENT.speed); + uint8_t aX = SEGMENT.custom1/16 + 9; + uint8_t aY = SEGMENT.custom2/16 + 1; + uint8_t aZ = SEGMENT.custom3 + 1; + for (int x = 0; x < cols; x++) for (int y = 0; y + #include "const.h" #define FASTLED_INTERNAL //remove annoying pragma messages @@ -38,6 +40,9 @@ #define DEFAULT_SPEED (uint8_t)128 #define DEFAULT_INTENSITY (uint8_t)128 #define DEFAULT_COLOR (uint32_t)0xFFAA00 +#define DEFAULT_C1 (uint8_t)128 +#define DEFAULT_C2 (uint8_t)128 +#define DEFAULT_C3 (uint8_t)16 #ifndef MIN #define MIN(a,b) ((a)<(b)?(a):(b)) @@ -46,41 +51,49 @@ #define MAX(a,b) ((a)>(b)?(a):(b)) #endif +//color mangling macros +#ifndef RGBW32 +#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) +#endif + /* Not used in all effects yet */ #define WLED_FPS 42 #define FRAMETIME_FIXED (1000/WLED_FPS) -#define FRAMETIME _frametime +//#define FRAMETIME _frametime +#define FRAMETIME strip.getFrameTime() /* each segment uses 52 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ #ifdef ESP8266 #define MAX_NUM_SEGMENTS 16 - /* How many color transitions can run at once */ - #define MAX_NUM_TRANSITIONS 8 /* How much data bytes all segments combined may allocate */ - #define MAX_SEGMENT_DATA 4096 + #define MAX_SEGMENT_DATA 5120 #else #ifndef MAX_NUM_SEGMENTS #define MAX_NUM_SEGMENTS 32 #endif - #define MAX_NUM_TRANSITIONS 24 - #define MAX_SEGMENT_DATA 20480 + #if defined(ARDUINO_ARCH_ESP32S2) + #define MAX_SEGMENT_DATA 24576 + #else + #define MAX_SEGMENT_DATA 32767 + #endif #endif /* How much data bytes each segment should max allocate to leave enough space for other segments, assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ -#define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / MAX_NUM_SEGMENTS) +#define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / strip.getMaxSegments()) -#define LED_SKIP_AMOUNT 1 #define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) #define NUM_COLORS 3 /* number of colors per segment */ -#define SEGMENT _segments[_segment_index] -#define SEGCOLOR(x) _colors_t[x] -#define SEGENV _segment_runtimes[_segment_index] -#define SEGLEN _virtualSegmentLength -#define SEGACT SEGMENT.stop -#define SPEED_FORMULA_L 5U + (50U*(255U - SEGMENT.speed))/SEGLEN +#define SEGMENT strip._segments[strip.getCurrSegmentId()] +#define SEGENV strip._segments[strip.getCurrSegmentId()] +//#define SEGCOLOR(x) strip._segments[strip.getCurrSegmentId()].currentColor(x, strip._segments[strip.getCurrSegmentId()].colors[x]) +//#define SEGLEN strip._segments[strip.getCurrSegmentId()].virtualLength() +#define SEGCOLOR(x) strip.segColor(x) /* saves us a few kbytes of code */ +#define SEGPALETTE Segment::getCurrentPalette() +#define SEGLEN strip._virtualSegmentLength /* saves us a few kbytes of code */ +#define SPEED_FORMULA_L (5U + (50U*(255U - SEGMENT.speed))/SEGLEN) // some common colors #define RED (uint32_t)0xFF0000 @@ -95,27 +108,20 @@ #define ORANGE (uint32_t)0xFF3000 #define PINK (uint32_t)0xFF1493 #define ULTRAWHITE (uint32_t)0xFFFFFFFF - -// options -// bit 7: segment is in transition mode -// bits 4-6: TBD -// bit 3: mirror effect within segment -// bit 2: segment is on -// bit 1: reverse segment -// bit 0: segment is selected -#define NO_OPTIONS (uint8_t)0x00 -#define TRANSITIONAL (uint8_t)0x80 -#define MIRROR (uint8_t)0x08 -#define SEGMENT_ON (uint8_t)0x04 -#define REVERSE (uint8_t)0x02 -#define SELECTED (uint8_t)0x01 -#define IS_TRANSITIONAL ((SEGMENT.options & TRANSITIONAL) == TRANSITIONAL) -#define IS_MIRROR ((SEGMENT.options & MIRROR ) == MIRROR ) -#define IS_SEGMENT_ON ((SEGMENT.options & SEGMENT_ON ) == SEGMENT_ON ) -#define IS_REVERSE ((SEGMENT.options & REVERSE ) == REVERSE ) -#define IS_SELECTED ((SEGMENT.options & SELECTED ) == SELECTED ) - -#define MODE_COUNT 118 +#define DARKSLATEGRAY (uint32_t)0x2F4F4F +#define DARKSLATEGREY DARKSLATEGRAY + +// segment options +#define NO_OPTIONS (uint16_t)0x0000 +#define TRANSPOSED (uint16_t)0x0100 // rotated 90deg & reversed +#define MIRROR_Y_2D (uint16_t)0x0080 +#define REVERSE_Y_2D (uint16_t)0x0040 +#define RESET_REQ (uint16_t)0x0020 +#define FROZEN (uint16_t)0x0010 +#define MIRROR (uint16_t)0x0008 +#define SEGMENT_ON (uint16_t)0x0004 +#define REVERSE (uint16_t)0x0002 +#define SELECTED (uint16_t)0x0001 #define FX_MODE_STATIC 0 #define FX_MODE_BLINK 1 @@ -136,7 +142,7 @@ #define FX_MODE_SAW 16 #define FX_MODE_TWINKLE 17 #define FX_MODE_DISSOLVE 18 -#define FX_MODE_DISSOLVE_RANDOM 19 +#define FX_MODE_DISSOLVE_RANDOM 19 // candidate for removal (use Dissolve with with check 3) #define FX_MODE_SPARKLE 20 #define FX_MODE_FLASH_SPARKLE 21 #define FX_MODE_HYPER_SPARKLE 22 @@ -165,12 +171,12 @@ #define FX_MODE_FIRE_FLICKER 45 #define FX_MODE_GRADIENT 46 #define FX_MODE_LOADING 47 -#define FX_MODE_POLICE 48 // candidate for removal (after below three) +#define FX_MODE_ROLLINGBALLS 48 //was Police before 0.14 #define FX_MODE_FAIRY 49 //was Police All prior to 0.13.0-b6 (use "Two Dots" with Red/Blue and full intensity) #define FX_MODE_TWO_DOTS 50 #define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity) #define FX_MODE_RUNNING_DUAL 52 -#define FX_MODE_HALLOWEEN 53 // candidate for removal +// #define FX_MODE_HALLOWEEN 53 // removed in 0.14! #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 #define FX_MODE_TRICOLOR_FADE 56 @@ -220,7 +226,7 @@ #define FX_MODE_HEARTBEAT 100 #define FX_MODE_PACIFICA 101 #define FX_MODE_CANDLE_MULTI 102 -#define FX_MODE_SOLID_GLITTER 103 +#define FX_MODE_SOLID_GLITTER 103 // candidate for removal (use glitter) #define FX_MODE_SUNRISE 104 #define FX_MODE_PHASED 105 #define FX_MODE_TWINKLEUP 106 @@ -231,720 +237,711 @@ #define FX_MODE_CHUNCHUN 111 #define FX_MODE_DANCING_SHADOWS 112 #define FX_MODE_WASHING_MACHINE 113 -#define FX_MODE_CANDY_CANE 114 // candidate for removal +// #define FX_MODE_CANDY_CANE 114 // removed in 0.14! #define FX_MODE_BLENDS 115 #define FX_MODE_TV_SIMULATOR 116 -#define FX_MODE_DYNAMIC_SMOOTH 117 +#define FX_MODE_DYNAMIC_SMOOTH 117 // candidate for removal (check3 in dynamic) + +// new 0.14 2D effects +#define FX_MODE_2DSPACESHIPS 118 //gap fill +#define FX_MODE_2DCRAZYBEES 119 //gap fill +#define FX_MODE_2DGHOSTRIDER 120 //gap fill +#define FX_MODE_2DBLOBS 121 //gap fill +#define FX_MODE_2DSCROLLTEXT 122 //gap fill +#define FX_MODE_2DDRIFTROSE 123 //gap fill +#define FX_MODE_2DDISTORTIONWAVES 124 //gap fill +#define FX_MODE_2DSOAP 125 //gap fill +#define FX_MODE_2DOCTOPUS 126 //gap fill +#define FX_MODE_2DWAVINGCELL 127 //gap fill + +// WLED-SR effects (SR compatible IDs !!!) +#define FX_MODE_PIXELS 128 +#define FX_MODE_PIXELWAVE 129 +#define FX_MODE_JUGGLES 130 +#define FX_MODE_MATRIPIX 131 +#define FX_MODE_GRAVIMETER 132 +#define FX_MODE_PLASMOID 133 +#define FX_MODE_PUDDLES 134 +#define FX_MODE_MIDNOISE 135 +#define FX_MODE_NOISEMETER 136 +#define FX_MODE_FREQWAVE 137 +#define FX_MODE_FREQMATRIX 138 +#define FX_MODE_2DGEQ 139 +#define FX_MODE_WATERFALL 140 +#define FX_MODE_FREQPIXELS 141 +#define FX_MODE_BINMAP 142 +#define FX_MODE_NOISEFIRE 143 +#define FX_MODE_PUDDLEPEAK 144 +#define FX_MODE_NOISEMOVE 145 +#define FX_MODE_2DNOISE 146 +#define FX_MODE_PERLINMOVE 147 +#define FX_MODE_RIPPLEPEAK 148 +#define FX_MODE_2DFIRENOISE 149 +#define FX_MODE_2DSQUAREDSWIRL 150 +#define FX_MODE_2DFIRE2012 151 +#define FX_MODE_2DDNA 152 +#define FX_MODE_2DMATRIX 153 +#define FX_MODE_2DMETABALLS 154 +#define FX_MODE_FREQMAP 155 +#define FX_MODE_GRAVCENTER 156 +#define FX_MODE_GRAVCENTRIC 157 +#define FX_MODE_GRAVFREQ 158 +#define FX_MODE_DJLIGHT 159 +#define FX_MODE_2DFUNKYPLANK 160 +#define FX_MODE_2DCENTERBARS 161 +#define FX_MODE_2DPULSER 162 +#define FX_MODE_BLURZ 163 +#define FX_MODE_2DDRIFT 164 +#define FX_MODE_2DWAVERLY 165 +#define FX_MODE_2DSUNRADIATION 166 +#define FX_MODE_2DCOLOREDBURSTS 167 +#define FX_MODE_2DJULIA 168 +// #define FX_MODE_2DPOOLNOISE 169 //have been removed in WLED SR in the past because of low mem but should be added back +// #define FX_MODE_2DTWISTER 170 //have been removed in WLED SR in the past because of low mem but should be added back +// #define FX_MODE_2DCAELEMENTATY 171 //have been removed in WLED SR in the past because of low mem but should be added back +#define FX_MODE_2DGAMEOFLIFE 172 +#define FX_MODE_2DTARTAN 173 +#define FX_MODE_2DPOLARLIGHTS 174 +#define FX_MODE_2DSWIRL 175 +#define FX_MODE_2DLISSAJOUS 176 +#define FX_MODE_2DFRIZZLES 177 +#define FX_MODE_2DPLASMABALL 178 +#define FX_MODE_FLOWSTRIPE 179 +#define FX_MODE_2DHIPHOTIC 180 +#define FX_MODE_2DSINDOTS 181 +#define FX_MODE_2DDNASPIRAL 182 +#define FX_MODE_2DBLACKHOLE 183 +#define FX_MODE_WAVESINS 184 +#define FX_MODE_ROCKTAVES 185 +#define FX_MODE_2DAKEMI 186 + +#define MODE_COUNT 187 + +typedef enum mapping1D2D { + M12_Pixels = 0, + M12_pBar = 1, + M12_pArc = 2, + M12_pCorner = 3 +} mapping1D2D_t; + +// segment, 80 bytes +typedef struct Segment { + public: + uint16_t start; // start index / start X coordinate 2D (left) + uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0 + uint16_t offset; + uint8_t speed; + uint8_t intensity; + uint8_t palette; + uint8_t mode; + union { + uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected + struct { + bool selected : 1; // 0 : selected + bool reverse : 1; // 1 : reversed + bool on : 1; // 2 : is On + bool mirror : 1; // 3 : mirrored + bool freeze : 1; // 4 : paused/frozen + bool reset : 1; // 5 : indicates that Segment runtime requires reset + bool reverse_y : 1; // 6 : reversed Y (2D) + bool mirror_y : 1; // 7 : mirrored Y (2D) + bool transpose : 1; // 8 : transposed (2D, swapped X & Y) + uint8_t map1D2D : 3; // 9-11 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) + uint8_t soundSim : 2; // 12-13 : 0-3 sound simulation types ("soft" & "hard" or "on"/"off") + uint8_t set : 2; // 14-15 : 0-3 UI segment sets/groups + }; + }; + uint8_t grouping, spacing; + uint8_t opacity; + uint32_t colors[NUM_COLORS]; + uint8_t cct; //0==1900K, 255==10091K + uint8_t custom1, custom2; // custom FX parameters/sliders + struct { + uint8_t custom3 : 5; // reduced range slider (0-31) + bool check1 : 1; // checkmark 1 + bool check2 : 1; // checkmark 2 + bool check3 : 1; // checkmark 3 + }; + uint8_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows + uint8_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows + char *name; + + // runtime data + unsigned long next_time; // millis() of next update + uint32_t step; // custom "step" var + uint32_t call; // call counter + uint16_t aux0; // custom var + uint16_t aux1; // custom var + byte *data; // effect data pointer + static uint16_t maxWidth, maxHeight; // these define matrix width & height (max. segment dimensions) + + typedef struct TemporarySegmentData { + uint16_t _optionsT; + uint32_t _colorT[NUM_COLORS]; + uint8_t _speedT; + uint8_t _intensityT; + uint8_t _custom1T, _custom2T; // custom FX parameters/sliders + struct { + uint8_t _custom3T : 5; // reduced range slider (0-31) + bool _check1T : 1; // checkmark 1 + bool _check2T : 1; // checkmark 2 + bool _check3T : 1; // checkmark 3 + }; + uint16_t _aux0T; + uint16_t _aux1T; + uint32_t _stepT; + uint32_t _callT; + uint8_t *_dataT; + uint16_t _dataLenT; + TemporarySegmentData() + : _dataT(nullptr) // just in case... + , _dataLenT(0) + {} + } tmpsegd_t; + private: + union { + uint8_t _capabilities; + struct { + bool _isRGB : 1; + bool _hasW : 1; + bool _isCCT : 1; + bool _manualW : 1; + uint8_t _reserved : 4; + }; + }; + uint16_t _dataLen; + static uint16_t _usedSegmentData; + + // perhaps this should be per segment, not static + static CRGBPalette16 _currentPalette; // palette used for current effect (includes transition, used in color_from_palette()) + static CRGBPalette16 _randomPalette; // actual random palette + static CRGBPalette16 _newRandomPalette; // target random palette + static unsigned long _lastPaletteChange; // last random palette change time in millis() + #ifndef WLED_DISABLE_MODE_BLEND + static bool _modeBlend; // mode/effect blending semaphore + #endif + + // transition data, valid only if transitional==true, holds values during transition (72 bytes) + struct Transition { + #ifndef WLED_DISABLE_MODE_BLEND + tmpsegd_t _segT; // previous segment environment + uint8_t _modeT; // previous mode/effect + #else + uint32_t _colorT[NUM_COLORS]; + #endif + uint8_t _briT; // temporary brightness + uint8_t _cctT; // temporary CCT + CRGBPalette16 _palT; // temporary palette + uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) + unsigned long _start; // must accommodate millis() + uint16_t _dur; + Transition(uint16_t dur=750) + : _palT(CRGBPalette16(CRGB::Black)) + , _prevPaletteBlends(0) + , _start(millis()) + , _dur(dur) + {} + } *_t; + + public: -class WS2812FX { - typedef uint16_t (WS2812FX::*mode_ptr)(void); + Segment(uint16_t sStart=0, uint16_t sStop=30) : + start(sStart), + stop(sStop), + offset(0), + speed(DEFAULT_SPEED), + intensity(DEFAULT_INTENSITY), + palette(0), + mode(DEFAULT_MODE), + options(SELECTED | SEGMENT_ON), + grouping(1), + spacing(0), + opacity(255), + colors{DEFAULT_COLOR,BLACK,BLACK}, + cct(127), + custom1(DEFAULT_C1), + custom2(DEFAULT_C2), + custom3(DEFAULT_C3), + check1(false), + check2(false), + check3(false), + startY(0), + stopY(1), + name(nullptr), + next_time(0), + step(0), + call(0), + aux0(0), + aux1(0), + data(nullptr), + _capabilities(0), + _dataLen(0), + _t(nullptr) + { + #ifdef WLED_DEBUG + //Serial.printf("-- Creating segment: %p\n", this); + #endif + } + + Segment(uint16_t sStartX, uint16_t sStopX, uint16_t sStartY, uint16_t sStopY) : Segment(sStartX, sStopX) { + startY = sStartY; + stopY = sStopY; + } - // pre show callback - typedef void (*show_callback) (void); + Segment(const Segment &orig); // copy constructor + Segment(Segment &&orig) noexcept; // move constructor + + ~Segment() { + #ifdef WLED_DEBUG + //Serial.printf("-- Destroying segment: %p\n", this); + //if (name) Serial.printf(" %s (%p)", name, name); + //if (data) Serial.printf(" %d (%p)", (int)_dataLen, data); + //Serial.println(); + #endif + if (name) { delete[] name; name = nullptr; } + stopTransition(); + deallocateData(); + } + + Segment& operator= (const Segment &orig); // copy assignment + Segment& operator= (Segment &&orig) noexcept; // move assignment + +#ifdef WLED_DEBUG + size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0); } +#endif + + inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } + inline bool isSelected(void) const { return selected; } + inline bool isInTransition(void) const { return _t != nullptr; } + inline bool isActive(void) const { return stop > start; } + inline bool is2D(void) const { return (width()>1 && height()>1); } + inline bool hasRGB(void) const { return _isRGB; } + inline bool hasWhite(void) const { return _hasW; } + inline bool isCCT(void) const { return _isCCT; } + inline uint16_t width(void) const { return isActive() ? (stop - start) : 0; } // segment width in physical pixels (length if 1D) + inline uint16_t height(void) const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) + inline uint16_t length(void) const { return width() * height(); } // segment length (count) in physical pixels + inline uint16_t groupLength(void) const { return grouping + spacing; } + inline uint8_t getLightCapabilities(void) const { return _capabilities; } + + static uint16_t getUsedSegmentData(void) { return _usedSegmentData; } + static void addUsedSegmentData(int len) { _usedSegmentData += len; } + #ifndef WLED_DISABLE_MODE_BLEND + static void modeBlend(bool blend) { _modeBlend = blend; } + #endif + static void handleRandomPalette(); + inline static const CRGBPalette16 &getCurrentPalette(void) { return Segment::_currentPalette; } + + void setUp(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1, uint8_t segId = 255); + bool setColor(uint8_t slot, uint32_t c); //returns true if changed + void setCCT(uint16_t k); + void setOpacity(uint8_t o); + void setOption(uint8_t n, bool val); + void setMode(uint8_t fx, bool loadDefaults = false); + void setPalette(uint8_t pal); + uint8_t differs(Segment& b) const; + void refreshLightCapabilities(void); + + // runtime data functions + inline uint16_t dataSize(void) const { return _dataLen; } + bool allocateData(size_t len); + void deallocateData(void); + void resetIfRequired(void); + /** + * Flags that before the next effect is calculated, + * the internal segment state should be reset. + * Call resetIfRequired before calling the next effect function. + * Safe to call from interrupts and network requests. + */ + inline void markForReset(void) { reset = true; } // setOption(SEG_OPTION_RESET, true) + + // transition functions + void startTransition(uint16_t dur); // transition has to start before actual segment values change + void stopTransition(void); + void handleTransition(void); + #ifndef WLED_DISABLE_MODE_BLEND + void swapSegenv(tmpsegd_t &tmpSegD); + void restoreSegenv(tmpsegd_t &tmpSegD); + #endif + uint16_t progress(void); //transition progression between 0-65535 + uint8_t currentBri(bool useCct = false); + uint8_t currentMode(void); + uint32_t currentColor(uint8_t slot); + CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); + void setCurrentPalette(void); + + // 1D strip + uint16_t virtualLength(void) const; + void setPixelColor(int n, uint32_t c); // set relative pixel within segment with color + void setPixelColor(unsigned n, uint32_t c) { setPixelColor(int(n), c); } + void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } // automatically inline + void setPixelColor(int n, CRGB c) { setPixelColor(n, RGBW32(c.r,c.g,c.b,0)); } // automatically inline + void setPixelColor(float i, uint32_t c, bool aa = true); + void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) { setPixelColor(i, RGBW32(r,g,b,w), aa); } + void setPixelColor(float i, CRGB c, bool aa = true) { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } + uint32_t getPixelColor(int i); + // 1D support functions (some implement 2D as well) + void blur(uint8_t); + void fill(uint32_t c); + void fade_out(uint8_t r); + void fadeToBlackBy(uint8_t fadeBy); + void blendPixelColor(int n, uint32_t color, uint8_t blend); + void blendPixelColor(int n, CRGB c, uint8_t blend) { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } + void addPixelColor(int n, uint32_t color, bool fast = false); + void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(n, RGBW32(r,g,b,w), fast); } // automatically inline + void addPixelColor(int n, CRGB c, bool fast = false) { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), fast); } // automatically inline + void fadePixelColor(uint16_t n, uint8_t fade); + uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255); + uint32_t color_wheel(uint8_t pos); + + // 2D matrix + uint16_t virtualWidth(void) const; + uint16_t virtualHeight(void) const; + uint16_t nrOfVStrips(void) const; + #ifndef WLED_DISABLE_2D + uint16_t XY(uint16_t x, uint16_t y); // support function to get relative index within segment + void setPixelColorXY(int x, int y, uint32_t c); // set relative pixel within segment with color + void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColorXY(int(x), int(y), c); } + void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } // automatically inline + void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } // automatically inline + void setPixelColorXY(float x, float y, uint32_t c, bool aa = true); + void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } + void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } + uint32_t getPixelColorXY(uint16_t x, uint16_t y); + // 2D support functions + void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend); + void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } + void addPixelColorXY(int x, int y, uint32_t color, bool fast = false); + void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColorXY(x, y, RGBW32(r,g,b,w), fast); } // automatically inline + void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } + void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade); + void box_blur(uint16_t i, bool vertical, fract8 blur_amount); // 1D box blur (with weight) + void blurRow(uint16_t row, fract8 blur_amount); + void blurCol(uint16_t col, fract8 blur_amount); + void moveX(int8_t delta, bool wrap = false); + void moveY(int8_t delta, bool wrap = false); + void move(uint8_t dir, uint8_t delta, bool wrap = false); + void draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c); + void fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c); + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c); + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0)); } // automatic inline + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0); + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0)); } // automatic inline + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline + void wu_pixel(uint32_t x, uint32_t y, CRGB c); + void blur1d(fract8 blur_amount); // blur all rows in 1 dimension + void blur2d(fract8 blur_amount) { blur(blur_amount); } + void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } + void nscale8(uint8_t scale); + #else + uint16_t XY(uint16_t x, uint16_t y) { return x; } + void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } + void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } + void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } + void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } + void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } + void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } + uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(x); } + void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } + void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } + void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { addPixelColor(x, color, fast); } + void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(x, RGBW32(r,g,b,w), fast); } + void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), fast); } + void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); } + void box_blur(uint16_t i, bool vertical, fract8 blur_amount) {} + void blurRow(uint16_t row, fract8 blur_amount) {} + void blurCol(uint16_t col, fract8 blur_amount) {} + void moveX(int8_t delta, bool wrap = false) {} + void moveY(int8_t delta, bool wrap = false) {} + void move(uint8_t dir, uint8_t delta, bool wrap = false) {} + void fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c) {} + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c) {} + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c) {} + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {} + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB color) {} + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {} + void wu_pixel(uint32_t x, uint32_t y, CRGB c) {} + #endif +} segment; +//static int segSize = sizeof(Segment); + +// main "strip" class +class WS2812FX { // 96 bytes + typedef uint16_t (*mode_ptr)(void); // pointer to mode function + typedef void (*show_callback)(void); // pre show callback + typedef struct ModeData { + uint8_t _id; // mode (effect) id + mode_ptr _fcn; // mode (effect) function + const char *_data; // mode (effect) name and its UI control data + ModeData(uint8_t id, uint16_t (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {} + } mode_data_t; static WS2812FX* instance; - - // segment parameters + public: - typedef struct Segment { // 31 (32 in memory) bytes - uint16_t start; - uint16_t stop; //segment invalid if stop == 0 - uint16_t offset; - uint8_t speed; - uint8_t intensity; - uint8_t palette; - uint8_t mode; - uint8_t options; //bit pattern: msb first: transitional needspixelstate tbd tbd (paused) on reverse selected - uint8_t grouping, spacing; - uint8_t opacity; - uint32_t colors[NUM_COLORS]; - uint8_t cct; //0==1900K, 255==10091K - uint8_t _capabilities; - char *name; - bool setColor(uint8_t slot, uint32_t c, uint8_t segn) { //returns true if changed - if (slot >= NUM_COLORS || segn >= MAX_NUM_SEGMENTS) return false; - if (c == colors[slot]) return false; - uint8_t b = (slot == 1) ? cct : opacity; - ColorTransition::startTransition(b, colors[slot], instance->_transitionDur, segn, slot); - colors[slot] = c; return true; - } - void setCCT(uint16_t k, uint8_t segn) { - if (segn >= MAX_NUM_SEGMENTS) return; - if (k > 255) { //kelvin value, convert to 0-255 - if (k < 1900) k = 1900; - if (k > 10091) k = 10091; - k = (k - 1900) >> 5; - } - if (cct == k) return; - ColorTransition::startTransition(cct, colors[1], instance->_transitionDur, segn, 1); - cct = k; - } - void setOpacity(uint8_t o, uint8_t segn) { - if (segn >= MAX_NUM_SEGMENTS) return; - if (opacity == o) return; - ColorTransition::startTransition(opacity, colors[0], instance->_transitionDur, segn, 0); - opacity = o; - } - void setOption(uint8_t n, bool val, uint8_t segn = 255) - { - bool prevOn = false; - if (n == SEG_OPTION_ON) { - prevOn = getOption(SEG_OPTION_ON); - if (!val && prevOn) { //fade off - ColorTransition::startTransition(opacity, colors[0], instance->_transitionDur, segn, 0); - } - } - - if (val) { - options |= 0x01 << n; - } else - { - options &= ~(0x01 << n); - } - - if (n == SEG_OPTION_ON && val && !prevOn) { //fade on - ColorTransition::startTransition(0, colors[0], instance->_transitionDur, segn, 0); - } - } - bool getOption(uint8_t n) - { - return ((options >> n) & 0x01); - } - inline bool isSelected() - { - return getOption(0); - } - inline bool isActive() - { - return stop > start; - } - inline uint16_t length() - { - return stop - start; - } - inline uint16_t groupLength() - { - return grouping + spacing; - } - uint16_t virtualLength() - { - uint16_t groupLen = groupLength(); - uint16_t vLength = (length() + groupLen - 1) / groupLen; - if (options & MIRROR) - vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED - return vLength; - } - uint8_t differs(Segment& b); - inline uint8_t getLightCapabilities() {return _capabilities;} - void refreshLightCapabilities(); - } segment; - - // segment runtime parameters - typedef struct Segment_runtime { // 28 bytes - unsigned long next_time; // millis() of next update - uint32_t step; // custom "step" var - uint32_t call; // call counter - uint16_t aux0; // custom var - uint16_t aux1; // custom var - byte* data = nullptr; - bool allocateData(uint16_t len){ - if (data && _dataLen == len) return true; //already allocated - deallocateData(); - if (WS2812FX::instance->_usedSegmentData + len > MAX_SEGMENT_DATA) return false; //not enough memory - // if possible use SPI RAM on ESP32 - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_PSRAM) - if (psramFound()) - data = (byte*) ps_malloc(len); - else - #endif - data = (byte*) malloc(len); - if (!data) return false; //allocation failed - WS2812FX::instance->_usedSegmentData += len; - _dataLen = len; - memset(data, 0, len); - return true; - } - void deallocateData(){ - free(data); - data = nullptr; - WS2812FX::instance->_usedSegmentData -= _dataLen; - _dataLen = 0; - } - - /** - * If reset of this segment was request, clears runtime - * settings of this segment. - * Must not be called while an effect mode function is running - * because it could access the data buffer and this method - * may free that data buffer. - */ - void resetIfRequired() { - if (_requiresReset) { - next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; - deallocateData(); - _requiresReset = false; - } - } - - /** - * Flags that before the next effect is calculated, - * the internal segment state should be reset. - * Call resetIfRequired before calling the next effect function. - * Safe to call from interrupts and network requests. - */ - inline void markForReset() { _requiresReset = true; } - private: - uint16_t _dataLen = 0; - bool _requiresReset = false; - } segment_runtime; - - typedef struct ColorTransition { // 12 bytes - uint32_t colorOld = 0; - uint32_t transitionStart; - uint16_t transitionDur; - uint8_t segment = 0xFF; //lower 6 bits: the segment this transition is for (255 indicates transition not in use/available) upper 2 bits: color channel - uint8_t briOld = 0; - static void startTransition(uint8_t oldBri, uint32_t oldCol, uint16_t dur, uint8_t segn, uint8_t slot) { - if (segn >= MAX_NUM_SEGMENTS || slot >= NUM_COLORS || dur == 0) return; - if (instance->_brightness == 0) return; //do not need transitions if master bri is off - if (!instance->_segments[segn].getOption(SEG_OPTION_ON)) return; //not if segment is off either - uint8_t tIndex = 0xFF; //none found - uint16_t tProgression = 0; - uint8_t s = segn + (slot << 6); //merge slot and segment into one byte - - for (uint8_t i = 0; i < MAX_NUM_TRANSITIONS; i++) { - uint8_t tSeg = instance->transitions[i].segment; - //see if this segment + color already has a running transition - if (tSeg == s) { - tIndex = i; break; - } - if (tSeg == 0xFF) { //free transition - tIndex = i; tProgression = 0xFFFF; - } - } - - if (tIndex == 0xFF) { //no slot found yet - for (uint8_t i = 0; i < MAX_NUM_TRANSITIONS; i++) { - //find most progressed transition to overwrite - uint16_t prog = instance->transitions[i].progress(); - if (prog > tProgression) { - tIndex = i; tProgression = prog; - } - } - } - - ColorTransition& t = instance->transitions[tIndex]; - if (t.segment == s) //this is an active transition on the same segment+color - { - bool wasTurningOff = (oldBri == 0); - t.briOld = t.currentBri(wasTurningOff, slot); - t.colorOld = t.currentColor(oldCol); - } else { - t.briOld = oldBri; - t.colorOld = oldCol; - uint8_t prevSeg = t.segment & 0x3F; - if (prevSeg < MAX_NUM_SEGMENTS) instance->_segments[prevSeg].setOption(SEG_OPTION_TRANSITIONAL, false); - } - t.transitionDur = dur; - t.transitionStart = millis(); - t.segment = s; - instance->_segments[segn].setOption(SEG_OPTION_TRANSITIONAL, true); - //refresh immediately, required for Solid mode - if (instance->_segment_runtimes[segn].next_time > t.transitionStart + 22) instance->_segment_runtimes[segn].next_time = t.transitionStart; - } - uint16_t progress(bool allowEnd = false) { //transition progression between 0-65535 - uint32_t timeNow = millis(); - if (timeNow - transitionStart > transitionDur) { - if (allowEnd) { - uint8_t segn = segment & 0x3F; - if (segn < MAX_NUM_SEGMENTS) instance->_segments[segn].setOption(SEG_OPTION_TRANSITIONAL, false); - segment = 0xFF; - } - return 0xFFFF; - } - uint32_t elapsed = timeNow - transitionStart; - uint32_t prog = elapsed * 0xFFFF / transitionDur; - return (prog > 0xFFFF) ? 0xFFFF : prog; - } - uint32_t currentColor(uint32_t colorNew) { - return instance->color_blend(colorOld, colorNew, progress(true), true); - } - uint8_t currentBri(bool turningOff = false, uint8_t slot = 0) { - uint8_t segn = segment & 0x3F; - if (segn >= MAX_NUM_SEGMENTS) return 0; - uint8_t briNew = instance->_segments[segn].opacity; - if (slot == 0) { - if (!instance->_segments[segn].getOption(SEG_OPTION_ON) || turningOff) briNew = 0; - } else { //transition slot 1 brightness for CCT transition - briNew = instance->_segments[segn].cct; - } - uint32_t prog = progress() + 1; - return ((briNew * prog) + (briOld * (0x10000 - prog))) >> 16; - } - } color_transition; - - WS2812FX() { + + WS2812FX() : + paletteFade(0), + paletteBlend(0), + milliampsPerLed(55), + cctBlending(0), + ablMilliampsMax(ABL_MILLIAMPS_DEFAULT), + currentMilliamps(0), + now(millis()), + timebase(0), + isMatrix(false), +#ifndef WLED_DISABLE_2D + panels(1), +#endif + // semi-private (just obscured) used in effect functions through macros + _colors_t{0,0,0}, + _virtualSegmentLength(0), + // true private variables + _length(DEFAULT_LED_COUNT), + _brightness(DEFAULT_BRIGHTNESS), + _transitionDur(750), + _targetFps(WLED_FPS), + _frametime(FRAMETIME_FIXED), + _cumulativeFps(2), + _isServicing(false), + _isOffRefreshRequired(false), + _hasWhiteChannel(false), + _triggered(false), + _modeCount(MODE_COUNT), + _callback(nullptr), + customMappingTable(nullptr), + customMappingSize(0), + _lastShow(0), + _segment_index(0), + _mainSegment(0), + _queuedChangesSegId(255), + _qStart(0), + _qStop(0), + _qStartY(0), + _qStopY(0), + _qGrouping(0), + _qSpacing(0), + _qOffset(0) + { WS2812FX::instance = this; - //assign each member of the _mode[] array to its respective function reference - _mode[FX_MODE_STATIC] = &WS2812FX::mode_static; - _mode[FX_MODE_BLINK] = &WS2812FX::mode_blink; - _mode[FX_MODE_COLOR_WIPE] = &WS2812FX::mode_color_wipe; - _mode[FX_MODE_COLOR_WIPE_RANDOM] = &WS2812FX::mode_color_wipe_random; - _mode[FX_MODE_RANDOM_COLOR] = &WS2812FX::mode_random_color; - _mode[FX_MODE_COLOR_SWEEP] = &WS2812FX::mode_color_sweep; - _mode[FX_MODE_DYNAMIC] = &WS2812FX::mode_dynamic; - _mode[FX_MODE_RAINBOW] = &WS2812FX::mode_rainbow; - _mode[FX_MODE_RAINBOW_CYCLE] = &WS2812FX::mode_rainbow_cycle; - _mode[FX_MODE_SCAN] = &WS2812FX::mode_scan; - _mode[FX_MODE_DUAL_SCAN] = &WS2812FX::mode_dual_scan; - _mode[FX_MODE_FADE] = &WS2812FX::mode_fade; - _mode[FX_MODE_THEATER_CHASE] = &WS2812FX::mode_theater_chase; - _mode[FX_MODE_THEATER_CHASE_RAINBOW] = &WS2812FX::mode_theater_chase_rainbow; - _mode[FX_MODE_SAW] = &WS2812FX::mode_saw; - _mode[FX_MODE_TWINKLE] = &WS2812FX::mode_twinkle; - _mode[FX_MODE_DISSOLVE] = &WS2812FX::mode_dissolve; - _mode[FX_MODE_DISSOLVE_RANDOM] = &WS2812FX::mode_dissolve_random; - _mode[FX_MODE_SPARKLE] = &WS2812FX::mode_sparkle; - _mode[FX_MODE_FLASH_SPARKLE] = &WS2812FX::mode_flash_sparkle; - _mode[FX_MODE_HYPER_SPARKLE] = &WS2812FX::mode_hyper_sparkle; - _mode[FX_MODE_STROBE] = &WS2812FX::mode_strobe; - _mode[FX_MODE_STROBE_RAINBOW] = &WS2812FX::mode_strobe_rainbow; - _mode[FX_MODE_MULTI_STROBE] = &WS2812FX::mode_multi_strobe; - _mode[FX_MODE_BLINK_RAINBOW] = &WS2812FX::mode_blink_rainbow; - _mode[FX_MODE_ANDROID] = &WS2812FX::mode_android; - _mode[FX_MODE_CHASE_COLOR] = &WS2812FX::mode_chase_color; - _mode[FX_MODE_CHASE_RANDOM] = &WS2812FX::mode_chase_random; - _mode[FX_MODE_CHASE_RAINBOW] = &WS2812FX::mode_chase_rainbow; - _mode[FX_MODE_CHASE_FLASH] = &WS2812FX::mode_chase_flash; - _mode[FX_MODE_CHASE_FLASH_RANDOM] = &WS2812FX::mode_chase_flash_random; - _mode[FX_MODE_CHASE_RAINBOW_WHITE] = &WS2812FX::mode_chase_rainbow_white; - _mode[FX_MODE_COLORFUL] = &WS2812FX::mode_colorful; - _mode[FX_MODE_TRAFFIC_LIGHT] = &WS2812FX::mode_traffic_light; - _mode[FX_MODE_COLOR_SWEEP_RANDOM] = &WS2812FX::mode_color_sweep_random; - _mode[FX_MODE_RUNNING_COLOR] = &WS2812FX::mode_running_color; - _mode[FX_MODE_AURORA] = &WS2812FX::mode_aurora; - _mode[FX_MODE_RUNNING_RANDOM] = &WS2812FX::mode_running_random; - _mode[FX_MODE_LARSON_SCANNER] = &WS2812FX::mode_larson_scanner; - _mode[FX_MODE_COMET] = &WS2812FX::mode_comet; - _mode[FX_MODE_FIREWORKS] = &WS2812FX::mode_fireworks; - _mode[FX_MODE_RAIN] = &WS2812FX::mode_rain; - _mode[FX_MODE_TETRIX] = &WS2812FX::mode_tetrix; - _mode[FX_MODE_FIRE_FLICKER] = &WS2812FX::mode_fire_flicker; - _mode[FX_MODE_GRADIENT] = &WS2812FX::mode_gradient; - _mode[FX_MODE_LOADING] = &WS2812FX::mode_loading; - _mode[FX_MODE_POLICE] = &WS2812FX::mode_police; - _mode[FX_MODE_FAIRY] = &WS2812FX::mode_fairy; - _mode[FX_MODE_TWO_DOTS] = &WS2812FX::mode_two_dots; - _mode[FX_MODE_FAIRYTWINKLE] = &WS2812FX::mode_fairytwinkle; - _mode[FX_MODE_RUNNING_DUAL] = &WS2812FX::mode_running_dual; - _mode[FX_MODE_HALLOWEEN] = &WS2812FX::mode_halloween; - _mode[FX_MODE_TRICOLOR_CHASE] = &WS2812FX::mode_tricolor_chase; - _mode[FX_MODE_TRICOLOR_WIPE] = &WS2812FX::mode_tricolor_wipe; - _mode[FX_MODE_TRICOLOR_FADE] = &WS2812FX::mode_tricolor_fade; - _mode[FX_MODE_BREATH] = &WS2812FX::mode_breath; - _mode[FX_MODE_RUNNING_LIGHTS] = &WS2812FX::mode_running_lights; - _mode[FX_MODE_LIGHTNING] = &WS2812FX::mode_lightning; - _mode[FX_MODE_ICU] = &WS2812FX::mode_icu; - _mode[FX_MODE_MULTI_COMET] = &WS2812FX::mode_multi_comet; - _mode[FX_MODE_DUAL_LARSON_SCANNER] = &WS2812FX::mode_dual_larson_scanner; - _mode[FX_MODE_RANDOM_CHASE] = &WS2812FX::mode_random_chase; - _mode[FX_MODE_OSCILLATE] = &WS2812FX::mode_oscillate; - _mode[FX_MODE_FIRE_2012] = &WS2812FX::mode_fire_2012; - _mode[FX_MODE_PRIDE_2015] = &WS2812FX::mode_pride_2015; - _mode[FX_MODE_BPM] = &WS2812FX::mode_bpm; - _mode[FX_MODE_JUGGLE] = &WS2812FX::mode_juggle; - _mode[FX_MODE_PALETTE] = &WS2812FX::mode_palette; - _mode[FX_MODE_COLORWAVES] = &WS2812FX::mode_colorwaves; - _mode[FX_MODE_FILLNOISE8] = &WS2812FX::mode_fillnoise8; - _mode[FX_MODE_NOISE16_1] = &WS2812FX::mode_noise16_1; - _mode[FX_MODE_NOISE16_2] = &WS2812FX::mode_noise16_2; - _mode[FX_MODE_NOISE16_3] = &WS2812FX::mode_noise16_3; - _mode[FX_MODE_NOISE16_4] = &WS2812FX::mode_noise16_4; - _mode[FX_MODE_COLORTWINKLE] = &WS2812FX::mode_colortwinkle; - _mode[FX_MODE_LAKE] = &WS2812FX::mode_lake; - _mode[FX_MODE_METEOR] = &WS2812FX::mode_meteor; - _mode[FX_MODE_METEOR_SMOOTH] = &WS2812FX::mode_meteor_smooth; - _mode[FX_MODE_RAILWAY] = &WS2812FX::mode_railway; - _mode[FX_MODE_RIPPLE] = &WS2812FX::mode_ripple; - _mode[FX_MODE_TWINKLEFOX] = &WS2812FX::mode_twinklefox; - _mode[FX_MODE_TWINKLECAT] = &WS2812FX::mode_twinklecat; - _mode[FX_MODE_HALLOWEEN_EYES] = &WS2812FX::mode_halloween_eyes; - _mode[FX_MODE_STATIC_PATTERN] = &WS2812FX::mode_static_pattern; - _mode[FX_MODE_TRI_STATIC_PATTERN] = &WS2812FX::mode_tri_static_pattern; - _mode[FX_MODE_SPOTS] = &WS2812FX::mode_spots; - _mode[FX_MODE_SPOTS_FADE] = &WS2812FX::mode_spots_fade; - _mode[FX_MODE_GLITTER] = &WS2812FX::mode_glitter; - _mode[FX_MODE_CANDLE] = &WS2812FX::mode_candle; - _mode[FX_MODE_STARBURST] = &WS2812FX::mode_starburst; - _mode[FX_MODE_EXPLODING_FIREWORKS] = &WS2812FX::mode_exploding_fireworks; - _mode[FX_MODE_BOUNCINGBALLS] = &WS2812FX::mode_bouncing_balls; - _mode[FX_MODE_SINELON] = &WS2812FX::mode_sinelon; - _mode[FX_MODE_SINELON_DUAL] = &WS2812FX::mode_sinelon_dual; - _mode[FX_MODE_SINELON_RAINBOW] = &WS2812FX::mode_sinelon_rainbow; - _mode[FX_MODE_POPCORN] = &WS2812FX::mode_popcorn; - _mode[FX_MODE_DRIP] = &WS2812FX::mode_drip; - _mode[FX_MODE_PLASMA] = &WS2812FX::mode_plasma; - _mode[FX_MODE_PERCENT] = &WS2812FX::mode_percent; - _mode[FX_MODE_RIPPLE_RAINBOW] = &WS2812FX::mode_ripple_rainbow; - _mode[FX_MODE_HEARTBEAT] = &WS2812FX::mode_heartbeat; - _mode[FX_MODE_PACIFICA] = &WS2812FX::mode_pacifica; - _mode[FX_MODE_CANDLE_MULTI] = &WS2812FX::mode_candle_multi; - _mode[FX_MODE_SOLID_GLITTER] = &WS2812FX::mode_solid_glitter; - _mode[FX_MODE_SUNRISE] = &WS2812FX::mode_sunrise; - _mode[FX_MODE_PHASED] = &WS2812FX::mode_phased; - _mode[FX_MODE_TWINKLEUP] = &WS2812FX::mode_twinkleup; - _mode[FX_MODE_NOISEPAL] = &WS2812FX::mode_noisepal; - _mode[FX_MODE_SINEWAVE] = &WS2812FX::mode_sinewave; - _mode[FX_MODE_PHASEDNOISE] = &WS2812FX::mode_phased_noise; - _mode[FX_MODE_FLOW] = &WS2812FX::mode_flow; - _mode[FX_MODE_CHUNCHUN] = &WS2812FX::mode_chunchun; - _mode[FX_MODE_DANCING_SHADOWS] = &WS2812FX::mode_dancing_shadows; - _mode[FX_MODE_WASHING_MACHINE] = &WS2812FX::mode_washing_machine; - _mode[FX_MODE_CANDY_CANE] = &WS2812FX::mode_candy_cane; - _mode[FX_MODE_BLENDS] = &WS2812FX::mode_blends; - _mode[FX_MODE_TV_SIMULATOR] = &WS2812FX::mode_tv_simulator; - _mode[FX_MODE_DYNAMIC_SMOOTH] = &WS2812FX::mode_dynamic_smooth; - - _brightness = DEFAULT_BRIGHTNESS; - currentPalette = CRGBPalette16(CRGB::Black); - targetPalette = CloudColors_p; - ablMilliampsMax = 850; - currentMilliamps = 0; - timebase = 0; - resetSegments(); + _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) + _modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) + if (_mode.capacity() <= 1 || _modeData.capacity() <= 1) _modeCount = 1; // memory allocation failed only show Solid + else setupEffectData(); + } + + ~WS2812FX() { + if (customMappingTable) delete[] customMappingTable; + _mode.clear(); + _modeData.clear(); + _segments.clear(); +#ifndef WLED_DISABLE_2D + panel.clear(); +#endif + customPalettes.clear(); } + static WS2812FX* getInstance(void) { return instance; } + void +#ifdef WLED_DEBUG + printSize(), +#endif finalizeInit(), service(void), - blur(uint8_t), - fill(uint32_t), - fade_out(uint8_t r), setMode(uint8_t segid, uint8_t m), - setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0), setColor(uint8_t slot, uint32_t c), setCCT(uint16_t k), setBrightness(uint8_t b, bool direct = false), setRange(uint16_t i, uint16_t i2, uint32_t col), - setShowCallback(show_callback cb), - setTransition(uint16_t t), setTransitionMode(bool t), - calcGammaTable(float), - trigger(void), - setSegment(uint8_t n, uint16_t start, uint16_t stop, uint8_t grouping = 0, uint8_t spacing = 0, uint16_t offset = UINT16_MAX), + purgeSegments(bool force = false), + setSegment(uint8_t n, uint16_t start, uint16_t stop, uint8_t grouping = 1, uint8_t spacing = 0, uint16_t offset = UINT16_MAX, uint16_t startY=0, uint16_t stopY=1), setMainSegmentId(uint8_t n), restartRuntime(), resetSegments(), makeAutoSegments(bool forceReset = false), fixInvalidSegments(), - setPixelColor(uint16_t n, uint32_t c), - setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0), + setPixelColor(int n, uint32_t c), show(void), - setTargetFps(uint8_t fps), - deserializeMap(uint8_t n=0); + setTargetFps(uint8_t fps); + + void setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setColor(slot, RGBW32(r,g,b,w)); } + void fill(uint32_t c) { for (int i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) + void addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp + void setupEffectData(void); // add default effects to the list; defined in FX.cpp + + // outsmart the compiler :) by correctly overloading + inline void setPixelColor(int n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } + inline void setPixelColor(int n, CRGB c) { setPixelColor(n, c.red, c.green, c.blue); } + inline void trigger(void) { _triggered = true; } // Forces the next frame to be computed on all active segments. + inline void setShowCallback(show_callback cb) { _callback = cb; } + inline void setTransition(uint16_t t) { _transitionDur = t; } + inline void appendSegment(const Segment &seg = Segment()) { if (_segments.size() < getMaxSegments()) _segments.push_back(seg); } bool - gammaCorrectBri = false, - gammaCorrectCol = true, checkSegmentAlignment(void), hasRGBWBus(void), hasCCTBus(void), // return true if the strip is being sent pixel updates - isUpdating(void); + isUpdating(void), + deserializeMap(uint8_t n=0); + + inline bool isServicing(void) { return _isServicing; } + inline bool hasWhiteChannel(void) {return _hasWhiteChannel;} + inline bool isOffRefreshRequired(void) {return _isOffRefreshRequired;} uint8_t - paletteFade = 0, - paletteBlend = 0, - milliampsPerLed = 55, - autoWhiteMode = RGBW_MODE_DUAL, - cctBlending = 0, - getBrightness(void), - getModeCount(void), - getPaletteCount(void), - getMaxSegments(void), + paletteFade, + paletteBlend, + milliampsPerLed, + cctBlending, getActiveSegmentsNum(void), getFirstSelectedSegId(void), - getMainSegmentId(void), getLastActiveSegmentId(void), - getTargetFps(void), - setPixelSegment(uint8_t n), - gamma8(uint8_t), - gamma8_cal(uint8_t, float), - get_random_wheel_index(uint8_t); - - inline uint8_t sin_gap(uint16_t in) { - if (in & 0x100) return 0; - return sin8(in + 192); // correct phase shift of sine so that it starts and stops at 0 - } - - int8_t - tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec); + getActiveSegsLightCapabilities(bool selectedOnly = false), + setPixelSegment(uint8_t n); + + inline uint8_t getBrightness(void) { return _brightness; } + inline uint8_t getMaxSegments(void) { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) + inline uint8_t getSegmentsNum(void) { return _segments.size(); } // returns currently present segments + inline uint8_t getCurrSegmentId(void) { return _segment_index; } + inline uint8_t getMainSegmentId(void) { return _mainSegment; } + inline uint8_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT; } // will only return built-in palette count + inline uint8_t getTargetFps() { return _targetFps; } + inline uint8_t getModeCount() { return _modeCount; } uint16_t ablMilliampsMax, currentMilliamps, - triwave16(uint16_t), - getLengthTotal(void), getLengthPhysical(void), + getLengthTotal(void), // will include virtual/nonexistent pixels in matrix getFps(); + inline uint16_t getFrameTime(void) { return _frametime; } + inline uint16_t getMinShowDelay(void) { return MIN_SHOW_DELAY; } + inline uint16_t getLength(void) { return _length; } // 2D matrix may have less pixels than W*H + inline uint16_t getTransition(void) { return _transitionDur; } + uint32_t now, timebase, - color_wheel(uint8_t), - color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255), - color_blend(uint32_t,uint32_t,uint16_t,bool b16=false), - currentColor(uint32_t colorNew, uint8_t tNr), - gamma32(uint32_t), - getLastShow(void), getPixelColor(uint16_t); - WS2812FX::Segment - &getSegment(uint8_t n), - &getFirstSelectedSeg(void), - &getMainSegment(void); + inline uint32_t getLastShow(void) { return _lastShow; } + inline uint32_t segColor(uint8_t i) { return _colors_t[i]; } - WS2812FX::Segment* - getSegments(void); + const char * + getModeData(uint8_t id = 0) { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } - // builtin modes - uint16_t - mode_static(void), - mode_blink(void), - mode_blink_rainbow(void), - mode_strobe(void), - mode_strobe_rainbow(void), - mode_color_wipe(void), - mode_color_sweep(void), - mode_color_wipe_random(void), - mode_color_sweep_random(void), - mode_random_color(void), - mode_dynamic(void), - mode_breath(void), - mode_fade(void), - mode_scan(void), - mode_dual_scan(void), - mode_theater_chase(void), - mode_theater_chase_rainbow(void), - mode_rainbow(void), - mode_rainbow_cycle(void), - mode_running_lights(void), - mode_saw(void), - mode_twinkle(void), - mode_dissolve(void), - mode_dissolve_random(void), - mode_sparkle(void), - mode_flash_sparkle(void), - mode_hyper_sparkle(void), - mode_multi_strobe(void), - mode_android(void), - mode_chase_color(void), - mode_chase_random(void), - mode_chase_rainbow(void), - mode_chase_flash(void), - mode_chase_flash_random(void), - mode_chase_rainbow_white(void), - mode_colorful(void), - mode_traffic_light(void), - mode_running_color(void), - mode_aurora(void), - mode_running_random(void), - mode_larson_scanner(void), - mode_comet(void), - mode_fireworks(void), - mode_rain(void), - mode_tetrix(void), - mode_halloween(void), - mode_fire_flicker(void), - mode_gradient(void), - mode_loading(void), - mode_police(void), - mode_fairy(void), - mode_two_dots(void), - mode_fairytwinkle(void), - mode_running_dual(void), - mode_bicolor_chase(void), - mode_tricolor_chase(void), - mode_tricolor_wipe(void), - mode_tricolor_fade(void), - mode_lightning(void), - mode_icu(void), - mode_multi_comet(void), - mode_dual_larson_scanner(void), - mode_random_chase(void), - mode_oscillate(void), - mode_fire_2012(void), - mode_pride_2015(void), - mode_bpm(void), - mode_juggle(void), - mode_palette(void), - mode_colorwaves(void), - mode_fillnoise8(void), - mode_noise16_1(void), - mode_noise16_2(void), - mode_noise16_3(void), - mode_noise16_4(void), - mode_colortwinkle(void), - mode_lake(void), - mode_meteor(void), - mode_meteor_smooth(void), - mode_railway(void), - mode_ripple(void), - mode_twinklefox(void), - mode_twinklecat(void), - mode_halloween_eyes(void), - mode_static_pattern(void), - mode_tri_static_pattern(void), - mode_spots(void), - mode_spots_fade(void), - mode_glitter(void), - mode_candle(void), - mode_starburst(void), - mode_exploding_fireworks(void), - mode_bouncing_balls(void), - mode_sinelon(void), - mode_sinelon_dual(void), - mode_sinelon_rainbow(void), - mode_popcorn(void), - mode_drip(void), - mode_plasma(void), - mode_percent(void), - mode_ripple_rainbow(void), - mode_heartbeat(void), - mode_pacifica(void), - mode_candle_multi(void), - mode_solid_glitter(void), - mode_sunrise(void), - mode_phased(void), - mode_twinkleup(void), - mode_noisepal(void), - mode_sinewave(void), - mode_phased_noise(void), - mode_flow(void), - mode_chunchun(void), - mode_dancing_shadows(void), - mode_washing_machine(void), - mode_candy_cane(void), - mode_blends(void), - mode_tv_simulator(void), - mode_dynamic_smooth(void); + const char ** + getModeDataSrc(void) { return &(_modeData[0]); } // vectors use arrays for underlying data - private: - uint32_t crgb_to_col(CRGB fastled); - CRGB col_to_crgb(uint32_t); - CRGBPalette16 currentPalette; - CRGBPalette16 targetPalette; + Segment& getSegment(uint8_t id); + inline Segment& getFirstSelectedSeg(void) { return _segments[getFirstSelectedSegId()]; } + inline Segment& getMainSegment(void) { return _segments[getMainSegmentId()]; } + inline Segment* getSegments(void) { return &(_segments[0]); } - uint16_t _length, _virtualSegmentLength; - uint16_t _rand16seed; - uint8_t _brightness; - uint16_t _usedSegmentData = 0; - uint16_t _transitionDur = 750; + // 2D support (panels) + bool + isMatrix; - uint8_t _targetFps = 42; - uint16_t _frametime = (1000/42); - uint16_t _cumulativeFps = 2; +#ifndef WLED_DISABLE_2D + #define WLED_MAX_PANELS 64 + uint8_t + panels; + + typedef struct panel_t { + uint16_t xOffset; // x offset relative to the top left of matrix in LEDs + uint16_t yOffset; // y offset relative to the top left of matrix in LEDs + uint8_t width; // width of the panel + uint8_t height; // height of the panel + union { + uint8_t options; + struct { + bool bottomStart : 1; // starts at bottom? + bool rightStart : 1; // starts on right? + bool vertical : 1; // is vertical? + bool serpentine : 1; // is serpentine? + }; + }; + panel_t() + : xOffset(0) + , yOffset(0) + , width(8) + , height(8) + , options(0) + {} + } Panel; + std::vector panel; +#endif - bool - _isOffRefreshRequired = false, //periodic refresh is required for the strip to remain off. - _hasWhiteChannel = false, - _triggered; + void setUpMatrix(); - mode_ptr _mode[MODE_COUNT]; // SRAM footprint: 4 bytes per element + // outsmart the compiler :) by correctly overloading + inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(y * Segment::maxWidth + x, c); } + inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } - show_callback _callback = nullptr; + inline uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(isMatrix ? y * Segment::maxWidth + x : x);} - // mode helper functions - uint16_t - blink(uint32_t, uint32_t, bool strobe, bool), - candle(bool), - color_wipe(bool, bool), - dynamic(bool), - scan(bool), - running_base(bool,bool), - larson_scanner(bool), - sinelon_base(bool,bool), - dissolve(uint32_t), - chase(uint32_t, uint32_t, uint32_t, bool), - gradient_base(bool), - ripple_base(bool), - police_base(uint32_t, uint32_t), - running(uint32_t, uint32_t, bool theatre=false), - tricolor_chase(uint32_t, uint32_t), - twinklefox_base(bool), - spots_base(uint16_t), - phased_base(uint8_t); - - CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat); - CRGB pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff); + // end 2D support - void - blendPixelColor(uint16_t n, uint32_t color, uint8_t blend), - startTransition(uint8_t oldBri, uint32_t oldCol, uint16_t dur, uint8_t segn, uint8_t slot), - estimateCurrentAndLimitBri(void), - load_gradient_palette(uint8_t), - handle_palette(void); - - uint16_t* customMappingTable = nullptr; - uint16_t customMappingSize = 0; - - uint32_t _lastPaletteChange = 0; - uint32_t _lastShow = 0; - - uint32_t _colors_t[3]; - uint8_t _bri_t; - bool _no_rgb = false; - - uint8_t _segment_index = 0; - uint8_t _segment_index_palette_last = 99; - uint8_t _mainSegment; + void loadCustomPalettes(void); // loads custom palettes from JSON + std::vector customPalettes; // TODO: move custom palettes out of WS2812FX class + + // using public variables to reduce code size increase due to inline function getSegment() (with bounds checking) + // and color transitions + uint32_t _colors_t[3]; // color used for effect (includes transition) + uint16_t _virtualSegmentLength; - segment _segments[MAX_NUM_SEGMENTS] = { // SRAM footprint: 24 bytes per element - // start, stop, offset, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[], capabilities - {0, 7, 0, DEFAULT_SPEED, 128, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}, 0} + std::vector _segments; + friend class Segment; + + private: + uint16_t _length; + uint8_t _brightness; + uint16_t _transitionDur; + + uint8_t _targetFps; + uint16_t _frametime; + uint16_t _cumulativeFps; + + // will require only 1 byte + struct { + bool _isServicing : 1; + bool _isOffRefreshRequired : 1; //periodic refresh is required for the strip to remain off. + bool _hasWhiteChannel : 1; + bool _triggered : 1; }; - segment_runtime _segment_runtimes[MAX_NUM_SEGMENTS]; // SRAM footprint: 28 bytes per element - friend class Segment_runtime; - ColorTransition transitions[MAX_NUM_TRANSITIONS]; //12 bytes per element - friend class ColorTransition; + uint8_t _modeCount; + std::vector _mode; // SRAM footprint: 4 bytes per element + std::vector _modeData; // mode (effect) name and its slider control data array - uint16_t - realPixelIndex(uint16_t i), - transitionProgress(uint8_t tNr); - - public: - inline bool hasWhiteChannel(void) {return _hasWhiteChannel;} - inline bool isOffRefreshRequired(void) {return _isOffRefreshRequired;} + show_callback _callback; + + uint16_t* customMappingTable; + uint16_t customMappingSize; + + unsigned long _lastShow; + + uint8_t _segment_index; + uint8_t _mainSegment; + uint8_t _queuedChangesSegId; + uint16_t _qStart, _qStop, _qStartY, _qStopY; + uint8_t _qGrouping, _qSpacing; + uint16_t _qOffset; + + uint8_t + estimateCurrentAndLimitBri(void); + + void + setUpSegmentFromQueuedChanges(void); }; -//10 names per line -const char JSON_mode_names[] PROGMEM = R"=====([ -"Solid","Blink","Breathe","Wipe","Wipe Random","Random Colors","Sweep","Dynamic","Colorloop","Rainbow", -"Scan","Scan Dual","Fade","Theater","Theater Rainbow","Running","Saw","Twinkle","Dissolve","Dissolve Rnd", -"Sparkle","Sparkle Dark","Sparkle+","Strobe","Strobe Rainbow","Strobe Mega","Blink Rainbow","Android","Chase","Chase Random", -"Chase Rainbow","Chase Flash","Chase Flash Rnd","Rainbow Runner","Colorful","Traffic Light","Sweep Random","Chase 2","Aurora","Stream", -"Scanner","Lighthouse","Fireworks","Rain","Tetrix","Fire Flicker","Gradient","Loading","Police","Fairy", -"Two Dots","Fairytwinkle","Running Dual","Halloween","Chase 3","Tri Wipe","Tri Fade","Lightning","ICU","Multi Comet", -"Scanner Dual","Stream 2","Oscillate","Pride 2015","Juggle","Palette","Fire 2012","Colorwaves","Bpm","Fill Noise", -"Noise 1","Noise 2","Noise 3","Noise 4","Colortwinkles","Lake","Meteor","Meteor Smooth","Railway","Ripple", -"Twinklefox","Twinklecat","Halloween Eyes","Solid Pattern","Solid Pattern Tri","Spots","Spots Fade","Glitter","Candle","Fireworks Starburst", -"Fireworks 1D","Bouncing Balls","Sinelon","Sinelon Dual","Sinelon Rainbow","Popcorn","Drip","Plasma","Percent","Ripple Rainbow", -"Heartbeat","Pacifica","Candle Multi", "Solid Glitter","Sunrise","Phased","Twinkleup","Noise Pal", "Sine","Phased Noise", -"Flow","Chunchun","Dancing Shadows","Washing Machine","Candy Cane","Blends","TV Simulator","Dynamic Smooth" -])====="; - - -const char JSON_palette_names[] PROGMEM = R"=====([ -"Default","* Random Cycle","* Color 1","* Colors 1&2","* Color Gradient","* Colors Only","Party","Cloud","Lava","Ocean", -"Forest","Rainbow","Rainbow Bands","Sunset","Rivendell","Breeze","Red & Blue","Yellowout","Analogous","Splash", -"Pastel","Sunset 2","Beech","Vintage","Departure","Landscape","Beach","Sherbet","Hult","Hult 64", -"Drywet","Jul","Grintage","Rewhi","Tertiary","Fire","Icefire","Cyane","Light Pink","Autumn", -"Magenta","Magred","Yelmag","Yelblu","Orange & Teal","Tiamat","April Night","Orangery","C9","Sakura", -"Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf", -"Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", -"Candy2" -])====="; +extern const char JSON_mode_names[]; +extern const char JSON_palette_names[]; #endif diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp new file mode 100644 index 0000000000..5dc9e9ff21 --- /dev/null +++ b/wled00/FX_2Dfcn.cpp @@ -0,0 +1,594 @@ +/* + FX_2Dfcn.cpp contains all 2D utility functions + + LICENSE + The MIT License (MIT) + Copyright (c) 2022 Blaz Kristan (https://blaz.at/home) + 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. + + Parts of the code adapted from WLED Sound Reactive +*/ +#include "wled.h" +#include "FX.h" +#include "palettes.h" + +// setUpMatrix() - constructs ledmap array from matrix of panels with WxH pixels +// this converts physical (possibly irregular) LED arrangement into well defined +// array of logical pixels: fist entry corresponds to left-topmost logical pixel +// followed by horizontal pixels, when Segment::maxWidth logical pixels are added they +// are followed by next row (down) of Segment::maxWidth pixels (and so forth) +// note: matrix may be comprised of multiple panels each with different orientation +// but ledmap takes care of that. ledmap is constructed upon initialization +// so matrix should disable regular ledmap processing +void WS2812FX::setUpMatrix() { +#ifndef WLED_DISABLE_2D + // erase old ledmap, just in case. + if (customMappingTable != nullptr) delete[] customMappingTable; + customMappingTable = nullptr; + customMappingSize = 0; + + // isMatrix is set in cfg.cpp or set.cpp + if (isMatrix) { + // calculate width dynamically because it will have gaps + Segment::maxWidth = 1; + Segment::maxHeight = 1; + for (size_t i = 0; i < panel.size(); i++) { + Panel &p = panel[i]; + if (p.xOffset + p.width > Segment::maxWidth) { + Segment::maxWidth = p.xOffset + p.width; + } + if (p.yOffset + p.height > Segment::maxHeight) { + Segment::maxHeight = p.yOffset + p.height; + } + } + + // safety check + if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth <= 1 || Segment::maxHeight <= 1) { + DEBUG_PRINTLN(F("2D Bounds error.")); + isMatrix = false; + Segment::maxWidth = _length; + Segment::maxHeight = 1; + panels = 0; + panel.clear(); // release memory allocated by panels + resetSegments(); + return; + } + + customMappingTable = new uint16_t[Segment::maxWidth * Segment::maxHeight]; + + if (customMappingTable != nullptr) { + customMappingSize = Segment::maxWidth * Segment::maxHeight; + + // fill with empty in case we don't fill the entire matrix + for (size_t i = 0; i< customMappingSize; i++) { + customMappingTable[i] = (uint16_t)-1; + } + + // we will try to load a "gap" array (a JSON file) + // the array has to have the same amount of values as mapping array (or larger) + // "gap" array is used while building ledmap (mapping array) + // and discarded afterwards as it has no meaning after the process + // content of the file is just raw JSON array in the form of [val1,val2,val3,...] + // there are no other "key":"value" pairs in it + // allowed values are: -1 (missing pixel/no LED attached), 0 (inactive/unused pixel), 1 (active/used pixel) + char fileName[32]; strcpy_P(fileName, PSTR("/2d-gaps.json")); // reduce flash footprint + bool isFile = WLED_FS.exists(fileName); + size_t gapSize = 0; + int8_t *gapTable = nullptr; + + if (isFile && requestJSONBufferLock(20)) { + DEBUG_PRINT(F("Reading LED gap from ")); + DEBUG_PRINTLN(fileName); + // read the array into global JSON buffer + if (readObjectFromFile(fileName, nullptr, &doc)) { + // the array is similar to ledmap, except it has only 3 values: + // -1 ... missing pixel (do not increase pixel count) + // 0 ... inactive pixel (it does count, but should be mapped out (-1)) + // 1 ... active pixel (it will count and will be mapped) + JsonArray map = doc.as(); + gapSize = map.size(); + if (!map.isNull() && gapSize >= customMappingSize) { // not an empty map + gapTable = new int8_t[gapSize]; + if (gapTable) for (size_t i = 0; i < gapSize; i++) { + gapTable[i] = constrain(map[i], -1, 1); + } + } + } + DEBUG_PRINTLN(F("Gaps loaded.")); + releaseJSONBufferLock(); + } + + uint16_t x, y, pix=0; //pixel + for (size_t pan = 0; pan < panel.size(); pan++) { + Panel &p = panel[pan]; + uint16_t h = p.vertical ? p.height : p.width; + uint16_t v = p.vertical ? p.width : p.height; + for (size_t j = 0; j < v; j++){ + for(size_t i = 0; i < h; i++) { + y = (p.vertical?p.rightStart:p.bottomStart) ? v-j-1 : j; + x = (p.vertical?p.bottomStart:p.rightStart) ? h-i-1 : i; + x = p.serpentine && j%2 ? h-x-1 : x; + size_t index = (p.yOffset + (p.vertical?x:y)) * Segment::maxWidth + p.xOffset + (p.vertical?y:x); + if (!gapTable || (gapTable && gapTable[index] > 0)) customMappingTable[index] = pix; // a useful pixel (otherwise -1 is retained) + if (!gapTable || (gapTable && gapTable[index] >= 0)) pix++; // not a missing pixel + } + } + } + + // delete gap array as we no longer need it + if (gapTable) delete[] gapTable; + + #ifdef WLED_DEBUG + DEBUG_PRINT(F("Matrix ledmap:")); + for (unsigned i=0; i= 1) + return isActive() ? (x%width) + (y%height) * width : 0; +} + +void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) +{ + if (!isActive()) return; // not active + if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit + + uint8_t _bri_t = currentBri(); + if (_bri_t < 255) { + byte r = scale8(R(col), _bri_t); + byte g = scale8(G(col), _bri_t); + byte b = scale8(B(col), _bri_t); + byte w = scale8(W(col), _bri_t); + col = RGBW32(r, g, b, w); + } + + if (reverse ) x = virtualWidth() - x - 1; + if (reverse_y) y = virtualHeight() - y - 1; + if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + + x *= groupLength(); // expand to physical pixels + y *= groupLength(); // expand to physical pixels + if (x >= width() || y >= height()) return; // if pixel would fall out of segment just exit + + uint32_t tmpCol = col; + for (int j = 0; j < grouping; j++) { // groupping vertically + for (int g = 0; g < grouping; g++) { // groupping horizontally + uint16_t xX = (x+g), yY = (y+j); + if (xX >= width() || yY >= height()) continue; // we have reached one dimension's end + +#ifndef WLED_DISABLE_MODE_BLEND + // if blending modes, blend with underlying pixel + if (_modeBlend) tmpCol = color_blend(strip.getPixelColorXY(start + xX, startY + yY), col, 0xFFFFU - progress(), true); +#endif + + strip.setPixelColorXY(start + xX, startY + yY, tmpCol); + + if (mirror) { //set the corresponding horizontally mirrored pixel + if (transpose) strip.setPixelColorXY(start + xX, startY + height() - yY - 1, tmpCol); + else strip.setPixelColorXY(start + width() - xX - 1, startY + yY, tmpCol); + } + if (mirror_y) { //set the corresponding vertically mirrored pixel + if (transpose) strip.setPixelColorXY(start + width() - xX - 1, startY + yY, tmpCol); + else strip.setPixelColorXY(start + xX, startY + height() - yY - 1, tmpCol); + } + if (mirror_y && mirror) { //set the corresponding vertically AND horizontally mirrored pixel + strip.setPixelColorXY(width() - xX - 1, height() - yY - 1, tmpCol); + } + } + } +} + +// anti-aliased version of setPixelColorXY() +void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) +{ + if (!isActive()) return; // not active + if (x<0.0f || x>1.0f || y<0.0f || y>1.0f) return; // not normalized + + const uint16_t cols = virtualWidth(); + const uint16_t rows = virtualHeight(); + + float fX = x * (cols-1); + float fY = y * (rows-1); + if (aa) { + uint16_t xL = roundf(fX-0.49f); + uint16_t xR = roundf(fX+0.49f); + uint16_t yT = roundf(fY-0.49f); + uint16_t yB = roundf(fY+0.49f); + float dL = (fX - xL)*(fX - xL); + float dR = (xR - fX)*(xR - fX); + float dT = (fY - yT)*(fY - yT); + float dB = (yB - fY)*(yB - fY); + uint32_t cXLYT = getPixelColorXY(xL, yT); + uint32_t cXRYT = getPixelColorXY(xR, yT); + uint32_t cXLYB = getPixelColorXY(xL, yB); + uint32_t cXRYB = getPixelColorXY(xR, yB); + + if (xL!=xR && yT!=yB) { + setPixelColorXY(xL, yT, color_blend(col, cXLYT, uint8_t(sqrtf(dL*dT)*255.0f))); // blend TL pixel + setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(sqrtf(dR*dT)*255.0f))); // blend TR pixel + setPixelColorXY(xL, yB, color_blend(col, cXLYB, uint8_t(sqrtf(dL*dB)*255.0f))); // blend BL pixel + setPixelColorXY(xR, yB, color_blend(col, cXRYB, uint8_t(sqrtf(dR*dB)*255.0f))); // blend BR pixel + } else if (xR!=xL && yT==yB) { + setPixelColorXY(xR, yT, color_blend(col, cXLYT, uint8_t(dL*255.0f))); // blend L pixel + setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(dR*255.0f))); // blend R pixel + } else if (xR==xL && yT!=yB) { + setPixelColorXY(xR, yT, color_blend(col, cXLYT, uint8_t(dT*255.0f))); // blend T pixel + setPixelColorXY(xL, yB, color_blend(col, cXLYB, uint8_t(dB*255.0f))); // blend B pixel + } else { + setPixelColorXY(xL, yT, col); // exact match (x & y land on a pixel) + } + } else { + setPixelColorXY(uint16_t(roundf(fX)), uint16_t(roundf(fY)), col); + } +} + +// returns RGBW values of pixel +uint32_t Segment::getPixelColorXY(uint16_t x, uint16_t y) { + if (!isActive()) return 0; // not active + if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit + if (reverse ) x = virtualWidth() - x - 1; + if (reverse_y) y = virtualHeight() - y - 1; + if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + x *= groupLength(); // expand to physical pixels + y *= groupLength(); // expand to physical pixels + if (x >= width() || y >= height()) return 0; + return strip.getPixelColorXY(start + x, startY + y); +} + +// Blends the specified color with the existing pixel color. +void Segment::blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { + setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); +} + +// Adds the specified color with the existing pixel color perserving color balance. +void Segment::addPixelColorXY(int x, int y, uint32_t color, bool fast) { + if (!isActive()) return; // not active + if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit + setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, fast)); +} + +void Segment::fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { + if (!isActive()) return; // not active + setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); +} + +// blurRow: perform a blur on a row of a rectangular matrix +void Segment::blurRow(uint16_t row, fract8 blur_amount) { + if (!isActive() || blur_amount == 0) return; // not active + const uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); + + if (row >= rows) return; + // blur one row + uint8_t keep = 255 - blur_amount; + uint8_t seep = blur_amount >> 1; + CRGB carryover = CRGB::Black; + for (unsigned x = 0; x < cols; x++) { + CRGB cur = getPixelColorXY(x, row); + CRGB before = cur; // remember color before blur + CRGB part = cur; + part.nscale8(seep); + cur.nscale8(keep); + cur += carryover; + if (x>0) { + CRGB prev = CRGB(getPixelColorXY(x-1, row)) + part; + setPixelColorXY(x-1, row, prev); + } + if (before != cur) // optimization: only set pixel if color has changed + setPixelColorXY(x, row, cur); + carryover = part; + } +} + +// blurCol: perform a blur on a column of a rectangular matrix +void Segment::blurCol(uint16_t col, fract8 blur_amount) { + if (!isActive() || blur_amount == 0) return; // not active + const uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); + + if (col >= cols) return; + // blur one column + uint8_t keep = 255 - blur_amount; + uint8_t seep = blur_amount >> 1; + CRGB carryover = CRGB::Black; + for (unsigned y = 0; y < rows; y++) { + CRGB cur = getPixelColorXY(col, y); + CRGB part = cur; + CRGB before = cur; // remember color before blur + part.nscale8(seep); + cur.nscale8(keep); + cur += carryover; + if (y>0) { + CRGB prev = CRGB(getPixelColorXY(col, y-1)) + part; + setPixelColorXY(col, y-1, prev); + } + if (before != cur) // optimization: only set pixel if color has changed + setPixelColorXY(col, y, cur); + carryover = part; + } +} + +// 1D Box blur (with added weight - blur_amount: [0=no blur, 255=max blur]) +void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { + if (!isActive() || blur_amount == 0) return; // not active + const uint16_t cols = virtualWidth(); + const uint16_t rows = virtualHeight(); + const uint16_t dim1 = vertical ? rows : cols; + const uint16_t dim2 = vertical ? cols : rows; + if (i >= dim2) return; + const float seep = blur_amount/255.f; + const float keep = 3.f - 2.f*seep; + // 1D box blur + CRGB tmp[dim1]; + for (int j = 0; j < dim1; j++) { + uint16_t x = vertical ? i : j; + uint16_t y = vertical ? j : i; + int16_t xp = vertical ? x : x-1; // "signed" to prevent underflow + int16_t yp = vertical ? y-1 : y; // "signed" to prevent underflow + uint16_t xn = vertical ? x : x+1; + uint16_t yn = vertical ? y+1 : y; + CRGB curr = getPixelColorXY(x,y); + CRGB prev = (xp<0 || yp<0) ? CRGB::Black : getPixelColorXY(xp,yp); + CRGB next = ((vertical && yn>=dim1) || (!vertical && xn>=dim1)) ? CRGB::Black : getPixelColorXY(xn,yn); + uint16_t r, g, b; + r = (curr.r*keep + (prev.r + next.r)*seep) / 3; + g = (curr.g*keep + (prev.g + next.g)*seep) / 3; + b = (curr.b*keep + (prev.b + next.b)*seep) / 3; + tmp[j] = CRGB(r,g,b); + } + for (int j = 0; j < dim1; j++) { + uint16_t x = vertical ? i : j; + uint16_t y = vertical ? j : i; + setPixelColorXY(x, y, tmp[j]); + } +} + +// blur1d: one-dimensional blur filter. Spreads light to 2 line neighbors. +// blur2d: two-dimensional blur filter. Spreads light to 8 XY neighbors. +// +// 0 = no spread at all +// 64 = moderate spreading +// 172 = maximum smooth, even spreading +// +// 173..255 = wider spreading, but increasing flicker +// +// Total light is NOT entirely conserved, so many repeated +// calls to 'blur' will also result in the light fading, +// eventually all the way to black; this is by design so that +// it can be used to (slowly) clear the LEDs to black. + +void Segment::blur1d(fract8 blur_amount) { + const uint16_t rows = virtualHeight(); + for (unsigned y = 0; y < rows; y++) blurRow(y, blur_amount); +} + +void Segment::moveX(int8_t delta, bool wrap) { + if (!isActive()) return; // not active + const uint16_t cols = virtualWidth(); + const uint16_t rows = virtualHeight(); + if (!delta || abs(delta) >= cols) return; + uint32_t newPxCol[cols]; + for (int y = 0; y < rows; y++) { + if (delta > 0) { + for (int x = 0; x < cols-delta; x++) newPxCol[x] = getPixelColorXY((x + delta), y); + for (int x = cols-delta; x < cols; x++) newPxCol[x] = getPixelColorXY(wrap ? (x + delta) - cols : x, y); + } else { + for (int x = cols-1; x >= -delta; x--) newPxCol[x] = getPixelColorXY((x + delta), y); + for (int x = -delta-1; x >= 0; x--) newPxCol[x] = getPixelColorXY(wrap ? (x + delta) + cols : x, y); + } + for (int x = 0; x < cols; x++) setPixelColorXY(x, y, newPxCol[x]); + } +} + +void Segment::moveY(int8_t delta, bool wrap) { + if (!isActive()) return; // not active + const uint16_t cols = virtualWidth(); + const uint16_t rows = virtualHeight(); + if (!delta || abs(delta) >= rows) return; + uint32_t newPxCol[rows]; + for (int x = 0; x < cols; x++) { + if (delta > 0) { + for (int y = 0; y < rows-delta; y++) newPxCol[y] = getPixelColorXY(x, (y + delta)); + for (int y = rows-delta; y < rows; y++) newPxCol[y] = getPixelColorXY(x, wrap ? (y + delta) - rows : y); + } else { + for (int y = rows-1; y >= -delta; y--) newPxCol[y] = getPixelColorXY(x, (y + delta)); + for (int y = -delta-1; y >= 0; y--) newPxCol[y] = getPixelColorXY(x, wrap ? (y + delta) + rows : y); + } + for (int y = 0; y < rows; y++) setPixelColorXY(x, y, newPxCol[y]); + } +} + +// move() - move all pixels in desired direction delta number of pixels +// @param dir direction: 0=left, 1=left-up, 2=up, 3=right-up, 4=right, 5=right-down, 6=down, 7=left-down +// @param delta number of pixels to move +// @param wrap around +void Segment::move(uint8_t dir, uint8_t delta, bool wrap) { + if (delta==0) return; + switch (dir) { + case 0: moveX( delta, wrap); break; + case 1: moveX( delta, wrap); moveY( delta, wrap); break; + case 2: moveY( delta, wrap); break; + case 3: moveX(-delta, wrap); moveY( delta, wrap); break; + case 4: moveX(-delta, wrap); break; + case 5: moveX(-delta, wrap); moveY(-delta, wrap); break; + case 6: moveY(-delta, wrap); break; + case 7: moveX( delta, wrap); moveY(-delta, wrap); break; + } +} + +void Segment::draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { + if (!isActive() || radius == 0) return; // not active + // Bresenham’s Algorithm + int d = 3 - (2*radius); + int y = radius, x = 0; + while (y >= x) { + setPixelColorXY(cx+x, cy+y, col); + setPixelColorXY(cx-x, cy+y, col); + setPixelColorXY(cx+x, cy-y, col); + setPixelColorXY(cx-x, cy-y, col); + setPixelColorXY(cx+y, cy+x, col); + setPixelColorXY(cx-y, cy+x, col); + setPixelColorXY(cx+y, cy-x, col); + setPixelColorXY(cx-y, cy-x, col); + x++; + if (d > 0) { + y--; + d += 4 * (x - y) + 10; + } else { + d += 4 * x + 6; + } + } +} + +// by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs +void Segment::fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { + if (!isActive() || radius == 0) return; // not active + const uint16_t cols = virtualWidth(); + const uint16_t rows = virtualHeight(); + for (int16_t y = -radius; y <= radius; y++) { + for (int16_t x = -radius; x <= radius; x++) { + if (x * x + y * y <= radius * radius && + int16_t(cx)+x>=0 && int16_t(cy)+y>=0 && + int16_t(cx)+x= cols || x1 >= cols || y0 >= rows || y1 >= rows) return; + const int16_t dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; + for (;;) { + setPixelColorXY(x0,y0,c); + if (x0==x1 && y0==y1) break; + e2 = err; + if (e2 >-dx) { err -= dy; x0 += sx; } + if (e2 < dy) { err += dx; y0 += sy; } + } +} + +#include "src/font/console_font_4x6.h" +#include "src/font/console_font_5x8.h" +#include "src/font/console_font_5x12.h" +#include "src/font/console_font_6x8.h" +#include "src/font/console_font_7x9.h" + +// draws a raster font character on canvas +// only supports: 4x6=24, 5x8=40, 5x12=60, 6x8=48 and 7x9=63 fonts ATM +void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2, int8_t rotate) { + if (!isActive()) return; // not active + if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported + chr -= 32; // align with font table entries + const uint16_t cols = virtualWidth(); + const uint16_t rows = virtualHeight(); + const int font = w*h; + + CRGB col = CRGB(color); + CRGBPalette16 grad = CRGBPalette16(col, col2 ? CRGB(col2) : col); + + //if (w<5 || w>6 || h!=8) return; + for (int i = 0; i= cols || y0 < 0 || y0 >= rows) continue; // drawing off-screen + if (((bits>>(j+(8-w))) & 0x01)) { // bit set + setPixelColorXY(x0, y0, col); + } + } + } +} + +#define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8)) +void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel procedure by reddit u/sutaburosu + if (!isActive()) return; // not active + // extract the fractional parts and derive their inverses + uint8_t xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; + // calculate the intensities for each affected pixel + uint8_t wu[4] = {WU_WEIGHT(ix, iy), WU_WEIGHT(xx, iy), + WU_WEIGHT(ix, yy), WU_WEIGHT(xx, yy)}; + // multiply the intensities by the colour, and saturating-add them to the pixels + for (int i = 0; i < 4; i++) { + CRGB led = getPixelColorXY((x >> 8) + (i & 1), (y >> 8) + ((i >> 1) & 1)); + led.r = qadd8(led.r, c.r * wu[i] >> 8); + led.g = qadd8(led.g, c.g * wu[i] >> 8); + led.b = qadd8(led.b, c.b * wu[i] >> 8); + setPixelColorXY(int((x >> 8) + (i & 1)), int((y >> 8) + ((i >> 1) & 1)), led); + } +} +#undef WU_WEIGHT + +#endif // WLED_DISABLE_2D diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 8bbec34a81..10847ef572 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -60,45 +60,1075 @@ #define DEFAULT_LED_TYPE TYPE_WS2812_RGB #endif +#ifndef DEFAULT_LED_COLOR_ORDER + #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB +#endif + + #if MAX_NUM_SEGMENTS < WLED_MAX_BUSSES #error "Max segments must be at least max number of busses!" #endif + +/////////////////////////////////////////////////////////////////////////////// +// Segment class implementation +/////////////////////////////////////////////////////////////////////////////// +uint16_t Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] +uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; +uint16_t Segment::maxHeight = 1; + +CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black); +CRGBPalette16 Segment::_randomPalette = CRGBPalette16(DEFAULT_COLOR); +CRGBPalette16 Segment::_newRandomPalette = CRGBPalette16(DEFAULT_COLOR); +unsigned long Segment::_lastPaletteChange = 0; // perhaps it should be per segment + +#ifndef WLED_DISABLE_MODE_BLEND +bool Segment::_modeBlend = false; +#endif + +// copy constructor +Segment::Segment(const Segment &orig) { + //DEBUG_PRINTF("-- Copy segment constructor: %p -> %p\n", &orig, this); + memcpy((void*)this, (void*)&orig, sizeof(Segment)); + _t = nullptr; // copied segment cannot be in transition + name = nullptr; + data = nullptr; + _dataLen = 0; + if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } + if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } +} + +// move constructor +Segment::Segment(Segment &&orig) noexcept { + //DEBUG_PRINTF("-- Move segment constructor: %p -> %p\n", &orig, this); + memcpy((void*)this, (void*)&orig, sizeof(Segment)); + orig._t = nullptr; // old segment cannot be in transition any more + orig.name = nullptr; + orig.data = nullptr; + orig._dataLen = 0; +} + +// copy assignment +Segment& Segment::operator= (const Segment &orig) { + //DEBUG_PRINTF("-- Copying segment: %p -> %p\n", &orig, this); + if (this != &orig) { + // clean destination + if (name) { delete[] name; name = nullptr; } + stopTransition(); + deallocateData(); + // copy source + memcpy((void*)this, (void*)&orig, sizeof(Segment)); + // erase pointers to allocated data + data = nullptr; + _dataLen = 0; + // copy source data + if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } + if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } + } + return *this; +} + +// move assignment +Segment& Segment::operator= (Segment &&orig) noexcept { + //DEBUG_PRINTF("-- Moving segment: %p -> %p\n", &orig, this); + if (this != &orig) { + if (name) { delete[] name; name = nullptr; } // free old name + stopTransition(); + deallocateData(); // free old runtime data + memcpy((void*)this, (void*)&orig, sizeof(Segment)); + orig.name = nullptr; + orig.data = nullptr; + orig._dataLen = 0; + orig._t = nullptr; // old segment cannot be in transition + } + return *this; +} + +bool Segment::allocateData(size_t len) { + if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) + if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + return true; + } + //DEBUG_PRINTF("-- Allocating data (%d): %p\n", len, this); + deallocateData(); + if (len == 0) return false; // nothing to do + if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { + // not enough memory + DEBUG_PRINT(F("!!! Effect RAM depleted: ")); + DEBUG_PRINTF("%d/%d !!!\n", len, Segment::getUsedSegmentData()); + return false; + } + // do not use SPI RAM on ESP32 since it is slow + data = (byte*) malloc(len); + if (!data) { DEBUG_PRINTLN(F("!!! Allocation failed. !!!")); return false; } //allocation failed + Segment::addUsedSegmentData(len); + //DEBUG_PRINTF("--- Allocated data (%p): %d/%d -> %p\n", this, len, Segment::getUsedSegmentData(), data); + _dataLen = len; + memset(data, 0, len); + return true; +} + +void Segment::deallocateData() { + if (!data) { _dataLen = 0; return; } + //DEBUG_PRINTF("--- Released data (%p): %d/%d -> %p\n", this, _dataLen, Segment::getUsedSegmentData(), data); + if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer + free(data); + } else { + DEBUG_PRINT(F("---- Released data ")); + DEBUG_PRINTF("(%p): ", this); + DEBUG_PRINT(F("inconsistent UsedSegmentData ")); + DEBUG_PRINTF("(%d/%d)", _dataLen, Segment::getUsedSegmentData()); + DEBUG_PRINTLN(F(", cowardly refusing to free nothing.")); + } + data = nullptr; + Segment::addUsedSegmentData(_dataLen <= Segment::getUsedSegmentData() ? -_dataLen : -Segment::getUsedSegmentData()); + _dataLen = 0; +} + +/** + * If reset of this segment was requested, clears runtime + * settings of this segment. + * Must not be called while an effect mode function is running + * because it could access the data buffer and this method + * may free that data buffer. + */ +void Segment::resetIfRequired() { + if (!reset) return; + //DEBUG_PRINTF("-- Segment reset: %p\n", this); + deallocateData(); + next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; + reset = false; +} + +CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { + if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; + if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // TODO remove strip dependency by moving customPalettes out of strip + //default palette. Differs depending on effect + if (pal == 0) switch (mode) { + case FX_MODE_FIRE_2012 : pal = 35; break; // heat palette + case FX_MODE_COLORWAVES : pal = 26; break; // landscape 33 + case FX_MODE_FILLNOISE8 : pal = 9; break; // ocean colors + case FX_MODE_NOISE16_1 : pal = 20; break; // Drywet + case FX_MODE_NOISE16_2 : pal = 43; break; // Blue cyan yellow + case FX_MODE_NOISE16_3 : pal = 35; break; // heat palette + case FX_MODE_NOISE16_4 : pal = 26; break; // landscape 33 + case FX_MODE_GLITTER : pal = 11; break; // rainbow colors + case FX_MODE_SUNRISE : pal = 35; break; // heat palette + case FX_MODE_RAILWAY : pal = 3; break; // prim + sec + case FX_MODE_2DSOAP : pal = 11; break; // rainbow colors + } + switch (pal) { + case 0: //default palette. Exceptions for specific effects above + targetPalette = PartyColors_p; break; + case 1: {//periodically replace palette with a random one + unsigned long timeSinceLastChange = millis() - _lastPaletteChange; + if (timeSinceLastChange > randomPaletteChangeTime * 1000U) { + _randomPalette = _newRandomPalette; + _newRandomPalette = CRGBPalette16( + CHSV(random8(), random8(160, 255), random8(128, 255)), + CHSV(random8(), random8(160, 255), random8(128, 255)), + CHSV(random8(), random8(160, 255), random8(128, 255)), + CHSV(random8(), random8(160, 255), random8(128, 255))); + _lastPaletteChange = millis(); + handleRandomPalette(); // do a 1st pass of blend + } + targetPalette = _randomPalette; + break;} + case 2: {//primary color only + CRGB prim = gamma32(colors[0]); + targetPalette = CRGBPalette16(prim); break;} + case 3: {//primary + secondary + CRGB prim = gamma32(colors[0]); + CRGB sec = gamma32(colors[1]); + targetPalette = CRGBPalette16(prim,prim,sec,sec); break;} + case 4: {//primary + secondary + tertiary + CRGB prim = gamma32(colors[0]); + CRGB sec = gamma32(colors[1]); + CRGB ter = gamma32(colors[2]); + targetPalette = CRGBPalette16(ter,sec,prim); break;} + case 5: {//primary + secondary (+tertiary if not off), more distinct + CRGB prim = gamma32(colors[0]); + CRGB sec = gamma32(colors[1]); + if (colors[2]) { + CRGB ter = gamma32(colors[2]); + targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,ter,ter,ter,ter,ter,prim); + } else { + targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,sec,sec,sec); + } + break;} + case 6: //Party colors + targetPalette = PartyColors_p; break; + case 7: //Cloud colors + targetPalette = CloudColors_p; break; + case 8: //Lava colors + targetPalette = LavaColors_p; break; + case 9: //Ocean colors + targetPalette = OceanColors_p; break; + case 10: //Forest colors + targetPalette = ForestColors_p; break; + case 11: //Rainbow colors + targetPalette = RainbowColors_p; break; + case 12: //Rainbow stripe colors + targetPalette = RainbowStripeColors_p; break; + default: //progmem palettes + if (pal>245) { + targetPalette = strip.customPalettes[255-pal]; // we checked bounds above + } else { + byte tcp[72]; + memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72); + targetPalette.loadDynamicGradientPalette(tcp); + } + break; + } + return targetPalette; +} + +void Segment::startTransition(uint16_t dur) { + if (dur == 0) { + if (isInTransition()) _t->_dur = dur; // this will stop transition in next handleTransition() + return; + } + if (isInTransition()) return; // already in transition no need to store anything + + // starting a transition has to occur before change so we get current values 1st + _t = new Transition(dur); // no previous transition running + if (!_t) return; // failed to allocate data + + //DEBUG_PRINTF("-- Started transition: %p\n", this); + loadPalette(_t->_palT, palette); + _t->_briT = on ? opacity : 0; + _t->_cctT = cct; +#ifndef WLED_DISABLE_MODE_BLEND + if (modeBlending) { + swapSegenv(_t->_segT); + _t->_modeT = mode; + _t->_segT._dataLenT = 0; + _t->_segT._dataT = nullptr; + if (_dataLen > 0 && data) { + _t->_segT._dataT = (byte *)malloc(_dataLen); + if (_t->_segT._dataT) { + //DEBUG_PRINTF("-- Allocated duplicate data (%d): %p\n", _dataLen, _t->_segT._dataT); + memcpy(_t->_segT._dataT, data, _dataLen); + _t->_segT._dataLenT = _dataLen; + } + } + } else { + for (size_t i=0; i_segT._colorT[i] = colors[i]; + } +#else + for (size_t i=0; i_colorT[i] = colors[i]; +#endif +} + +void Segment::stopTransition() { + //DEBUG_PRINTF("-- Stopping transition: %p\n", this); + if (isInTransition()) { + #ifndef WLED_DISABLE_MODE_BLEND + if (_t->_segT._dataT && _t->_segT._dataLenT > 0) { + //DEBUG_PRINTF("-- Released duplicate data (%d): %p\n", _t->_segT._dataLenT, _t->_segT._dataT); + free(_t->_segT._dataT); + _t->_segT._dataT = nullptr; + _t->_segT._dataLenT = 0; + } + #endif + delete _t; + _t = nullptr; + } +} + +void Segment::handleTransition() { + uint16_t _progress = progress(); + if (_progress == 0xFFFFU) stopTransition(); +} + +// transition progression between 0-65535 +uint16_t Segment::progress() { + if (isInTransition()) { + unsigned diff = millis() - _t->_start; + if (_t->_dur > 0 && diff < _t->_dur) return diff * 0xFFFFU / _t->_dur; + } + return 0xFFFFU; +} + +#ifndef WLED_DISABLE_MODE_BLEND +void Segment::swapSegenv(tmpsegd_t &tmpSeg) { + //DEBUG_PRINTF("-- Saving temp seg: %p (%p)\n", this, tmpSeg); + tmpSeg._optionsT = options; + for (size_t i=0; i_segT)) { + // swap SEGENV with transitional data + options = _t->_segT._optionsT; + for (size_t i=0; i_segT._colorT[i]; + speed = _t->_segT._speedT; + intensity = _t->_segT._intensityT; + custom1 = _t->_segT._custom1T; + custom2 = _t->_segT._custom2T; + custom3 = _t->_segT._custom3T; + check1 = _t->_segT._check1T; + check2 = _t->_segT._check2T; + check3 = _t->_segT._check3T; + aux0 = _t->_segT._aux0T; + aux1 = _t->_segT._aux1T; + step = _t->_segT._stepT; + call = _t->_segT._callT; + data = _t->_segT._dataT; + _dataLen = _t->_segT._dataLenT; + } + //DEBUG_PRINTF("-- temp seg data: %p (%d,%p)\n", this, _dataLen, data); +} + +void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { + //DEBUG_PRINTF("-- Restoring temp seg: %p (%p)\n", this, tmpSeg); + if (_t && &(_t->_segT) != &tmpSeg) { + // update possibly changed variables to keep old effect running correctly + _t->_segT._aux0T = aux0; + _t->_segT._aux1T = aux1; + _t->_segT._stepT = step; + _t->_segT._callT = call; + //if (_t->_segT._dataT != data) DEBUG_PRINTF("--- data re-allocated: (%p) %p -> %p\n", this, _t->_segT._dataT, data); + _t->_segT._dataT = data; + _t->_segT._dataLenT = _dataLen; + } + options = tmpSeg._optionsT; + for (size_t i=0; i_cctT : _t->_briT) * (0xFFFFU - prog); + return curBri / 0xFFFFU; + } + return (useCct ? cct : (on ? opacity : 0)); +} + +uint8_t Segment::currentMode() { +#ifndef WLED_DISABLE_MODE_BLEND + uint16_t prog = progress(); + if (modeBlending && prog < 0xFFFFU) return _t->_modeT; +#endif + return mode; +} + +uint32_t Segment::currentColor(uint8_t slot) { +#ifndef WLED_DISABLE_MODE_BLEND + return isInTransition() ? color_blend(_t->_segT._colorT[slot], colors[slot], progress(), true) : colors[slot]; +#else + return isInTransition() ? color_blend(_t->_colorT[slot], colors[slot], progress(), true) : colors[slot]; +#endif +} + +void Segment::setCurrentPalette() { + loadPalette(_currentPalette, palette); + unsigned prog = progress(); + if (strip.paletteFade && prog < 0xFFFFU) { + // blend palettes + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) + // minimum blend time is 100ms maximum is 65535ms + unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); + _currentPalette = _t->_palT; // copy transitioning/temporary palette + } +} + +// relies on WS2812FX::service() to call it max every 8ms or more (MIN_SHOW_DELAY) +void Segment::handleRandomPalette() { + // just do a blend; if the palettes are identical it will just compare 48 bytes (same as _randomPalette == _newRandomPalette) + // this will slowly blend _newRandomPalette into _randomPalette every 15ms or 8ms (depending on MIN_SHOW_DELAY) + nblendPaletteTowardPalette(_randomPalette, _newRandomPalette, 48); +} + +// segId is given when called from network callback, changes are queued if that segment is currently in its effect function +void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y, uint8_t segId) { + // return if neither bounds nor grouping have changed + bool boundsUnchanged = (start == i1 && stop == i2); + #ifndef WLED_DISABLE_2D + if (Segment::maxHeight>1) boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D + #endif + if (boundsUnchanged + && (!grp || (grouping == grp && spacing == spc)) + && (ofs == UINT16_MAX || ofs == offset)) return; + + stateChanged = true; // send UDP/WS broadcast + + if (stop) fill(BLACK); // turn old segment range off (clears pixels if changing spacing) + if (grp) { // prevent assignment of 0 + grouping = grp; + spacing = spc; + } else { + grouping = 1; + spacing = 0; + } + if (ofs < UINT16_MAX) offset = ofs; + + DEBUG_PRINT(F("setUp segment: ")); DEBUG_PRINT(i1); + DEBUG_PRINT(','); DEBUG_PRINT(i2); + DEBUG_PRINT(F(" -> ")); DEBUG_PRINT(i1Y); + DEBUG_PRINT(','); DEBUG_PRINTLN(i2Y); + markForReset(); + if (boundsUnchanged) return; + + // apply change immediately + if (i2 <= i1) { //disable segment + stop = 0; + 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()) : (i2 > Segment::maxWidth ? Segment::maxWidth : MAX(1,i2)); + startY = 0; + stopY = 1; + #ifndef WLED_DISABLE_2D + if (Segment::maxHeight>1) { // 2D + if (i1Y < Segment::maxHeight) startY = i1Y; + stopY = i2Y > Segment::maxHeight ? Segment::maxHeight : MAX(1,i2Y); + } + #endif + // safety check + if (start >= stop || startY >= stopY) { + stop = 0; + return; + } + refreshLightCapabilities(); +} + + +bool Segment::setColor(uint8_t slot, uint32_t c) { //returns true if changed + if (slot >= NUM_COLORS || c == colors[slot]) return false; + if (!_isRGB && !_hasW) { + if (slot == 0 && c == BLACK) return false; // on/off segment cannot have primary color black + if (slot == 1 && c != BLACK) return false; // on/off segment cannot have secondary color non black + } + if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change + colors[slot] = c; + stateChanged = true; // send UDP/WS broadcast + return true; +} + +void Segment::setCCT(uint16_t k) { + if (k > 255) { //kelvin value, convert to 0-255 + if (k < 1900) k = 1900; + if (k > 10091) k = 10091; + k = (k - 1900) >> 5; + } + if (cct == k) return; + if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change + cct = k; + stateChanged = true; // send UDP/WS broadcast +} + +void Segment::setOpacity(uint8_t o) { + if (opacity == o) return; + if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change + opacity = o; + stateChanged = true; // send UDP/WS broadcast +} + +void Segment::setOption(uint8_t n, bool val) { + bool prevOn = on; + if (fadeTransition && n == SEG_OPTION_ON && val != prevOn) startTransition(strip.getTransition()); // start transition prior to change + if (val) options |= 0x01 << n; + else options &= ~(0x01 << n); + if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET)) stateChanged = true; // send UDP/WS broadcast +} + +void Segment::setMode(uint8_t fx, bool loadDefaults) { + // if we have a valid mode & is not reserved + if (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4)) { + if (fx != mode) { +#ifndef WLED_DISABLE_MODE_BLEND + if (modeBlending) startTransition(strip.getTransition()); // set effect transitions +#endif + mode = fx; + // load default values from effect string + if (loadDefaults) { + int16_t sOpt; + sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; + sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; + sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; + sOpt = extractModeDefaults(fx, "c2"); custom2 = (sOpt >= 0) ? sOpt : DEFAULT_C2; + sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? sOpt : DEFAULT_C3; + sOpt = extractModeDefaults(fx, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false; + sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false; + sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false; + sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) map1D2D = constrain(sOpt, 0, 7); else map1D2D = M12_Pixels; // reset mapping if not defined (2D FX may not work) + sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) soundSim = constrain(sOpt, 0, 1); + sOpt = extractModeDefaults(fx, "rev"); if (sOpt >= 0) reverse = (bool)sOpt; + sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business + sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; + sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business + sOpt = extractModeDefaults(fx, "pal"); if (sOpt >= 0) setPalette(sOpt); //else setPalette(0); + } + markForReset(); + stateChanged = true; // send UDP/WS broadcast + } + } +} + +void Segment::setPalette(uint8_t pal) { + if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes + if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // custom palettes + if (pal != palette) { + if (strip.paletteFade) startTransition(strip.getTransition()); + palette = pal; + stateChanged = true; // send UDP/WS broadcast + } +} + +// 2D matrix +uint16_t Segment::virtualWidth() const { + uint16_t groupLen = groupLength(); + uint16_t vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; + if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED + return vWidth; +} + +uint16_t Segment::virtualHeight() const { + uint16_t groupLen = groupLength(); + uint16_t vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; + if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED + return vHeight; +} + +uint16_t Segment::nrOfVStrips() const { + uint16_t vLen = 1; +#ifndef WLED_DISABLE_2D + if (is2D()) { + switch (map1D2D) { + case M12_pBar: + vLen = virtualWidth(); + break; + } + } +#endif + return vLen; +} + +// 1D strip +uint16_t Segment::virtualLength() const { +#ifndef WLED_DISABLE_2D + if (is2D()) { + uint16_t vW = virtualWidth(); + uint16_t vH = virtualHeight(); + uint16_t vLen = vW * vH; // use all pixels from segment + switch (map1D2D) { + case M12_pBar: + vLen = vH; + break; + case M12_pCorner: + case M12_pArc: + vLen = max(vW,vH); // get the longest dimension + break; + } + return vLen; + } +#endif + uint16_t groupLen = groupLength(); // is always >= 1 + uint16_t vLength = (length() + groupLen - 1) / groupLen; + if (mirror) vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED + return vLength; +} + +void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) +{ + if (!isActive()) return; // not active +#ifndef WLED_DISABLE_2D + int vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) +#endif + i &= 0xFFFF; + + if (i >= virtualLength() || i<0) return; // if pixel would fall out of segment just exit + +#ifndef WLED_DISABLE_2D + if (is2D()) { + uint16_t vH = virtualHeight(); // segment height in logical pixels + uint16_t vW = virtualWidth(); + switch (map1D2D) { + case M12_Pixels: + // use all available pixels as a long strip + setPixelColorXY(i % vW, i / vW, col); + break; + case M12_pBar: + // expand 1D effect vertically or have it play on virtual strips + if (vStrip>0) setPixelColorXY(vStrip - 1, vH - i - 1, col); + else for (int x = 0; x < vW; x++) setPixelColorXY(x, vH - i - 1, col); + break; + case M12_pArc: + // expand in circular fashion from center + if (i==0) + setPixelColorXY(0, 0, col); + else { + float step = HALF_PI / (2.85f*i); + for (float rad = 0.0f; rad <= HALF_PI+step/2; rad += step) { + // may want to try float version as well (with or without antialiasing) + int x = roundf(sin_t(rad) * i); + int y = roundf(cos_t(rad) * i); + setPixelColorXY(x, y, col); + } + // Bresenham’s Algorithm (may not fill every pixel) + //int d = 3 - (2*i); + //int y = i, x = 0; + //while (y >= x) { + // setPixelColorXY(x, y, col); + // setPixelColorXY(y, x, col); + // x++; + // if (d > 0) { + // y--; + // d += 4 * (x - y) + 10; + // } else { + // d += 4 * x + 6; + // } + //} + } + break; + case M12_pCorner: + for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); + for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); + break; + } + return; + } else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) { + if (start < Segment::maxWidth*Segment::maxHeight) { + // we have a vertical or horizontal 1D segment (WARNING: virtual...() may be transposed) + int x = 0, y = 0; + if (virtualHeight()>1) y = i; + if (virtualWidth() >1) x = i; + setPixelColorXY(x, y, col); + return; + } + } +#endif + + uint16_t len = length(); + uint8_t _bri_t = currentBri(); + if (_bri_t < 255) { + byte r = scale8(R(col), _bri_t); + byte g = scale8(G(col), _bri_t); + byte b = scale8(B(col), _bri_t); + byte w = scale8(W(col), _bri_t); + col = RGBW32(r, g, b, w); + } + + // expand pixel (taking into account start, grouping, spacing [and offset]) + i = i * groupLength(); + if (reverse) { // is segment reversed? + if (mirror) { // is segment mirrored? + i = (len - 1) / 2 - i; //only need to index half the pixels + } else { + i = (len - 1) - i; + } + } + i += start; // starting pixel in a group + + uint32_t tmpCol = col; + // set all the pixels in the group + for (int j = 0; j < grouping; j++) { + uint16_t indexSet = i + ((reverse) ? -j : j); + if (indexSet >= start && indexSet < stop) { + if (mirror) { //set the corresponding mirrored pixel + uint16_t indexMir = stop - indexSet + start - 1; + indexMir += offset; // offset/phase + if (indexMir >= stop) indexMir -= len; // wrap +#ifndef WLED_DISABLE_MODE_BLEND + if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexMir), col, 0xFFFFU - progress(), true); +#endif + strip.setPixelColor(indexMir, tmpCol); + } + indexSet += offset; // offset/phase + if (indexSet >= stop) indexSet -= len; // wrap +#ifndef WLED_DISABLE_MODE_BLEND + if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexSet), col, 0xFFFFU - progress(), true); +#endif + strip.setPixelColor(indexSet, tmpCol); + } + } +} + +// anti-aliased normalized version of setPixelColor() +void Segment::setPixelColor(float i, uint32_t col, bool aa) +{ + if (!isActive()) return; // not active + int vStrip = int(i/10.0f); // hack to allow running on virtual strips (2D segment columns/rows) + i -= int(i); + + if (i<0.0f || i>1.0f) return; // not normalized + + float fC = i * (virtualLength()-1); + if (aa) { + uint16_t iL = roundf(fC-0.49f); + uint16_t iR = roundf(fC+0.49f); + float dL = (fC - iL)*(fC - iL); + float dR = (iR - fC)*(iR - fC); + uint32_t cIL = getPixelColor(iL | (vStrip<<16)); + uint32_t cIR = getPixelColor(iR | (vStrip<<16)); + if (iR!=iL) { + // blend L pixel + cIL = color_blend(col, cIL, uint8_t(dL*255.0f)); + setPixelColor(iL | (vStrip<<16), cIL); + // blend R pixel + cIR = color_blend(col, cIR, uint8_t(dR*255.0f)); + setPixelColor(iR | (vStrip<<16), cIR); + } else { + // exact match (x & y land on a pixel) + setPixelColor(iL | (vStrip<<16), col); + } + } else { + setPixelColor(uint16_t(roundf(fC)) | (vStrip<<16), col); + } +} + +uint32_t Segment::getPixelColor(int i) +{ + if (!isActive()) return 0; // not active +#ifndef WLED_DISABLE_2D + int vStrip = i>>16; +#endif + i &= 0xFFFF; + +#ifndef WLED_DISABLE_2D + if (is2D()) { + uint16_t vH = virtualHeight(); // segment height in logical pixels + uint16_t vW = virtualWidth(); + switch (map1D2D) { + case M12_Pixels: + return getPixelColorXY(i % vW, i / vW); + break; + case M12_pBar: + if (vStrip>0) return getPixelColorXY(vStrip - 1, vH - i -1); + else return getPixelColorXY(0, vH - i -1); + break; + case M12_pArc: + case M12_pCorner: + // use longest dimension + return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); + break; + } + return 0; + } +#endif + + if (reverse) i = virtualLength() - i - 1; + i *= groupLength(); + i += start; + /* offset/phase */ + i += offset; + if ((i >= stop) && (stop>0)) i -= length(); // avoids negative pixel index (stop = 0 is a possible value) + return strip.getPixelColor(i); +} + +uint8_t Segment::differs(Segment& b) const { + uint8_t d = 0; + if (start != b.start) d |= SEG_DIFFERS_BOUNDS; + if (stop != b.stop) d |= SEG_DIFFERS_BOUNDS; + if (offset != b.offset) d |= SEG_DIFFERS_GSO; + if (grouping != b.grouping) d |= SEG_DIFFERS_GSO; + if (spacing != b.spacing) d |= SEG_DIFFERS_GSO; + if (opacity != b.opacity) d |= SEG_DIFFERS_BRI; + if (mode != b.mode) d |= SEG_DIFFERS_FX; + if (speed != b.speed) d |= SEG_DIFFERS_FX; + if (intensity != b.intensity) d |= SEG_DIFFERS_FX; + if (palette != b.palette) d |= SEG_DIFFERS_FX; + if (custom1 != b.custom1) d |= SEG_DIFFERS_FX; + if (custom2 != b.custom2) d |= SEG_DIFFERS_FX; + if (custom3 != b.custom3) d |= SEG_DIFFERS_FX; + if (startY != b.startY) d |= SEG_DIFFERS_BOUNDS; + if (stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; + + //bit pattern: (msb first) + // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected] + if ((options & 0b1111111111011110U) != (b.options & 0b1111111111011110U)) d |= SEG_DIFFERS_OPT; + if ((options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; + for (unsigned i = 0; i < NUM_COLORS; i++) if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; + + return d; +} + +void Segment::refreshLightCapabilities() { + uint8_t capabilities = 0; + uint16_t segStartIdx = 0xFFFFU; + uint16_t segStopIdx = 0; + + if (!isActive()) { + _capabilities = 0; + return; + } + + if (start < Segment::maxWidth * Segment::maxHeight) { + // we are withing 2D matrix (includes 1D segments) + for (int y = startY; y < stopY; y++) for (int x = start; x < stop; x++) { + uint16_t index = x + Segment::maxWidth * y; + if (index < strip.customMappingSize) index = strip.customMappingTable[index]; // convert logical address to physical + if (index < 0xFFFFU) { + if (segStartIdx > index) segStartIdx = index; + if (segStopIdx < index) segStopIdx = index; + } + if (segStartIdx == segStopIdx) segStopIdx++; // we only have 1 pixel segment + } + } else { + // we are on the strip located after the matrix + segStartIdx = start; + segStopIdx = stop; + } + + for (unsigned b = 0; b < busses.getNumBusses(); b++) { + Bus *bus = busses.getBus(b); + if (bus == nullptr || bus->getLength()==0) break; + if (!bus->isOk()) continue; + if (bus->getStart() >= segStopIdx) continue; + if (bus->getStart() + bus->getLength() <= segStartIdx) continue; + + //uint8_t type = bus->getType(); + if (bus->hasRGB() || (cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; + if (!cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; + if (correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) + if (bus->hasWhite()) { + uint8_t aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); + bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed + // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses + if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; + // if auto white calculation from RGB is disabled/optional (None/Dual), allow white channel adjustments + if ( whiteSlider) capabilities |= SEG_CAPABILITY_W; + } + } + _capabilities = capabilities; +} + +/* + * Fills segment with color + */ +void Segment::fill(uint32_t c) { + if (!isActive()) return; // not active + const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); + const uint16_t rows = virtualHeight(); // will be 1 for 1D + for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { + if (is2D()) setPixelColorXY(x, y, c); + else setPixelColor(x, c); + } +} + +// Blends the specified color with the existing pixel color. +void Segment::blendPixelColor(int n, uint32_t color, uint8_t blend) { + setPixelColor(n, color_blend(getPixelColor(n), color, blend)); +} + +// Adds the specified color with the existing pixel color perserving color balance. +void Segment::addPixelColor(int n, uint32_t color, bool fast) { + if (!isActive()) return; // not active + setPixelColor(n, color_add(getPixelColor(n), color, fast)); +} + +void Segment::fadePixelColor(uint16_t n, uint8_t fade) { + if (!isActive()) return; // not active + setPixelColor(n, color_fade(getPixelColor(n), fade, true)); +} + +/* + * fade out function, higher rate = quicker fade + */ +void Segment::fade_out(uint8_t rate) { + if (!isActive()) return; // not active + const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); + const uint16_t rows = virtualHeight(); // will be 1 for 1D + + rate = (255-rate) >> 1; + float mappedRate = float(rate) +1.1f; + + uint32_t color = colors[1]; // SEGCOLOR(1); // target color + int w2 = W(color); + int r2 = R(color); + int g2 = G(color); + int b2 = B(color); + + for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { + color = is2D() ? getPixelColorXY(x, y) : getPixelColor(x); + int w1 = W(color); + int r1 = R(color); + int g1 = G(color); + int b1 = B(color); + + int wdelta = (w2 - w1) / mappedRate; + int rdelta = (r2 - r1) / mappedRate; + int gdelta = (g2 - g1) / mappedRate; + int bdelta = (b2 - b1) / mappedRate; + + // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) + wdelta += (w2 == w1) ? 0 : (w2 > w1) ? 1 : -1; + rdelta += (r2 == r1) ? 0 : (r2 > r1) ? 1 : -1; + gdelta += (g2 == g1) ? 0 : (g2 > g1) ? 1 : -1; + bdelta += (b2 == b1) ? 0 : (b2 > b1) ? 1 : -1; + + if (is2D()) setPixelColorXY(x, y, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); + else setPixelColor(x, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); + } +} + +// fades all pixels to black using nscale8() +void Segment::fadeToBlackBy(uint8_t fadeBy) { + if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply + const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); + const uint16_t rows = virtualHeight(); // will be 1 for 1D + + for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { + if (is2D()) setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), 255-fadeBy)); + else setPixelColor(x, color_fade(getPixelColor(x), 255-fadeBy)); + } +} + +/* + * blurs segment content, source: FastLED colorutils.cpp + */ +void Segment::blur(uint8_t blur_amount) +{ + if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" +#ifndef WLED_DISABLE_2D + if (is2D()) { + // compatibility with 2D + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); + for (unsigned i = 0; i < rows; i++) blurRow(i, blur_amount); // blur all rows + for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount); // blur all columns + return; + } +#endif + uint8_t keep = 255 - blur_amount; + uint8_t seep = blur_amount >> 1; + uint32_t carryover = BLACK; + unsigned vlength = virtualLength(); + for (unsigned i = 0; i < vlength; i++) { + uint32_t cur = getPixelColor(i); + uint32_t part = color_fade(cur, seep); + cur = color_add(color_fade(cur, keep), carryover, true); + if (i > 0) { + uint32_t c = getPixelColor(i-1); + setPixelColor(i-1, color_add(c, part, true)); + } + setPixelColor(i, cur); + carryover = part; + } +} + +/* + * Put a value 0 to 255 in to get a color value. + * The colours are a transition r -> g -> b -> back to r + * Inspired by the Adafruit examples. + */ +uint32_t Segment::color_wheel(uint8_t pos) { + if (palette) return color_from_palette(pos, false, true, 0); + pos = 255 - pos; + if (pos < 85) { + return ((uint32_t)(255 - pos * 3) << 16) | ((uint32_t)(0) << 8) | (pos * 3); + } else if(pos < 170) { + pos -= 85; + return ((uint32_t)(0) << 16) | ((uint32_t)(pos * 3) << 8) | (255 - pos * 3); + } else { + pos -= 170; + return ((uint32_t)(pos * 3) << 16) | ((uint32_t)(255 - pos * 3) << 8) | (0); + } +} + +/* + * Gets a single color from the currently selected palette. + * @param i Palette Index (if mapping is true, the full palette will be _virtualSegmentLength long, if false, 255). Will wrap around automatically. + * @param mapping if true, LED position in segment is considered for color + * @param wrap FastLED palettes will usually wrap back to the start smoothly. Set false to get a hard edge + * @param mcol If the default palette 0 is selected, return the standard color 0, 1 or 2 instead. If >2, Party palette is used instead + * @param pbri Value to scale the brightness of the returned color by. Default is 255. (no scaling) + * @returns Single color from palette + */ +uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) +{ + // default palette or no RGB support on segment + if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) { + uint32_t color = currentColor(mcol); + color = gamma32(color); + if (pbri == 255) return color; + return color_fade(color, pbri, true); + } + + uint8_t paletteIndex = i; + if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1); + if (!wrap && strip.paletteBlend != 3) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" + CRGB fastled_col = ColorFromPalette(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global + + return RGBW32(fastled_col.r, fastled_col.g, fastled_col.b, 0); +} + + +/////////////////////////////////////////////////////////////////////////////// +// WS2812FX class implementation +/////////////////////////////////////////////////////////////////////////////// + //do not call this method from system context (network callback) void WS2812FX::finalizeInit(void) { //reset segment runtimes - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) { - _segment_runtimes[i].markForReset(); - _segment_runtimes[i].resetIfRequired(); + for (segment &seg : _segments) { + seg.markForReset(); + seg.resetIfRequired(); } + // for the lack of better place enumerate ledmaps here + // if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs + // unfortunately this means we do not get updates after uploads + enumerateLedmaps(); + _hasWhiteChannel = _isOffRefreshRequired = false; //if busses failed to load, add default (fresh install, FS issue, ...) if (busses.getNumBusses() == 0) { + DEBUG_PRINTLN(F("No busses, init default")); const uint8_t defDataPins[] = {DATA_PINS}; const uint16_t defCounts[] = {PIXEL_COUNTS}; const uint8_t defNumBusses = ((sizeof defDataPins) / (sizeof defDataPins[0])); const uint8_t defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); uint16_t prevLen = 0; - for (uint8_t i = 0; i < defNumBusses && i < WLED_MAX_BUSSES; i++) { + for (int i = 0; i < defNumBusses && i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { uint8_t defPin[] = {defDataPins[i]}; uint16_t start = prevLen; uint16_t count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; prevLen += count; - BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, COL_ORDER_GRB); - busses.add(defCfg); + BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY); + if (busses.add(defCfg) == -1) break; } } _length = 0; - for (uint8_t i=0; igetStart() + bus->getLength() > MAX_LEDS) break; //RGBW mode is enabled if at least one of the strips is RGBW - _hasWhiteChannel |= bus->isRgbw(); + _hasWhiteChannel |= bus->hasWhite(); //refresh is required to remain off if at least one of the strips requires the refresh. _isOffRefreshRequired |= bus->isOffRefreshRequired(); uint16_t busEnd = bus->getStart() + bus->getLength(); @@ -112,135 +1142,110 @@ void WS2812FX::finalizeInit(void) #endif } - //segments are created in makeAutoSegments(); + if (isMatrix) setUpMatrix(); + else { + Segment::maxWidth = _length; + Segment::maxHeight = 1; + } - setBrightness(_brightness); + //segments are created in makeAutoSegments(); + DEBUG_PRINTLN(F("Loading custom palettes")); + loadCustomPalettes(); // (re)load all custom palettes + DEBUG_PRINTLN(F("Loading custom ledmaps")); + deserializeMap(); // (re)load default ledmap } void WS2812FX::service() { - uint32_t nowUp = millis(); // Be aware, millis() rolls over every 49 days + unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days now = nowUp + timebase; if (nowUp - _lastShow < MIN_SHOW_DELAY) return; bool doShow = false; - for(uint8_t i=0; i < MAX_NUM_SEGMENTS; i++) - { - _segment_index = i; - - // reset the segment runtime data if needed, called before isActive to ensure deleted - // segment's buffers are cleared - SEGENV.resetIfRequired(); + _isServicing = true; + _segment_index = 0; + Segment::handleRandomPalette(); // move it into for loop when each segment has individual random palette + for (segment &seg : _segments) { + // process transition (mode changes in the middle of transition) + seg.handleTransition(); + // reset the segment runtime data if needed + seg.resetIfRequired(); - if (!SEGMENT.isActive()) continue; + if (!seg.isActive()) continue; // last condition ensures all solid segments are updated at the same time - if(nowUp > SEGENV.next_time || _triggered || (doShow && SEGMENT.mode == 0)) + if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { - if (SEGMENT.grouping == 0) SEGMENT.grouping = 1; //sanity check doShow = true; uint16_t delay = FRAMETIME; - if (!SEGMENT.getOption(SEG_OPTION_FREEZE)) { //only run effect function if not frozen - _virtualSegmentLength = SEGMENT.virtualLength(); - _bri_t = SEGMENT.opacity; _colors_t[0] = SEGMENT.colors[0]; _colors_t[1] = SEGMENT.colors[1]; _colors_t[2] = SEGMENT.colors[2]; - uint8_t _cct_t = SEGMENT.cct; - if (!IS_SEGMENT_ON) _bri_t = 0; - for (uint8_t t = 0; t < MAX_NUM_TRANSITIONS; t++) { - if ((transitions[t].segment & 0x3F) != i) continue; - uint8_t slot = transitions[t].segment >> 6; - if (slot == 0) _bri_t = transitions[t].currentBri(); - if (slot == 1) _cct_t = transitions[t].currentBri(false, 1); - _colors_t[slot] = transitions[t].currentColor(SEGMENT.colors[slot]); + if (!seg.freeze) { //only run effect function if not frozen + _virtualSegmentLength = seg.virtualLength(); + _colors_t[0] = seg.currentColor(0); + _colors_t[1] = seg.currentColor(1); + _colors_t[2] = seg.currentColor(2); + seg.setCurrentPalette(); // load actual palette + + if (!cctFromRgb || correctWB) busses.setSegmentCCT(seg.currentBri(true), correctWB); + for (int c = 0; c < NUM_COLORS; c++) _colors_t[c] = gamma32(_colors_t[c]); + + // Effect blending + // When two effects are being blended, each may have different segment data, this + // data needs to be saved first and then restored before running previous mode. + // The blending will largely depend on the effect behaviour since actual output (LEDs) may be + // overwritten by later effect. To enable seamless blending for every effect, additional LED buffer + // would need to be allocated for each effect and then blended together for each pixel. + [[maybe_unused]] uint8_t tmpMode = seg.currentMode(); // this will return old mode while in transition + delay = (*_mode[seg.mode])(); // run new/current mode +#ifndef WLED_DISABLE_MODE_BLEND + if (modeBlending && seg.mode != tmpMode) { + Segment::tmpsegd_t _tmpSegData; + Segment::modeBlend(true); // set semaphore + seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state) + _virtualSegmentLength = seg.virtualLength(); // update SEGLEN (mapping may have changed) + uint16_t d2 = (*_mode[tmpMode])(); // run old mode + seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state) + delay = MIN(delay,d2); // use shortest delay + Segment::modeBlend(false); // unset semaphore } - if (!cctFromRgb || correctWB) busses.setSegmentCCT(_cct_t, correctWB); - for (uint8_t c = 0; c < NUM_COLORS; c++) { - _colors_t[c] = gamma32(_colors_t[c]); - } - handle_palette(); - - // if segment is not RGB capable, force None auto white mode - // If not RGB capable, also treat palette as if default (0), as palettes set white channel to 0 - _no_rgb = !(SEGMENT.getLightCapabilities() & 0x01); - if (_no_rgb) Bus::setAutoWhiteMode(RGBW_MODE_MANUAL_ONLY); - delay = (this->*_mode[SEGMENT.mode])(); //effect function - if (SEGMENT.mode != FX_MODE_HALLOWEEN_EYES) SEGENV.call++; - Bus::setAutoWhiteMode(strip.autoWhiteMode); +#endif + if (seg.mode != FX_MODE_HALLOWEEN_EYES) seg.call++; + if (seg.isInTransition() && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition } - SEGENV.next_time = nowUp + delay; + seg.next_time = nowUp + delay; } + if (_segment_index == _queuedChangesSegId) setUpSegmentFromQueuedChanges(); + _segment_index++; } _virtualSegmentLength = 0; busses.setSegmentCCT(-1); - if(doShow) { + _isServicing = false; + _triggered = false; + + #ifdef WLED_DEBUG + if (millis() - nowUp > _frametime) DEBUG_PRINTLN(F("Slow effects.")); + #endif + if (doShow) { yield(); show(); } - _triggered = false; -} - -void IRAM_ATTR WS2812FX::setPixelColor(uint16_t n, uint32_t c) { - setPixelColor(n, R(c), G(c), B(c), W(c)); + #ifdef WLED_DEBUG + if (millis() - nowUp > _frametime) DEBUG_PRINTLN(F("Slow strip.")); + #endif } -//used to map from segment index to physical pixel, taking into account grouping, offsets, reverse and mirroring -uint16_t IRAM_ATTR WS2812FX::realPixelIndex(uint16_t i) { - int16_t iGroup = i * SEGMENT.groupLength(); - - /* reverse just an individual segment */ - int16_t realIndex = iGroup; - if (IS_REVERSE) { - if (IS_MIRROR) { - realIndex = (SEGMENT.length() - 1) / 2 - iGroup; //only need to index half the pixels - } else { - realIndex = (SEGMENT.length() - 1) - iGroup; - } - } - - realIndex += SEGMENT.start; - return realIndex; +void IRAM_ATTR WS2812FX::setPixelColor(int i, uint32_t col) +{ + if (i < customMappingSize) i = customMappingTable[i]; + if (i >= _length) return; + busses.setPixelColor(i, col); } -void IRAM_ATTR WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) +uint32_t WS2812FX::getPixelColor(uint16_t i) { - if (SEGLEN) {//from segment - uint16_t realIndex = realPixelIndex(i); - uint16_t len = SEGMENT.length(); - - //color_blend(getpixel, col, _bri_t); (pseudocode for future blending of segments) - if (_bri_t < 255) { - r = scale8(r, _bri_t); - g = scale8(g, _bri_t); - b = scale8(b, _bri_t); - w = scale8(w, _bri_t); - } - uint32_t col = RGBW32(r, g, b, w); - - /* Set all the pixels in the group */ - for (uint16_t j = 0; j < SEGMENT.grouping; j++) { - uint16_t indexSet = realIndex + (IS_REVERSE ? -j : j); - if (indexSet >= SEGMENT.start && indexSet < SEGMENT.stop) { - if (IS_MIRROR) { //set the corresponding mirrored pixel - uint16_t indexMir = SEGMENT.stop - indexSet + SEGMENT.start - 1; - /* offset/phase */ - indexMir += SEGMENT.offset; - if (indexMir >= SEGMENT.stop) indexMir -= len; - - if (indexMir < customMappingSize) indexMir = customMappingTable[indexMir]; - busses.setPixelColor(indexMir, col); - } - /* offset/phase */ - indexSet += SEGMENT.offset; - if (indexSet >= SEGMENT.stop) indexSet -= len; - - if (indexSet < customMappingSize) indexSet = customMappingTable[indexSet]; - busses.setPixelColor(indexSet, col); - } - } - } else { //live data, etc. - if (i < customMappingSize) i = customMappingTable[i]; - busses.setPixelColor(i, RGBW32(r, g, b, w)); - } + if (i < customMappingSize) i = customMappingTable[i]; + if (i >= _length) return 0; + return busses.getPixelColor(i); } @@ -251,11 +1256,11 @@ void IRAM_ATTR WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte //Stay safe with high amperage and have a reasonable safety margin! //I am NOT to be held liable for burned down garages! -//fine tune power estimation constants for your setup +//fine tune power estimation constants for your setup #define MA_FOR_ESP 100 //how much mA does the ESP use (Wemos D1 about 80mA, ESP32 about 120mA) //you can set it to 0 if the ESP is powered by USB and the LEDs by external -void WS2812FX::estimateCurrentAndLimitBri() { +uint8_t WS2812FX::estimateCurrentAndLimitBri() { //power limit calculation //each LED can draw up 195075 "power units" (approx. 53mA) //one PU is the power it takes to have 1 channel 1 step brighter per brightness step @@ -263,35 +1268,28 @@ void WS2812FX::estimateCurrentAndLimitBri() { bool useWackyWS2815PowerModel = false; byte actualMilliampsPerLed = milliampsPerLed; - if(milliampsPerLed == 255) { - useWackyWS2815PowerModel = true; - actualMilliampsPerLed = 12; // from testing an actual strip - } - if (ablMilliampsMax < 150 || actualMilliampsPerLed == 0) { //0 mA per LED and too low numbers turn off calculation currentMilliamps = 0; - busses.setBrightness(_brightness); - return; + return _brightness; } - uint16_t pLen = getLengthPhysical(); - uint32_t puPerMilliamp = 195075 / actualMilliampsPerLed; - uint32_t powerBudget = (ablMilliampsMax - MA_FOR_ESP) * puPerMilliamp; //100mA for ESP power - if (powerBudget > puPerMilliamp * pLen) { //each LED uses about 1mA in standby, exclude that from power budget - powerBudget -= puPerMilliamp * pLen; - } else { - powerBudget = 0; + if (milliampsPerLed == 255) { + useWackyWS2815PowerModel = true; + actualMilliampsPerLed = 12; // from testing an actual strip } - uint32_t powerSum = 0; + size_t powerBudget = (ablMilliampsMax - MA_FOR_ESP); //100mA for ESP power - for (uint8_t b = 0; b < busses.getNumBusses(); b++) { - Bus *bus = busses.getBus(b); - if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses + size_t pLen = 0; //getLengthPhysical(); + size_t powerSum = 0; + for (uint_fast8_t bNum = 0; bNum < busses.getNumBusses(); bNum++) { + Bus *bus = busses.getBus(bNum); + if (!IS_DIGITAL(bus->getType())) continue; //exclude non-digital network busses uint16_t len = bus->getLength(); + pLen += len; uint32_t busPowerSum = 0; - for (uint16_t i = 0; i < len; i++) { //sum up the usage of each LED - uint32_t c = bus->getPixelColor(i); + for (uint_fast16_t i = 0; i < len; i++) { //sum up the usage of each LED + uint32_t c = bus->getPixelColor(i); // always returns original or restored color without brightness scaling byte r = R(c), g = G(c), b = B(c), w = W(c); if(useWackyWS2815PowerModel) { //ignore white component on WS2815 power calculation @@ -301,50 +1299,59 @@ void WS2812FX::estimateCurrentAndLimitBri() { } } - if (bus->isRgbw()) { //RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less + if (bus->hasWhite()) { //RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less busPowerSum *= 3; - busPowerSum = busPowerSum >> 2; //same as /= 4 + busPowerSum >>= 2; //same as /= 4 } powerSum += busPowerSum; } - uint32_t powerSum0 = powerSum; - powerSum *= _brightness; - - if (powerSum > powerBudget) //scale brightness down to stay in current limit - { - float scale = (float)powerBudget / (float)powerSum; + if (powerBudget > pLen) { //each LED uses about 1mA in standby, exclude that from power budget + powerBudget -= pLen; + } else { + powerBudget = 0; + } + + // powerSum has all the values of channels summed (max would be pLen*765 as white is excluded) so convert to milliAmps + powerSum = (powerSum * actualMilliampsPerLed) / 765; + + uint8_t newBri = _brightness; + if (powerSum * _brightness / 255 > powerBudget) { //scale brightness down to stay in current limit + float scale = (float)(powerBudget * 255) / (float)(powerSum * _brightness); uint16_t scaleI = scale * 255; uint8_t scaleB = (scaleI > 255) ? 255 : scaleI; - uint8_t newBri = scale8(_brightness, scaleB); - busses.setBrightness(newBri); //to keep brightness uniform, sets virtual busses too - currentMilliamps = (powerSum0 * newBri) / puPerMilliamp; - } else { - currentMilliamps = powerSum / puPerMilliamp; - busses.setBrightness(_brightness); + newBri = scale8(_brightness, scaleB) + 1; } + currentMilliamps = (powerSum * newBri) / 255; currentMilliamps += MA_FOR_ESP; //add power of ESP back to estimate - currentMilliamps += pLen; //add standby power back to estimate + currentMilliamps += pLen; //add standby power (1mA/LED) back to estimate + return newBri; } void WS2812FX::show(void) { - - // avoid race condition, caputre _callback value + // avoid race condition, capture _callback value show_callback callback = _callback; if (callback) callback(); - estimateCurrentAndLimitBri(); - + uint8_t newBri = estimateCurrentAndLimitBri(); + busses.setBrightness(newBri); // "repaints" all pixels if brightness changed + // some buses send asynchronously and this method will return before // all of the data has been sent. // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods busses.show(); - unsigned long now = millis(); - unsigned long diff = now - _lastShow; - uint16_t fpsCurr = 200; + + // restore bus brightness to its original value + // this is done right after show, so this is only OK if LED updates are completed before show() returns + // or async show has a separate buffer (ESP32 RMT and I2S are ok) + if (newBri < _brightness) busses.setBrightness(_brightness); + + unsigned long showNow = millis(); + size_t diff = showNow - _lastShow; + size_t fpsCurr = 200; if (diff > 0) fpsCurr = 1000 / diff; - _cumulativeFps = (3 * _cumulativeFps + fpsCurr) >> 2; - _lastShow = now; + _cumulativeFps = (3 * _cumulativeFps + fpsCurr +2) >> 2; // "+2" for proper rounding (2/4 = 0.5) + _lastShow = showNow; } /** @@ -357,135 +1364,96 @@ bool WS2812FX::isUpdating() { /** * Returns the refresh rate of the LED strip. Useful for finding out whether a given setup is fast enough. - * Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accurary varies + * Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accuracy varies */ uint16_t WS2812FX::getFps() { if (millis() - _lastShow > 2000) return 0; return _cumulativeFps +1; } -uint8_t WS2812FX::getTargetFps() { - return _targetFps; -} - void WS2812FX::setTargetFps(uint8_t fps) { - if (fps > 0 && fps <= 120) _targetFps = fps; - _frametime = 1000 / _targetFps; -} - -/** - * Forces the next frame to be computed on all active segments. - */ -void WS2812FX::trigger() { - _triggered = true; + if (fps > 0 && fps <= 120) _targetFps = fps; + _frametime = 1000 / _targetFps; } void WS2812FX::setMode(uint8_t segid, uint8_t m) { - if (segid >= MAX_NUM_SEGMENTS) return; - - if (m >= MODE_COUNT) m = MODE_COUNT - 1; + if (segid >= _segments.size()) return; - if (_segments[segid].mode != m) - { - _segment_runtimes[segid].markForReset(); - _segments[segid].mode = m; - } -} - -uint8_t WS2812FX::getModeCount() -{ - return MODE_COUNT; -} + if (m >= getModeCount()) m = getModeCount() - 1; -uint8_t WS2812FX::getPaletteCount() -{ - return 13 + GRADIENT_PALETTE_COUNT; -} - -void WS2812FX::setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w) { - setColor(slot, RGBW32(r, g, b, w)); + if (_segments[segid].mode != m) { + _segments[segid].setMode(m); // do not load defaults + } } //applies to all active and selected segments void WS2812FX::setColor(uint8_t slot, uint32_t c) { if (slot >= NUM_COLORS) return; - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].isActive() && _segments[i].isSelected()) { - _segments[i].setColor(slot, c, i); + for (segment &seg : _segments) { + if (seg.isActive() && seg.isSelected()) { + seg.setColor(slot, c); } } } void WS2812FX::setCCT(uint16_t k) { - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].isActive() && _segments[i].isSelected()) { - _segments[i].setCCT(k, i); + for (segment &seg : _segments) { + if (seg.isActive() && seg.isSelected()) { + seg.setCCT(k); } } } +// direct=true either expects the caller to call show() themselves (realtime modes) or be ok waiting for the next frame for the change to apply +// direct=false immediately triggers an effect redraw void WS2812FX::setBrightness(uint8_t b, bool direct) { if (gammaCorrectBri) b = gamma8(b); if (_brightness == b) return; _brightness = b; if (_brightness == 0) { //unfreeze all segments on power off - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - _segments[i].setOption(SEG_OPTION_FREEZE, false); + for (segment &seg : _segments) { + seg.freeze = false; } } - if (direct) { - // would be dangerous if applied immediately (could exceed ABL), but will not output until the next show() - busses.setBrightness(b); - } else { - unsigned long t = millis(); - if (_segment_runtimes[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) show(); //apply brightness change immediately if no refresh soon + // setting brightness with NeoPixelBusLg has no effect on already painted pixels, + // so we need to force an update to existing buffer + busses.setBrightness(b); + if (!direct) { + unsigned long t = millis(); + if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon } } -uint8_t WS2812FX::getBrightness(void) { - return _brightness; -} - -uint8_t WS2812FX::getMaxSegments(void) { - return MAX_NUM_SEGMENTS; +uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) { + uint8_t totalLC = 0; + for (segment &seg : _segments) { + if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities(); + } + return totalLC; } uint8_t WS2812FX::getFirstSelectedSegId(void) { - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].isActive() && _segments[i].isSelected()) return i; + size_t i = 0; + for (segment &seg : _segments) { + if (seg.isActive() && seg.isSelected()) return i; + i++; } // if none selected, use the main segment return getMainSegmentId(); } void WS2812FX::setMainSegmentId(uint8_t n) { - if (n >= MAX_NUM_SEGMENTS) return; - //use supplied n if active, or first active - if (_segments[n].isActive()) { - _mainSegment = n; return; - } - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].isActive()) { - _mainSegment = i; return; - } - } _mainSegment = 0; + if (n < _segments.size()) { + _mainSegment = n; + } return; } -uint8_t WS2812FX::getMainSegmentId(void) { - return _mainSegment; -} - uint8_t WS2812FX::getLastActiveSegmentId(void) { - for (uint8_t i = MAX_NUM_SEGMENTS -1; i > 0; i--) { + for (size_t i = _segments.size() -1; i > 0; i--) { if (_segments[i].isActive()) return i; } return 0; @@ -493,145 +1461,43 @@ uint8_t WS2812FX::getLastActiveSegmentId(void) { uint8_t WS2812FX::getActiveSegmentsNum(void) { uint8_t c = 0; - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { + for (size_t i = 0; i < _segments.size(); i++) { if (_segments[i].isActive()) c++; - } - return c; -} - -uint32_t WS2812FX::getPixelColor(uint16_t i) -{ - i = realPixelIndex(i); - - if (SEGLEN) { - /* offset/phase */ - i += SEGMENT.offset; - if (i >= SEGMENT.stop) i -= SEGMENT.length(); - } - - if (i < customMappingSize) i = customMappingTable[i]; - if (i >= _length) return 0; - - return busses.getPixelColor(i); -} - -WS2812FX::Segment& WS2812FX::getSegment(uint8_t id) { - if (id >= MAX_NUM_SEGMENTS) return _segments[0]; - return _segments[id]; -} - -WS2812FX::Segment& WS2812FX::getFirstSelectedSeg(void) { - return _segments[getFirstSelectedSegId()]; -} - -WS2812FX::Segment& WS2812FX::getMainSegment(void) { - return _segments[getMainSegmentId()]; -} - -WS2812FX::Segment* WS2812FX::getSegments(void) { - return _segments; -} - -uint32_t WS2812FX::getLastShow(void) { - return _lastShow; -} - -uint16_t WS2812FX::getLengthTotal(void) { - return _length; -} - -uint16_t WS2812FX::getLengthPhysical(void) { - uint16_t len = 0; - for (uint8_t b = 0; b < busses.getNumBusses(); b++) { - Bus *bus = busses.getBus(b); - if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses - len += bus->getLength(); - } - return len; -} - -uint8_t WS2812FX::Segment::differs(Segment& b) { - uint8_t d = 0; - if (start != b.start) d |= SEG_DIFFERS_BOUNDS; - if (stop != b.stop) d |= SEG_DIFFERS_BOUNDS; - if (offset != b.offset) d |= SEG_DIFFERS_GSO; - if (grouping != b.grouping) d |= SEG_DIFFERS_GSO; - if (spacing != b.spacing) d |= SEG_DIFFERS_GSO; - if (opacity != b.opacity) d |= SEG_DIFFERS_BRI; - if (mode != b.mode) d |= SEG_DIFFERS_FX; - if (speed != b.speed) d |= SEG_DIFFERS_FX; - if (intensity != b.intensity) d |= SEG_DIFFERS_FX; - if (palette != b.palette) d |= SEG_DIFFERS_FX; - - if ((options & 0b00101110) != (b.options & 0b00101110)) d |= SEG_DIFFERS_OPT; - if ((options & 0x01) != (b.options & 0x01)) d |= SEG_DIFFERS_SEL; - - for (uint8_t i = 0; i < NUM_COLORS; i++) - { - if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; - } - - return d; + } + return c; } -void WS2812FX::Segment::refreshLightCapabilities() { - if (!isActive()) { - _capabilities = 0; return; - } - uint8_t capabilities = 0; - uint8_t awm = instance->autoWhiteMode; - bool whiteSlider = (awm == RGBW_MODE_DUAL || awm == RGBW_MODE_MANUAL_ONLY); - bool segHasValidBus = false; +uint16_t WS2812FX::getLengthTotal(void) { + uint16_t len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D + if (isMatrix && _length > len) len = _length; // for 2D with trailing strip + return len; +} - for (uint8_t b = 0; b < busses.getNumBusses(); b++) { +uint16_t WS2812FX::getLengthPhysical(void) { + uint16_t len = 0; + for (size_t b = 0; b < busses.getNumBusses(); b++) { Bus *bus = busses.getBus(b); - if (bus == nullptr || bus->getLength()==0) break; - if (bus->getStart() >= stop) continue; - if (bus->getStart() + bus->getLength() <= start) continue; - - segHasValidBus = true; - uint8_t type = bus->getType(); - if (type != TYPE_ANALOG_1CH && (cctFromRgb || type != TYPE_ANALOG_2CH)) - { - capabilities |= 0x01; // segment supports RGB (full color) - } - if (bus->isRgbw() && whiteSlider) capabilities |= 0x02; // segment supports white channel - if (!cctFromRgb) { - switch (type) { - case TYPE_ANALOG_5CH: - case TYPE_ANALOG_2CH: - capabilities |= 0x04; //segment supports white CCT - } - } - if (correctWB && type != TYPE_ANALOG_1CH) capabilities |= 0x04; //white balance correction (uses CCT slider) + if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses + len += bus->getLength(); } - // if seg has any bus, but no bus has RGB, it by definition supports white (at least for now) - // In case of no RGB, disregard auto white mode and always show a white slider - if (segHasValidBus && !(capabilities & 0x01)) capabilities |= 0x02; // segment supports white channel - _capabilities = capabilities; + return len; } //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. //returns if there is an RGBW bus (supports RGB and White, not only white) //not influenced by auto-white mode, also true if white slider does not affect output white channel bool WS2812FX::hasRGBWBus(void) { - for (uint8_t b = 0; b < busses.getNumBusses(); b++) { + for (size_t b = 0; b < busses.getNumBusses(); b++) { Bus *bus = busses.getBus(b); if (bus == nullptr || bus->getLength()==0) break; - switch (bus->getType()) { - case TYPE_SK6812_RGBW: - case TYPE_TM1814: - case TYPE_ANALOG_4CH: - return true; - } + if (bus->hasRGB() && bus->hasWhite()) return true; } - return false; + return false; } bool WS2812FX::hasCCTBus(void) { - if (cctFromRgb && !correctWB) return false; - for (uint8_t b = 0; b < busses.getNumBusses(); b++) { + if (cctFromRgb && !correctWB) return false; + for (size_t b = 0; b < busses.getNumBusses(); b++) { Bus *bus = busses.getBus(b); if (bus == nullptr || bus->getLength()==0) break; switch (bus->getType()) { @@ -640,94 +1506,101 @@ bool WS2812FX::hasCCTBus(void) { return true; } } - return false; + return false; } -void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset) { - if (n >= MAX_NUM_SEGMENTS) return; - Segment& seg = _segments[n]; - - //return if neither bounds nor grouping have changed - bool boundsUnchanged = (seg.start == i1 && seg.stop == i2); - if (boundsUnchanged - && (!grouping || (seg.grouping == grouping && seg.spacing == spacing)) - && (offset == UINT16_MAX || offset == seg.offset)) return; - - if (seg.stop) setRange(seg.start, seg.stop -1, 0); //turn old segment range off - if (i2 <= i1) //disable segment - { - seg.stop = 0; - if (seg.name) { - delete[] seg.name; - seg.name = nullptr; +void WS2812FX::purgeSegments(bool force) { + // remove all inactive segments (from the back) + int deleted = 0; + if (_segments.size() <= 1) return; + for (size_t i = _segments.size()-1; i > 0; i--) + if (_segments[i].stop == 0 || force) { + deleted++; + _segments.erase(_segments.begin() + i); } - // if main segment is deleted, set first active as main segment - if (n == _mainSegment) setMainSegmentId(0); - return; + if (deleted) { + _segments.shrink_to_fit(); + /*if (_mainSegment >= _segments.size())*/ setMainSegmentId(0); + } +} + +Segment& WS2812FX::getSegment(uint8_t id) { + return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors +} + +// sets new segment bounds, queues if that segment is currently running +void WS2812FX::setSegment(uint8_t segId, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset, uint16_t startY, uint16_t stopY) { + if (segId >= getSegmentsNum()) { + if (i2 <= i1) return; // do not append empty/inactive segments + appendSegment(Segment(0, strip.getLengthTotal())); + segId = getSegmentsNum()-1; // segments are added at the end of list } - if (i1 < _length) seg.start = i1; - seg.stop = i2; - if (i2 > _length) seg.stop = _length; - if (grouping) { - seg.grouping = grouping; - seg.spacing = spacing; + + if (_queuedChangesSegId == segId) _queuedChangesSegId = 255; // cancel queued change if already queued for this segment + + if (segId < getMaxSegments() && segId == getCurrSegmentId() && isServicing()) { // queue change to prevent concurrent access + // queuing a change for a second segment will lead to the loss of the first change if not yet applied + // however this is not a problem as the queued change is applied immediately after the effect function in that segment returns + _qStart = i1; _qStop = i2; _qStartY = startY; _qStopY = stopY; + _qGrouping = grouping; _qSpacing = spacing; _qOffset = offset; + _queuedChangesSegId = segId; + DEBUG_PRINT(F("Segment queued: ")); DEBUG_PRINTLN(segId); + return; // queued changes are applied immediately after effect function returns } - if (offset < UINT16_MAX) seg.offset = offset; - _segment_runtimes[n].markForReset(); - if (!boundsUnchanged) seg.refreshLightCapabilities(); + + _segments[segId].setUp(i1, i2, grouping, spacing, offset, startY, stopY); + if (segId > 0 && segId == getSegmentsNum()-1 && i2 <= i1) _segments.pop_back(); // if last segment was deleted remove it from vector +} + +void WS2812FX::setUpSegmentFromQueuedChanges() { + if (_queuedChangesSegId >= getSegmentsNum()) return; + getSegment(_queuedChangesSegId).setUp(_qStart, _qStop, _qGrouping, _qSpacing, _qOffset, _qStartY, _qStopY); + _queuedChangesSegId = 255; } void WS2812FX::restartRuntime() { - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) { - _segment_runtimes[i].markForReset(); - } + for (segment &seg : _segments) seg.markForReset(); } void WS2812FX::resetSegments() { - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) if (_segments[i].name) delete[] _segments[i].name; + _segments.clear(); // destructs all Segment as part of clearing + #ifndef WLED_DISABLE_2D + segment seg = isMatrix ? Segment(0, Segment::maxWidth, 0, Segment::maxHeight) : Segment(0, _length); + #else + segment seg = Segment(0, _length); + #endif + _segments.push_back(seg); _mainSegment = 0; - memset(_segments, 0, sizeof(_segments)); - //memset(_segment_runtimes, 0, sizeof(_segment_runtimes)); - _segment_index = 0; - _segments[0].mode = DEFAULT_MODE; - _segments[0].colors[0] = DEFAULT_COLOR; - _segments[0].start = 0; - _segments[0].speed = DEFAULT_SPEED; - _segments[0].intensity = DEFAULT_INTENSITY; - _segments[0].stop = _length; - _segments[0].grouping = 1; - _segments[0].setOption(SEG_OPTION_SELECTED, 1); - _segments[0].setOption(SEG_OPTION_ON, 1); - _segments[0].opacity = 255; - _segments[0].cct = 127; - - for (uint16_t i = 1; i < MAX_NUM_SEGMENTS; i++) - { - _segments[i].colors[0] = color_wheel(i*51); - _segments[i].grouping = 1; - _segments[i].setOption(SEG_OPTION_ON, 1); - _segments[i].opacity = 255; - _segments[i].cct = 127; - _segments[i].speed = DEFAULT_SPEED; - _segments[i].intensity = DEFAULT_INTENSITY; - _segment_runtimes[i].markForReset(); - } - _segment_runtimes[0].markForReset(); } void WS2812FX::makeAutoSegments(bool forceReset) { if (autoSegments) { //make one segment per bus uint16_t segStarts[MAX_NUM_SEGMENTS] = {0}; uint16_t segStops [MAX_NUM_SEGMENTS] = {0}; - uint8_t s = 0; - for (uint8_t i = 0; i < busses.getNumBusses(); i++) { + size_t s = 0; + + #ifndef WLED_DISABLE_2D + // 2D segment is the 1st one using entire matrix + if (isMatrix) { + segStarts[0] = 0; + segStops[0] = Segment::maxWidth*Segment::maxHeight; + s++; + } + #endif + + for (size_t i = s; i < busses.getNumBusses(); i++) { Bus* b = busses.getBus(i); segStarts[s] = b->getStart(); - segStops[s] = segStarts[s] + b->getLength(); + segStops[s] = segStarts[s] + b->getLength(); + + #ifndef WLED_DISABLE_2D + if (isMatrix && segStops[s] < Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix + if (isMatrix && segStarts[s] < Segment::maxWidth*Segment::maxHeight) segStarts[s] = Segment::maxWidth*Segment::maxHeight; + #endif //check for overlap with previous segments - for (uint8_t j = 0; j < s; j++) { + for (size_t j = 0; j < s; j++) { if (segStops[j] > segStarts[s] && segStarts[j] < segStops[s]) { //segments overlap, merge segStarts[j] = min(segStarts[s],segStarts[j]); @@ -737,49 +1610,79 @@ void WS2812FX::makeAutoSegments(bool forceReset) { } s++; } - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) { - setSegment(i, segStarts[i], segStops[i]); + + _segments.clear(); + _segments.reserve(s); // prevent reallocations + // there is always at least one segment (but we need to differentiate between 1D and 2D) + #ifndef WLED_DISABLE_2D + if (isMatrix) + _segments.push_back(Segment(0, Segment::maxWidth, 0, Segment::maxHeight)); + else + #endif + _segments.push_back(Segment(segStarts[0], segStops[0])); + for (size_t i = 1; i < s; i++) { + _segments.push_back(Segment(segStarts[i], segStops[i])); } + } else { - //expand the main seg to the entire length, but only if there are no other segments, or reset is forced - uint8_t mainSeg = getMainSegmentId(); - if (forceReset) { - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) { - setSegment(i, 0, 0); - } - } - - if (getActiveSegmentsNum() < 2) { - setSegment(mainSeg, 0, _length); + if (forceReset || getSegmentsNum() == 0) resetSegments(); + //expand the main seg to the entire length, but only if there are no other segments, or reset is forced + else if (getActiveSegmentsNum() == 1) { + size_t i = getLastActiveSegmentId(); + #ifndef WLED_DISABLE_2D + _segments[i].start = 0; + _segments[i].stop = Segment::maxWidth; + _segments[i].startY = 0; + _segments[i].stopY = Segment::maxHeight; + _segments[i].grouping = 1; + _segments[i].spacing = 0; + #else + _segments[i].start = 0; + _segments[i].stop = _length; + #endif } } + _mainSegment = 0; fixInvalidSegments(); } void WS2812FX::fixInvalidSegments() { //make sure no segment is longer than total (sanity check) - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].start >= _length) setSegment(i, 0, 0); - if (_segments[i].stop > _length) setSegment(i, _segments[i].start, _length); - // this is always called as the last step after finalizeInit(), update covered bus types - getSegment(i).refreshLightCapabilities(); + for (size_t i = getSegmentsNum()-1; i > 0; i--) { + if (isMatrix) { + #ifndef WLED_DISABLE_2D + if (_segments[i].start >= Segment::maxWidth * Segment::maxHeight) { + // 1D segment at the end of matrix + if (_segments[i].start >= _length || _segments[i].startY > 0 || _segments[i].stopY > 1) { _segments.erase(_segments.begin()+i); continue; } + if (_segments[i].stop > _length) _segments[i].stop = _length; + continue; + } + if (_segments[i].start >= Segment::maxWidth || _segments[i].startY >= Segment::maxHeight) { _segments.erase(_segments.begin()+i); continue; } + if (_segments[i].stop > Segment::maxWidth) _segments[i].stop = Segment::maxWidth; + if (_segments[i].stopY > Segment::maxHeight) _segments[i].stopY = Segment::maxHeight; + #endif + } else { + if (_segments[i].start >= _length) { _segments.erase(_segments.begin()+i); continue; } + if (_segments[i].stop > _length) _segments[i].stop = _length; + } } + // this is always called as the last step after finalizeInit(), update covered bus types + for (segment &seg : _segments) + seg.refreshLightCapabilities(); } //true if all segments align with a bus, or if a segment covers the total length +//irrelevant in 2D set-up bool WS2812FX::checkSegmentAlignment() { - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].start >= _segments[i].stop) continue; //inactive segment - bool aligned = false; - for (uint8_t b = 0; bgetStart() && _segments[i].stop == bus->getStart() + bus->getLength()) aligned = true; + if (seg.start == bus->getStart() && seg.stop == bus->getStart() + bus->getLength()) aligned = true; } - if (_segments[i].start == 0 && _segments[i].stop == _length) aligned = true; + if (seg.start == 0 && seg.stop == _length) aligned = true; if (!aligned) return false; } return true; @@ -788,401 +1691,116 @@ bool WS2812FX::checkSegmentAlignment() { //After this function is called, setPixelColor() will use that segment (offsets, grouping, ... will apply) //Note: If called in an interrupt (e.g. JSON API), original segment must be restored, //otherwise it can lead to a crash on ESP32 because _segment_index is modified while in use by the main thread -uint8_t WS2812FX::setPixelSegment(uint8_t n) -{ +uint8_t WS2812FX::setPixelSegment(uint8_t n) { uint8_t prevSegId = _segment_index; - if (n < MAX_NUM_SEGMENTS) { + if (n < _segments.size()) { _segment_index = n; - _virtualSegmentLength = SEGMENT.virtualLength(); + _virtualSegmentLength = _segments[_segment_index].virtualLength(); } return prevSegId; } -void WS2812FX::setRange(uint16_t i, uint16_t i2, uint32_t col) -{ - if (i2 >= i) - { - for (uint16_t x = i; x <= i2; x++) setPixelColor(x, col); - } else - { - for (uint16_t x = i2; x <= i; x++) setPixelColor(x, col); - } -} - -void WS2812FX::setShowCallback(show_callback cb) -{ - _callback = cb; -} - -void WS2812FX::setTransition(uint16_t t) -{ - _transitionDur = t; -} - -void WS2812FX::setTransitionMode(bool t) -{ - unsigned long waitMax = millis() + 20; //refresh after 20 ms if transition enabled - for (uint16_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - _segments[i].setOption(SEG_OPTION_TRANSITIONAL, t); - - if (t && _segments[i].mode == FX_MODE_STATIC && _segment_runtimes[i].next_time > waitMax) - _segment_runtimes[i].next_time = waitMax; - } -} - -/* - * color blend function - */ -uint32_t IRAM_ATTR WS2812FX::color_blend(uint32_t color1, uint32_t color2, uint16_t blend, bool b16) { - if(blend == 0) return color1; - uint16_t blendmax = b16 ? 0xFFFF : 0xFF; - if(blend == blendmax) return color2; - uint8_t shift = b16 ? 16 : 8; - - uint32_t w1 = W(color1); - uint32_t r1 = R(color1); - uint32_t g1 = G(color1); - uint32_t b1 = B(color1); - - uint32_t w2 = W(color2); - uint32_t r2 = R(color2); - uint32_t g2 = G(color2); - uint32_t b2 = B(color2); - - uint32_t w3 = ((w2 * blend) + (w1 * (blendmax - blend))) >> shift; - uint32_t r3 = ((r2 * blend) + (r1 * (blendmax - blend))) >> shift; - uint32_t g3 = ((g2 * blend) + (g1 * (blendmax - blend))) >> shift; - uint32_t b3 = ((b2 * blend) + (b1 * (blendmax - blend))) >> shift; - - return RGBW32(r3, g3, b3, w3); -} - -/* - * Fills segment with color - */ -void WS2812FX::fill(uint32_t c) { - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, c); - } -} - -/* - * Blends the specified color with the existing pixel color. - */ -void WS2812FX::blendPixelColor(uint16_t n, uint32_t color, uint8_t blend) -{ - setPixelColor(n, color_blend(getPixelColor(n), color, blend)); -} - -/* - * fade out function, higher rate = quicker fade - */ -void WS2812FX::fade_out(uint8_t rate) { - rate = (255-rate) >> 1; - float mappedRate = float(rate) +1.1; - - uint32_t color = SEGCOLOR(1); // target color - int w2 = W(color); - int r2 = R(color); - int g2 = G(color); - int b2 = B(color); - - for(uint16_t i = 0; i < SEGLEN; i++) { - color = getPixelColor(i); - int w1 = W(color); - int r1 = R(color); - int g1 = G(color); - int b1 = B(color); - - int wdelta = (w2 - w1) / mappedRate; - int rdelta = (r2 - r1) / mappedRate; - int gdelta = (g2 - g1) / mappedRate; - int bdelta = (b2 - b1) / mappedRate; - - // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) - wdelta += (w2 == w1) ? 0 : (w2 > w1) ? 1 : -1; - rdelta += (r2 == r1) ? 0 : (r2 > r1) ? 1 : -1; - gdelta += (g2 == g1) ? 0 : (g2 > g1) ? 1 : -1; - bdelta += (b2 == b1) ? 0 : (b2 > b1) ? 1 : -1; - - setPixelColor(i, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); - } -} - -/* - * blurs segment content, source: FastLED colorutils.cpp - */ -void WS2812FX::blur(uint8_t blur_amount) -{ - uint8_t keep = 255 - blur_amount; - uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; - for(uint16_t i = 0; i < SEGLEN; i++) - { - CRGB cur = col_to_crgb(getPixelColor(i)); - CRGB part = cur; - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if(i > 0) { - uint32_t c = getPixelColor(i-1); - uint8_t r = R(c); - uint8_t g = G(c); - uint8_t b = B(c); - setPixelColor(i-1, qadd8(r, part.red), qadd8(g, part.green), qadd8(b, part.blue)); - } - setPixelColor(i,cur.red, cur.green, cur.blue); - carryover = part; - } -} - -uint16_t IRAM_ATTR WS2812FX::triwave16(uint16_t in) -{ - if (in < 0x8000) return in *2; - return 0xFFFF - (in - 0x8000)*2; -} - -/* - * Generates a tristate square wave w/ attac & decay - * @param x input value 0-255 - * @param pulsewidth 0-127 - * @param attdec attac & decay, max. pulsewidth / 2 - * @returns signed waveform value - */ -int8_t WS2812FX::tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { - int8_t a = 127; - if (x > 127) { - a = -127; - x -= 127; - } - - if (x < attdec) { //inc to max - return (int16_t) x * a / attdec; - } - else if (x < pulsewidth - attdec) { //max - return a; - } - else if (x < pulsewidth) { //dec to 0 - return (int16_t) (pulsewidth - x) * a / attdec; - } - return 0; -} - -/* - * Put a value 0 to 255 in to get a color value. - * The colours are a transition r -> g -> b -> back to r - * Inspired by the Adafruit examples. - */ -uint32_t WS2812FX::color_wheel(uint8_t pos) { - if (SEGMENT.palette) return color_from_palette(pos, false, true, 0); - pos = 255 - pos; - if(pos < 85) { - return ((uint32_t)(255 - pos * 3) << 16) | ((uint32_t)(0) << 8) | (pos * 3); - } else if(pos < 170) { - pos -= 85; - return ((uint32_t)(0) << 16) | ((uint32_t)(pos * 3) << 8) | (255 - pos * 3); - } else { - pos -= 170; - return ((uint32_t)(pos * 3) << 16) | ((uint32_t)(255 - pos * 3) << 8) | (0); - } -} - -/* - * Returns a new, random wheel index with a minimum distance of 42 from pos. - */ -uint8_t WS2812FX::get_random_wheel_index(uint8_t pos) { - uint8_t r = 0, x = 0, y = 0, d = 0; - - while(d < 42) { - r = random8(); - x = abs(pos - r); - y = 255 - x; - d = MIN(x, y); - } - return r; +void WS2812FX::setRange(uint16_t i, uint16_t i2, uint32_t col) { + if (i2 < i) std::swap(i,i2); + for (unsigned x = i; x <= i2; x++) setPixelColor(x, col); } - -uint32_t IRAM_ATTR WS2812FX::crgb_to_col(CRGB fastled) -{ - return RGBW32(fastled.red, fastled.green, fastled.blue, 0); +void WS2812FX::setTransitionMode(bool t) { + for (segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } - -CRGB IRAM_ATTR WS2812FX::col_to_crgb(uint32_t color) -{ - CRGB fastled_col; - fastled_col.red = R(color); - fastled_col.green = G(color); - fastled_col.blue = B(color); - return fastled_col; +#ifdef WLED_DEBUG +void WS2812FX::printSize() { + size_t size = 0; + for (const Segment &seg : _segments) size += seg.getSize(); + DEBUG_PRINTF("Segments: %d -> %uB\n", _segments.size(), size); + DEBUG_PRINTF("Modes: %d*%d=%uB\n", sizeof(mode_ptr), _mode.size(), (_mode.capacity()*sizeof(mode_ptr))); + DEBUG_PRINTF("Data: %d*%d=%uB\n", sizeof(const char *), _modeData.size(), (_modeData.capacity()*sizeof(const char *))); + DEBUG_PRINTF("Map: %d*%d=%uB\n", sizeof(uint16_t), (int)customMappingSize, customMappingSize*sizeof(uint16_t)); + size = getLengthTotal(); + if (useGlobalLedBuffer) DEBUG_PRINTF("Buffer: %d*%u=%uB\n", sizeof(CRGB), size, size*sizeof(CRGB)); } +#endif - -void WS2812FX::load_gradient_palette(uint8_t index) -{ - byte i = constrain(index, 0, GRADIENT_PALETTE_COUNT -1); +void WS2812FX::loadCustomPalettes() { byte tcp[72]; //support gradient palettes with up to 18 entries - memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i])), 72); - targetPalette.loadDynamicGradientPalette(tcp); -} - - -/* - * FastLED palette modes helper function. Limitation: Due to memory reasons, multiple active segments with FastLED will disable the Palette transitions - */ -void WS2812FX::handle_palette(void) -{ - bool singleSegmentMode = (_segment_index == _segment_index_palette_last); - _segment_index_palette_last = _segment_index; - - byte paletteIndex = SEGMENT.palette; - if (paletteIndex == 0) //default palette. Differs depending on effect - { - switch (SEGMENT.mode) - { - case FX_MODE_FIRE_2012 : paletteIndex = 35; break; //heat palette - case FX_MODE_COLORWAVES : paletteIndex = 26; break; //landscape 33 - case FX_MODE_FILLNOISE8 : paletteIndex = 9; break; //ocean colors - case FX_MODE_NOISE16_1 : paletteIndex = 20; break; //Drywet - case FX_MODE_NOISE16_2 : paletteIndex = 43; break; //Blue cyan yellow - case FX_MODE_NOISE16_3 : paletteIndex = 35; break; //heat palette - case FX_MODE_NOISE16_4 : paletteIndex = 26; break; //landscape 33 - case FX_MODE_GLITTER : paletteIndex = 11; break; //rainbow colors - case FX_MODE_SUNRISE : paletteIndex = 35; break; //heat palette - case FX_MODE_FLOW : paletteIndex = 6; break; //party - } - } - if (SEGMENT.mode >= FX_MODE_METEOR && paletteIndex == 0) paletteIndex = 4; - - switch (paletteIndex) - { - case 0: //default palette. Exceptions for specific effects above - targetPalette = PartyColors_p; break; - case 1: {//periodically replace palette with a random one. Doesn't work with multiple FastLED segments - if (!singleSegmentMode) - { - targetPalette = PartyColors_p; break; //fallback - } - if (millis() - _lastPaletteChange > 1000 + ((uint32_t)(255-SEGMENT.intensity))*100) - { - targetPalette = CRGBPalette16( - CHSV(random8(), 255, random8(128, 255)), - CHSV(random8(), 255, random8(128, 255)), - CHSV(random8(), 192, random8(128, 255)), - CHSV(random8(), 255, random8(128, 255))); - _lastPaletteChange = millis(); - } break;} - case 2: {//primary color only - CRGB prim = col_to_crgb(SEGCOLOR(0)); - targetPalette = CRGBPalette16(prim); break;} - case 3: {//primary + secondary - CRGB prim = col_to_crgb(SEGCOLOR(0)); - CRGB sec = col_to_crgb(SEGCOLOR(1)); - targetPalette = CRGBPalette16(prim,prim,sec,sec); break;} - case 4: {//primary + secondary + tertiary - CRGB prim = col_to_crgb(SEGCOLOR(0)); - CRGB sec = col_to_crgb(SEGCOLOR(1)); - CRGB ter = col_to_crgb(SEGCOLOR(2)); - targetPalette = CRGBPalette16(ter,sec,prim); break;} - case 5: {//primary + secondary (+tert if not off), more distinct - CRGB prim = col_to_crgb(SEGCOLOR(0)); - CRGB sec = col_to_crgb(SEGCOLOR(1)); - if (SEGCOLOR(2)) { - CRGB ter = col_to_crgb(SEGCOLOR(2)); - targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,ter,ter,ter,ter,ter,prim); - } else { - targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,sec,sec,sec); + CRGBPalette16 targetPalette; + customPalettes.clear(); // start fresh + for (int index = 0; index<10; index++) { + char fileName[32]; + sprintf_P(fileName, PSTR("/palette%d.json"), index); + + StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers + if (WLED_FS.exists(fileName)) { + DEBUG_PRINT(F("Reading palette from ")); + DEBUG_PRINTLN(fileName); + + if (readObjectFromFile(fileName, nullptr, &pDoc)) { + JsonArray pal = pDoc[F("palette")]; + if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) + if (pal[0].is() && pal[1].is()) { + // we have an array of index & hex strings + size_t palSize = MIN(pal.size(), 36); + palSize -= palSize % 2; // make sure size is multiple of 2 + for (size_t i=0, j=0; i()<256; i+=2, j+=4) { + uint8_t rgbw[] = {0,0,0,0}; + tcp[ j ] = (uint8_t) pal[ i ].as(); // index + colorFromHexString(rgbw, pal[i+1].as()); // will catch non-string entires + for (size_t c=0; c<3; c++) tcp[j+1+c] = gamma8(rgbw[c]); // only use RGB component + DEBUG_PRINTF("%d(%d) : %d %d %d\n", i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); + } + } else { + size_t palSize = MIN(pal.size(), 72); + palSize -= palSize % 4; // make sure size is multiple of 4 + for (size_t i=0; i()<256; i+=4) { + tcp[ i ] = (uint8_t) pal[ i ].as(); // index + tcp[i+1] = gamma8((uint8_t) pal[i+1].as()); // R + tcp[i+2] = gamma8((uint8_t) pal[i+2].as()); // G + tcp[i+3] = gamma8((uint8_t) pal[i+3].as()); // B + DEBUG_PRINTF("%d(%d) : %d %d %d\n", i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); + } + } + customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); + } else { + DEBUG_PRINTLN(F("Wrong palette format.")); + } } - break;} - case 6: //Party colors - targetPalette = PartyColors_p; break; - case 7: //Cloud colors - targetPalette = CloudColors_p; break; - case 8: //Lava colors - targetPalette = LavaColors_p; break; - case 9: //Ocean colors - targetPalette = OceanColors_p; break; - case 10: //Forest colors - targetPalette = ForestColors_p; break; - case 11: //Rainbow colors - targetPalette = RainbowColors_p; break; - case 12: //Rainbow stripe colors - targetPalette = RainbowStripeColors_p; break; - default: //progmem palettes - load_gradient_palette(paletteIndex -13); - } - - if (singleSegmentMode && paletteFade && SEGENV.call > 0) //only blend if just one segment uses FastLED mode - { - nblendPaletteTowardPalette(currentPalette, targetPalette, 48); - } else - { - currentPalette = targetPalette; - } -} - - -/* - * Gets a single color from the currently selected palette. - * @param i Palette Index (if mapping is true, the full palette will be SEGLEN long, if false, 255). Will wrap around automatically. - * @param mapping if true, LED position in segment is considered for color - * @param wrap FastLED palettes will usally wrap back to the start smoothly. Set false to get a hard edge - * @param mcol If the default palette 0 is selected, return the standard color 0, 1 or 2 instead. If >2, Party palette is used instead - * @param pbri Value to scale the brightness of the returned color by. Default is 255. (no scaling) - * @returns Single color from palette - */ -uint32_t IRAM_ATTR WS2812FX::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) -{ - if ((SEGMENT.palette == 0 && mcol < 3) || _no_rgb) { - uint32_t color = SEGCOLOR(mcol); - if (pbri == 255) return color; - return RGBW32(scale8_video(R(color),pbri), scale8_video(G(color),pbri), scale8_video(B(color),pbri), scale8_video(W(color),pbri)); + } else { + break; + } } - - uint8_t paletteIndex = i; - if (mapping && SEGLEN > 1) paletteIndex = (i*255)/(SEGLEN -1); - if (!wrap) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" - CRGB fastled_col; - fastled_col = ColorFromPalette(currentPalette, paletteIndex, pbri, (paletteBlend == 3)? NOBLEND:LINEARBLEND); - - return crgb_to_col(fastled_col); } - //load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) -void WS2812FX::deserializeMap(uint8_t n) { +bool WS2812FX::deserializeMap(uint8_t n) { + // 2D support creates its own ledmap (on the fly) if a ledmap.json exists it will overwrite built one. + char fileName[32]; strcpy_P(fileName, PSTR("/ledmap")); if (n) sprintf(fileName +7, "%d", n); - strcat(fileName, ".json"); + strcat_P(fileName, PSTR(".json")); bool isFile = WLED_FS.exists(fileName); if (!isFile) { // erase custom mapping if selecting nonexistent ledmap.json (n==0) - if (!n && customMappingTable != nullptr) { + if (!isMatrix && !n && customMappingTable != nullptr) { customMappingSize = 0; delete[] customMappingTable; customMappingTable = nullptr; } - return; + return false; } - #ifdef WLED_USE_DYNAMIC_JSON - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - #else - if (!requestJSONBufferLock(7)) return; - #endif - - DEBUG_PRINT(F("Reading LED map from ")); - DEBUG_PRINTLN(fileName); + if (!requestJSONBufferLock(7)) return false; if (!readObjectFromFile(fileName, nullptr, &doc)) { releaseJSONBufferLock(); - return; //if file does not exist just exit + return false; //if file does not exist just exit } + DEBUG_PRINT(F("Reading LED map from ")); + DEBUG_PRINTLN(fileName); + // erase old custom ledmap if (customMappingTable != nullptr) { customMappingSize = 0; @@ -1194,66 +1812,26 @@ void WS2812FX::deserializeMap(uint8_t n) { if (!map.isNull() && map.size()) { // not an empty map customMappingSize = map.size(); customMappingTable = new uint16_t[customMappingSize]; - for (uint16_t i=0; i #define NODE_TYPE_ID_UNDEFINED 0 -#define NODE_TYPE_ID_ESP8266 82 -#define NODE_TYPE_ID_ESP32 32 +#define NODE_TYPE_ID_ESP8266 82 // should be 1 +#define NODE_TYPE_ID_ESP32 32 // should be 2 +#define NODE_TYPE_ID_ESP32S2 33 // etc +#define NODE_TYPE_ID_ESP32S3 34 +#define NODE_TYPE_ID_ESP32C3 35 /*********************************************************************************************\ * NodeStruct @@ -19,9 +22,14 @@ struct NodeStruct { String nodeName; IPAddress ip; - uint8_t unit; uint8_t age; - uint8_t nodeType; + union { + uint8_t nodeType; // a waste of space as we only have 5 types + struct { + uint8_t type : 7; // still a waste of space (4 bits would be enough and future-proof) + bool on : 1; + }; + }; uint32_t build; NodeStruct() : age(0), nodeType(0), build(0) diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index e41b7e3fb8..179a522c01 100644 --- a/wled00/alexa.cpp +++ b/wled00/alexa.cpp @@ -2,7 +2,7 @@ /* * Alexa Voice On/Off/Brightness/Color Control. Emulates a Philips Hue bridge to Alexa. - * + * * This was put together from these two excellent projects: * https://github.com/kakopappa/arduino-esp8266-alexa-wemo-switch * https://github.com/probonopd/ESP8266HueEmulator @@ -14,17 +14,25 @@ void onAlexaChange(EspalexaDevice* dev); void alexaInit() { - if (alexaEnabled && WLED_CONNECTED) - { - if (espalexaDevice == nullptr) //only init once + if (!alexaEnabled || !WLED_CONNECTED) return; + + espalexa.removeAllDevices(); + // the original configured device for on/off or macros (added first, i.e. index 0) + espalexaDevice = new EspalexaDevice(alexaInvocationName, onAlexaChange, EspalexaDeviceType::extendedcolor); + espalexa.addDevice(espalexaDevice); + + // up to 9 devices (added second, third, ... i.e. index 1 to 9) serve for switching on up to nine presets (preset IDs 1 to 9 in WLED), + // names are identical as the preset names, switching off can be done by switching off any of them + if (alexaNumPresets) { + String name = ""; + for (byte presetIndex = 1; presetIndex <= alexaNumPresets; presetIndex++) { - espalexaDevice = new EspalexaDevice(alexaInvocationName, onAlexaChange, EspalexaDeviceType::extendedcolor); - espalexa.addDevice(espalexaDevice); - espalexa.begin(&server); - } else { - espalexaDevice->setName(alexaInvocationName); + if (!getPresetName(presetIndex, name)) break; // no more presets + EspalexaDevice* dev = new EspalexaDevice(name.c_str(), onAlexaChange, EspalexaDeviceType::extendedcolor); + espalexa.addDevice(dev); } } + espalexa.begin(&server); } void handleAlexa() @@ -35,20 +43,34 @@ void handleAlexa() void onAlexaChange(EspalexaDevice* dev) { - EspalexaDeviceProperty m = espalexaDevice->getLastChangedProperty(); - + EspalexaDeviceProperty m = dev->getLastChangedProperty(); + if (m == EspalexaDeviceProperty::on) { - if (!macroAlexaOn) + if (dev->getId() == 0) // Device 0 is for on/off or macros { - if (bri == 0) + if (!macroAlexaOn) { - bri = briLast; - stateUpdated(CALL_MODE_ALEXA); + if (bri == 0) + { + bri = briLast; + stateUpdated(CALL_MODE_ALEXA); + } + } else + { + applyPreset(macroAlexaOn, CALL_MODE_ALEXA); + if (bri == 0) dev->setValue(briLast); //stop Alexa from complaining if macroAlexaOn does not actually turn on } - } else { - applyPreset(macroAlexaOn, CALL_MODE_ALEXA); - if (bri == 0) espalexaDevice->setValue(briLast); //stop Alexa from complaining if macroAlexaOn does not actually turn on + } else // switch-on behavior for preset devices + { + // turn off other preset devices + for (byte i = 1; i < espalexa.getDeviceCount(); i++) + { + if (i == dev->getId()) continue; + espalexa.getDevice(i)->setValue(0); // turn off other presets + } + + applyPreset(dev->getId(), CALL_MODE_ALEXA); // in alexaInit() preset 1 device was added second (index 1), preset 2 third (index 2) etc. } } else if (m == EspalexaDeviceProperty::off) { @@ -60,41 +82,53 @@ void onAlexaChange(EspalexaDevice* dev) bri = 0; stateUpdated(CALL_MODE_ALEXA); } - } else { + } else + { applyPreset(macroAlexaOff, CALL_MODE_ALEXA); - if (bri != 0) espalexaDevice->setValue(0); //stop Alexa from complaining if macroAlexaOff does not actually turn off + // below for loop stops Alexa from complaining if macroAlexaOff does not actually turn off + } + for (byte i = 0; i < espalexa.getDeviceCount(); i++) + { + espalexa.getDevice(i)->setValue(0); } } else if (m == EspalexaDeviceProperty::bri) { - bri = espalexaDevice->getValue(); + bri = dev->getValue(); stateUpdated(CALL_MODE_ALEXA); } else //color { - if (espalexaDevice->getColorMode() == EspalexaColorMode::ct) //shade of white + if (dev->getColorMode() == EspalexaColorMode::ct) //shade of white { byte rgbw[4]; - uint16_t ct = espalexaDevice->getCt(); - if (!ct) return; - uint16_t k = 1000000 / ct; //mireds to kelvin - - if (strip.hasCCTBus()) { - strip.setCCT(k); - rgbw[0]= 0; rgbw[1]= 0; rgbw[2]= 0; rgbw[3]= 255; - } else if (strip.hasWhiteChannel()) { + uint16_t ct = dev->getCt(); + if (!ct) return; + uint16_t k = 1000000 / ct; //mireds to kelvin + + if (strip.hasCCTBus()) { + bool hasManualWhite = strip.getActiveSegsLightCapabilities(true) & SEG_CAPABILITY_W; + + strip.setCCT(k); + if (hasManualWhite) { + rgbw[0] = 0; rgbw[1] = 0; rgbw[2] = 0; rgbw[3] = 255; + } else { + rgbw[0] = 255; rgbw[1] = 255; rgbw[2] = 255; rgbw[3] = 0; + dev->setValue(255); + } + } else if (strip.hasWhiteChannel()) { switch (ct) { //these values empirically look good on RGBW case 199: rgbw[0]=255; rgbw[1]=255; rgbw[2]=255; rgbw[3]=255; break; case 234: rgbw[0]=127; rgbw[1]=127; rgbw[2]=127; rgbw[3]=255; break; case 284: rgbw[0]= 0; rgbw[1]= 0; rgbw[2]= 0; rgbw[3]=255; break; case 350: rgbw[0]=130; rgbw[1]= 90; rgbw[2]= 0; rgbw[3]=255; break; case 383: rgbw[0]=255; rgbw[1]=153; rgbw[2]= 0; rgbw[3]=255; break; - default : colorKtoRGB(k, rgbw); + default : colorKtoRGB(k, rgbw); } } else { colorKtoRGB(k, rgbw); } - strip.setColor(0, rgbw[0], rgbw[1], rgbw[2], rgbw[3]); + strip.setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); } else { - uint32_t color = espalexaDevice->getRGB(); + uint32_t color = dev->getRGB(); strip.setColor(0, color); } stateUpdated(CALL_MODE_ALEXA); diff --git a/wled00/blynk.cpp b/wled00/blynk.cpp deleted file mode 100644 index b1619d816e..0000000000 --- a/wled00/blynk.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include "wled.h" -#include "src/dependencies/blynk/Blynk/BlynkHandlers.h" - -/* - * Remote light control with the free Blynk app - */ - -uint16_t blHue = 0; -byte blSat = 255; - -void initBlynk(const char *auth, const char *host, uint16_t port) -{ - #ifndef WLED_DISABLE_BLYNK - if (!WLED_CONNECTED) return; - blynkEnabled = (auth[0] != 0); - if (blynkEnabled) Blynk.config(auth, host, port); - #endif -} - -void handleBlynk() -{ - #ifndef WLED_DISABLE_BLYNK - if (WLED_CONNECTED && blynkEnabled) - Blynk.run(); - #endif -} - -void updateBlynk() -{ - #ifndef WLED_DISABLE_BLYNK - if (!WLED_CONNECTED) return; - Blynk.virtualWrite(V0, bri); - //we need a RGB -> HSB convert here - Blynk.virtualWrite(V3, bri? 1:0); - Blynk.virtualWrite(V4, effectCurrent); - Blynk.virtualWrite(V5, effectSpeed); - Blynk.virtualWrite(V6, effectIntensity); - Blynk.virtualWrite(V7, nightlightActive); - Blynk.virtualWrite(V8, notifyDirect); - #endif -} - -#ifndef WLED_DISABLE_BLYNK -BLYNK_WRITE(V0) -{ - bri = param.asInt();//bri - stateUpdated(CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V1) -{ - blHue = param.asInt();//hue - colorHStoRGB(blHue*10,blSat,col); - colorUpdated(CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V2) -{ - blSat = param.asInt();//sat - colorHStoRGB(blHue*10,blSat,col); - colorUpdated(CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V3) -{ - bool on = (param.asInt()>0); - if (!on != !bri) {toggleOnOff(); stateUpdated(CALL_MODE_BLYNK);} -} - -BLYNK_WRITE(V4) -{ - effectCurrent = param.asInt()-1;//fx - colorUpdated(CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V5) -{ - effectSpeed = param.asInt();//sx - colorUpdated(CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V6) -{ - effectIntensity = param.asInt();//ix - colorUpdated(CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V7) -{ - nightlightActive = (param.asInt()>0); -} - -BLYNK_WRITE(V8) -{ - notifyDirect = (param.asInt()>0); //send notifications -} -#endif diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp new file mode 100644 index 0000000000..4e514f4f12 --- /dev/null +++ b/wled00/bus_manager.cpp @@ -0,0 +1,641 @@ +/* + * Class implementation for addressing various light types + */ + +#include +#include +#include "const.h" +#include "pin_manager.h" +#include "bus_wrapper.h" +#include "bus_manager.h" + +//colors.cpp +uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); +uint16_t approximateKelvinFromRGB(uint32_t rgb); +void colorRGBtoRGBW(byte* rgb); + +//udp.cpp +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte *buffer, uint8_t bri=255, bool isRGBW=false); + +// enable additional debug output +#if defined(WLED_DEBUG_HOST) + #include "net_debug.h" + #define DEBUGOUT NetDebug +#else + #define DEBUGOUT Serial +#endif + +#ifdef WLED_DEBUG + #ifndef ESP8266 + #include + #endif + #define DEBUG_PRINT(x) DEBUGOUT.print(x) + #define DEBUG_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUG_PRINTF(x...) DEBUGOUT.printf(x) +#else + #define DEBUG_PRINT(x) + #define DEBUG_PRINTLN(x) + #define DEBUG_PRINTF(x...) +#endif + +//color mangling macros +#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) +#define R(c) (byte((c) >> 16)) +#define G(c) (byte((c) >> 8)) +#define B(c) (byte(c)) +#define W(c) (byte((c) >> 24)) + + +void ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { + if (_count >= WLED_MAX_COLOR_ORDER_MAPPINGS) { + return; + } + if (len == 0) { + return; + } + if (colorOrder > COL_ORDER_MAX) { + return; + } + _mappings[_count].start = start; + _mappings[_count].len = len; + _mappings[_count].colorOrder = colorOrder; + _count++; +} + +uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { + if (_count == 0) return defaultColorOrder; + // upper nibble contains W swap information + uint8_t swapW = defaultColorOrder >> 4; + for (uint8_t i = 0; i < _count; i++) { + if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { + return _mappings[i].colorOrder | (swapW << 4); + } + } + return defaultColorOrder; +} + + +uint32_t Bus::autoWhiteCalc(uint32_t c) { + uint8_t aWM = _autoWhiteMode; + if (_gAWM != AW_GLOBAL_DISABLED) aWM = _gAWM; + if (aWM == RGBW_MODE_MANUAL_ONLY) return c; + uint8_t w = W(c); + //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) + if (w > 0 && aWM == RGBW_MODE_DUAL) return c; + uint8_t r = R(c); + uint8_t g = G(c); + uint8_t b = B(c); + if (aWM == RGBW_MODE_MAX) return RGBW32(r, g, b, r > g ? (r > b ? r : b) : (g > b ? g : b)); // brightest RGB channel + w = r < g ? (r < b ? r : b) : (g < b ? g : b); + if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode + return RGBW32(r, g, b, w); +} + +uint8_t *Bus::allocData(size_t size) { + if (_data) free(_data); // should not happen, but for safety + return _data = (uint8_t *)(size>0 ? calloc(size, sizeof(uint8_t)) : nullptr); +} + + +BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) +: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814)) +, _skip(bc.skipAmount) //sacrificial pixels +, _colorOrder(bc.colorOrder) +, _colorOrderMap(com) +{ + if (!IS_DIGITAL(bc.type) || !bc.count) return; + if (!pinManager.allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; + _frequencykHz = 0U; + _pins[0] = bc.pins[0]; + if (IS_2PIN(bc.type)) { + if (!pinManager.allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { + cleanup(); + return; + } + _pins[1] = bc.pins[1]; + _frequencykHz = bc.frequency ? bc.frequency : 2000U; // 2MHz clock if undefined + } + _iType = PolyBus::getI(bc.type, _pins, nr); + if (_iType == I_NONE) return; + if (bc.doubleBuffer && !allocData(bc.count * (Bus::hasWhite(_type) + 3*Bus::hasRGB(_type)))) return; //warning: hardcoded channel count + _buffering = bc.doubleBuffer; + uint16_t lenToCreate = bc.count; + if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus + _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, nr, _frequencykHz); + _valid = (_busPtr != nullptr); + DEBUG_PRINTF("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u)\n", _valid?"S":"Uns", nr, bc.count, bc.type, _pins[0], _pins[1], _iType); +} + +void BusDigital::show() { + if (!_valid) return; + if (_buffering) { // should be _data != nullptr, but that causes ~20% FPS drop + size_t channels = Bus::hasWhite(_type) + 3*Bus::hasRGB(_type); + for (size_t i=0; i<_len; i++) { + size_t offset = i*channels; + uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); + uint32_t c; + if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs (_len is always a multiple of 3) + switch (i%3) { + case 0: c = RGBW32(_data[offset] , _data[offset+1], _data[offset+2], 0); break; + case 1: c = RGBW32(_data[offset-1], _data[offset] , _data[offset+1], 0); break; + case 2: c = RGBW32(_data[offset-2], _data[offset-1], _data[offset] , 0); break; + } + } else { + c = RGBW32(_data[offset],_data[offset+1],_data[offset+2],(Bus::hasWhite(_type)?_data[offset+3]:0)); + } + uint16_t pix = i; + if (_reversed) pix = _len - pix -1; + pix += _skip; + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co); + } + #if !defined(STATUSLED) || STATUSLED>=0 + if (_skip) PolyBus::setPixelColor(_busPtr, _iType, 0, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black + #endif + for (int i=1; i<_skip; i++) PolyBus::setPixelColor(_busPtr, _iType, i, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black + } + PolyBus::show(_busPtr, _iType, !_buffering); // faster if buffer consistency is not important +} + +bool BusDigital::canShow() { + if (!_valid) return true; + return PolyBus::canShow(_busPtr, _iType); +} + +void BusDigital::setBrightness(uint8_t b) { + if (_bri == b) return; + //Fix for turning off onboard LED breaking bus + #ifdef LED_BUILTIN + if (_bri == 0) { // && b > 0, covered by guard if above + if (_pins[0] == LED_BUILTIN || _pins[1] == LED_BUILTIN) reinit(); + } + #endif + uint8_t prevBri = _bri; + Bus::setBrightness(b); + PolyBus::setBrightness(_busPtr, _iType, b); + + if (_buffering) return; + + // must update/repaint every LED in the NeoPixelBus buffer to the new brightness + // the only case where repainting is unnecessary is when all pixels are set after the brightness change but before the next show + // (which we can't rely on) + uint16_t hwLen = _len; + if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus + for (uint_fast16_t i = 0; i < hwLen; i++) { + // use 0 as color order, actual order does not matter here as we just update the channel values as-is + uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, i, 0),prevBri); + PolyBus::setPixelColor(_busPtr, _iType, i, c, 0); + } +} + +//If LEDs are skipped, it is possible to use the first as a status LED. +//TODO only show if no new show due in the next 50ms +void BusDigital::setStatusPixel(uint32_t c) { + if (_valid && _skip) { + PolyBus::setPixelColor(_busPtr, _iType, 0, c, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); + if (canShow()) PolyBus::show(_busPtr, _iType); + } +} + +void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { + if (!_valid) return; + if (Bus::hasWhite(_type)) c = autoWhiteCalc(c); + if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT + if (_buffering) { // should be _data != nullptr, but that causes ~20% FPS drop + size_t channels = Bus::hasWhite(_type) + 3*Bus::hasRGB(_type); + size_t offset = pix*channels; + if (Bus::hasRGB(_type)) { + _data[offset++] = R(c); + _data[offset++] = G(c); + _data[offset++] = B(c); + } + if (Bus::hasWhite(_type)) _data[offset] = W(c); + } else { + if (_reversed) pix = _len - pix -1; + pix += _skip; + uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs + uint16_t pOld = pix; + pix = IC_INDEX_WS2812_1CH_3X(pix); + uint32_t cOld = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, pix, co),_bri); + switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) + case 0: c = RGBW32(R(cOld), W(c) , B(cOld), 0); break; + case 1: c = RGBW32(W(c) , G(cOld), B(cOld), 0); break; + case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break; + } + } + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co); + } +} + +// returns original color if global buffering is enabled, else returns lossly restored color from bus +uint32_t BusDigital::getPixelColor(uint16_t pix) { + if (!_valid) return 0; + if (_buffering) { // should be _data != nullptr, but that causes ~20% FPS drop + size_t channels = Bus::hasWhite(_type) + 3*Bus::hasRGB(_type); + size_t offset = pix*channels; + uint32_t c; + if (!Bus::hasRGB(_type)) { + c = RGBW32(_data[offset], _data[offset], _data[offset], _data[offset]); + } else { + c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], Bus::hasWhite(_type) ? _data[offset+3] : 0); + } + return c; + } else { + if (_reversed) pix = _len - pix -1; + pix += _skip; + uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri); + if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs + uint8_t r = R(c); + uint8_t g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? + uint8_t b = _reversed ? G(c) : B(c); + switch (pix % 3) { // get only the single channel + case 0: c = RGBW32(g, g, g, g); break; + case 1: c = RGBW32(r, r, r, r); break; + case 2: c = RGBW32(b, b, b, b); break; + } + } + return c; + } +} + +uint8_t BusDigital::getPins(uint8_t* pinArray) { + uint8_t numPins = IS_2PIN(_type) ? 2 : 1; + for (uint8_t i = 0; i < numPins; i++) pinArray[i] = _pins[i]; + return numPins; +} + +void BusDigital::setColorOrder(uint8_t colorOrder) { + // upper nibble contains W swap information + if ((colorOrder & 0x0F) > 5) return; + _colorOrder = colorOrder; +} + +void BusDigital::reinit() { + if (!_valid) return; + PolyBus::begin(_busPtr, _iType, _pins); +} + +void BusDigital::cleanup() { + DEBUG_PRINTLN(F("Digital Cleanup.")); + PolyBus::cleanup(_busPtr, _iType); + _iType = I_NONE; + _valid = false; + _busPtr = nullptr; + if (_data != nullptr) freeData(); + pinManager.deallocatePin(_pins[1], PinOwner::BusDigital); + pinManager.deallocatePin(_pins[0], PinOwner::BusDigital); +} + + +BusPwm::BusPwm(BusConfig &bc) +: Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) +{ + if (!IS_PWM(bc.type)) return; + uint8_t numPins = NUM_PWM_PINS(bc.type); + _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ; + + #ifdef ESP8266 + analogWriteRange(255); //same range as one RGB channel + analogWriteFreq(_frequency); + #else + _ledcStart = pinManager.allocateLedc(numPins); + if (_ledcStart == 255) { //no more free LEDC channels + deallocatePins(); return; + } + #endif + + for (uint8_t i = 0; i < numPins; i++) { + uint8_t currentPin = bc.pins[i]; + if (!pinManager.allocatePin(currentPin, true, PinOwner::BusPwm)) { + deallocatePins(); return; + } + _pins[i] = currentPin; //store only after allocatePin() succeeds + #ifdef ESP8266 + pinMode(_pins[i], OUTPUT); + #else + ledcSetup(_ledcStart + i, _frequency, 8); + ledcAttachPin(_pins[i], _ledcStart + i); + #endif + } + _data = _pwmdata; // avoid malloc() and use stack + _valid = true; +} + +void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { + if (pix != 0 || !_valid) return; //only react to first pixel + if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c); + if (_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { + c = colorBalanceFromKelvin(_cct, c); //color correction from CCT + } + uint8_t r = R(c); + uint8_t g = G(c); + uint8_t b = B(c); + uint8_t w = W(c); + uint8_t cct = 0; //0 - full warm white, 255 - full cold white + if (_cct > -1) { + if (_cct >= 1900) cct = (_cct - 1900) >> 5; + else if (_cct < 256) cct = _cct; + } else { + cct = (approximateKelvinFromRGB(c) - 1900) >> 5; + } + + uint8_t ww, cw; + #ifdef WLED_USE_IC_CCT + ww = w; + cw = cct; + #else + //0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold) + if (cct < _cctBlend) ww = 255; + else ww = ((255-cct) * 255) / (255 - _cctBlend); + + if ((255-cct) < _cctBlend) cw = 255; + else cw = (cct * 255) / (255 - _cctBlend); + + ww = (w * ww) / 255; //brightness scaling + cw = (w * cw) / 255; + #endif + + switch (_type) { + case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation + _data[0] = w; + break; + case TYPE_ANALOG_2CH: //warm white + cold white + _data[1] = cw; + _data[0] = ww; + break; + case TYPE_ANALOG_5CH: //RGB + warm white + cold white + _data[4] = cw; + w = ww; + case TYPE_ANALOG_4CH: //RGBW + _data[3] = w; + case TYPE_ANALOG_3CH: //standard dumb RGB + _data[0] = r; _data[1] = g; _data[2] = b; + break; + } +} + +//does no index check +uint32_t BusPwm::getPixelColor(uint16_t pix) { + if (!_valid) return 0; + return RGBW32(_data[0], _data[1], _data[2], _data[3]); +} + +void BusPwm::show() { + if (!_valid) return; + uint8_t numPins = NUM_PWM_PINS(_type); + for (uint8_t i = 0; i < numPins; i++) { + uint8_t scaled = (_data[i] * _bri) / 255; + if (_reversed) scaled = 255 - scaled; + #ifdef ESP8266 + analogWrite(_pins[i], scaled); + #else + ledcWrite(_ledcStart + i, scaled); + #endif + } +} + +uint8_t BusPwm::getPins(uint8_t* pinArray) { + if (!_valid) return 0; + uint8_t numPins = NUM_PWM_PINS(_type); + for (uint8_t i = 0; i < numPins; i++) { + pinArray[i] = _pins[i]; + } + return numPins; +} + +void BusPwm::deallocatePins() { + uint8_t numPins = NUM_PWM_PINS(_type); + for (uint8_t i = 0; i < numPins; i++) { + pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); + if (!pinManager.isPinOk(_pins[i])) continue; + #ifdef ESP8266 + digitalWrite(_pins[i], LOW); //turn off PWM interrupt + #else + if (_ledcStart < 16) ledcDetachPin(_pins[i]); + #endif + } + #ifdef ARDUINO_ARCH_ESP32 + pinManager.deallocateLedc(_ledcStart, numPins); + #endif +} + + +BusOnOff::BusOnOff(BusConfig &bc) +: Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) +, _onoffdata(0) +{ + if (bc.type != TYPE_ONOFF) return; + + uint8_t currentPin = bc.pins[0]; + if (!pinManager.allocatePin(currentPin, true, PinOwner::BusOnOff)) { + return; + } + _pin = currentPin; //store only after allocatePin() succeeds + pinMode(_pin, OUTPUT); + _data = &_onoffdata; // avoid malloc() and use stack + _valid = true; +} + +void BusOnOff::setPixelColor(uint16_t pix, uint32_t c) { + if (pix != 0 || !_valid) return; //only react to first pixel + c = autoWhiteCalc(c); + uint8_t r = R(c); + uint8_t g = G(c); + uint8_t b = B(c); + uint8_t w = W(c); + _data[0] = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; +} + +uint32_t BusOnOff::getPixelColor(uint16_t pix) { + if (!_valid) return 0; + return RGBW32(_data[0], _data[0], _data[0], _data[0]); +} + +void BusOnOff::show() { + if (!_valid) return; + digitalWrite(_pin, _reversed ? !(bool)_data[0] : (bool)_data[0]); +} + +uint8_t BusOnOff::getPins(uint8_t* pinArray) { + if (!_valid) return 0; + pinArray[0] = _pin; + return 1; +} + + +BusNetwork::BusNetwork(BusConfig &bc) +: Bus(bc.type, bc.start, bc.autoWhite, bc.count) +, _broadcastLock(false) +{ + switch (bc.type) { + case TYPE_NET_ARTNET_RGB: + _rgbw = false; + _UDPtype = 2; + break; + case TYPE_NET_E131_RGB: + _rgbw = false; + _UDPtype = 1; + break; + default: // TYPE_NET_DDP_RGB / TYPE_NET_DDP_RGBW + _rgbw = bc.type == TYPE_NET_DDP_RGBW; + _UDPtype = 0; + break; + } + _UDPchannels = _rgbw ? 4 : 3; + _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); + _valid = (allocData(_len * _UDPchannels) != nullptr); +} + +void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { + if (!_valid || pix >= _len) return; + if (_rgbw) c = autoWhiteCalc(c); + if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT + uint16_t offset = pix * _UDPchannels; + _data[offset] = R(c); + _data[offset+1] = G(c); + _data[offset+2] = B(c); + if (_rgbw) _data[offset+3] = W(c); +} + +uint32_t BusNetwork::getPixelColor(uint16_t pix) { + if (!_valid || pix >= _len) return 0; + uint16_t offset = pix * _UDPchannels; + return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (_rgbw ? _data[offset+3] : 0)); +} + +void BusNetwork::show() { + if (!_valid || !canShow()) return; + _broadcastLock = true; + realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw); + _broadcastLock = false; +} + +uint8_t BusNetwork::getPins(uint8_t* pinArray) { + for (uint8_t i = 0; i < 4; i++) { + pinArray[i] = _client[i]; + } + return 4; +} + +void BusNetwork::cleanup() { + _type = I_NONE; + _valid = false; + freeData(); +} + + +//utility to get the approx. memory usage of a given BusConfig +uint32_t BusManager::memUsage(BusConfig &bc) { + uint8_t type = bc.type; + uint16_t len = bc.count + bc.skipAmount; + if (type > 15 && type < 32) { // digital types + if (type == TYPE_UCS8903 || type == TYPE_UCS8904) len *= 2; // 16-bit LEDs + #ifdef ESP8266 + if (bc.pins[0] == 3) { //8266 DMA uses 5x the mem + if (type > 28) return len*20; //RGBW + return len*15; + } + if (type > 28) return len*4; //RGBW + return len*3; + #else //ESP32 RMT uses double buffer? + if (type > 28) return len*8; //RGBW + return len*6; + #endif + } + if (type > 31 && type < 48) return 5; + return len*3; //RGB +} + +int BusManager::add(BusConfig &bc) { + if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; + if (bc.type >= TYPE_NET_DDP_RGB && bc.type < 96) { + busses[numBusses] = new BusNetwork(bc); + } else if (IS_DIGITAL(bc.type)) { + busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap); + } else if (bc.type == TYPE_ONOFF) { + busses[numBusses] = new BusOnOff(bc); + } else { + busses[numBusses] = new BusPwm(bc); + } + return numBusses++; +} + +//do not call this method from system context (network callback) +void BusManager::removeAll() { + DEBUG_PRINTLN(F("Removing all.")); + //prevents crashes due to deleting busses while in use. + while (!canAllShow()) yield(); + for (uint8_t i = 0; i < numBusses; i++) delete busses[i]; + numBusses = 0; +} + +void BusManager::show() { + for (uint8_t i = 0; i < numBusses; i++) { + busses[i]->show(); + } +} + +void BusManager::setStatusPixel(uint32_t c) { + for (uint8_t i = 0; i < numBusses; i++) { + busses[i]->setStatusPixel(c); + } +} + +void IRAM_ATTR BusManager::setPixelColor(uint16_t pix, uint32_t c) { + for (uint8_t i = 0; i < numBusses; i++) { + Bus* b = busses[i]; + uint16_t bstart = b->getStart(); + if (pix < bstart || pix >= bstart + b->getLength()) continue; + busses[i]->setPixelColor(pix - bstart, c); + } +} + +void BusManager::setBrightness(uint8_t b) { + for (uint8_t i = 0; i < numBusses; i++) { + busses[i]->setBrightness(b); + } +} + +void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { + if (cct > 255) cct = 255; + if (cct >= 0) { + //if white balance correction allowed, save as kelvin value instead of 0-255 + if (allowWBCorrection) cct = 1900 + (cct << 5); + } else cct = -1; + Bus::setCCT(cct); +} + +uint32_t BusManager::getPixelColor(uint16_t pix) { + for (uint8_t i = 0; i < numBusses; i++) { + Bus* b = busses[i]; + uint16_t bstart = b->getStart(); + if (pix < bstart || pix >= bstart + b->getLength()) continue; + return b->getPixelColor(pix - bstart); + } + return 0; +} + +bool BusManager::canAllShow() { + for (uint8_t i = 0; i < numBusses; i++) { + if (!busses[i]->canShow()) return false; + } + return true; +} + +Bus* BusManager::getBus(uint8_t busNr) { + if (busNr >= numBusses) return nullptr; + return busses[busNr]; +} + +//semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) +uint16_t BusManager::getTotalLength() { + uint16_t len = 0; + for (uint8_t i=0; igetLength(); + return len; +} + +// Bus static member definition +int16_t Bus::_cct = -1; +uint8_t Bus::_cctBlend = 0; +uint8_t Bus::_gAWM = 255; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 4503a7da93..bff98629d1 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -6,58 +6,53 @@ */ #include "const.h" -#include "pin_manager.h" -#include "bus_wrapper.h" -#include - -//colors.cpp -uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); -void colorRGBtoRGBW(byte* rgb); - -// enable additional debug output -#ifdef WLED_DEBUG - #ifndef ESP8266 - #include - #endif - #define DEBUG_PRINT(x) Serial.print(x) - #define DEBUG_PRINTLN(x) Serial.println(x) - #define DEBUG_PRINTF(x...) Serial.printf(x) -#else - #define DEBUG_PRINT(x) - #define DEBUG_PRINTLN(x) - #define DEBUG_PRINTF(x...) -#endif #define GET_BIT(var,bit) (((var)>>(bit))&0x01) #define SET_BIT(var,bit) ((var)|=(uint16_t)(0x0001<<(bit))) #define UNSET_BIT(var,bit) ((var)&=(~(uint16_t)(0x0001<<(bit)))) -//color mangling macros -#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) -#define R(c) (byte((c) >> 16)) -#define G(c) (byte((c) >> 8)) -#define B(c) (byte(c)) -#define W(c) (byte((c) >> 24)) +#define NUM_ICS_WS2812_1CH_3X(len) (((len)+2)/3) // 1 WS2811 IC controls 3 zones (each zone has 1 LED, W) +#define IC_INDEX_WS2812_1CH_3X(i) ((i)/3) + +#define NUM_ICS_WS2812_2CH_3X(len) (((len)+1)*2/3) // 2 WS2811 ICs control 3 zones (each zone has 2 LEDs, CW and WW) +#define IC_INDEX_WS2812_2CH_3X(i) ((i)*2/3) +#define WS2812_2CH_3X_SPANS_2_ICS(i) ((i)&0x01) // every other LED zone is on two different ICs + +// flag for using double buffering in BusDigital +extern bool useGlobalLedBuffer; + //temporary struct for passing bus configuration to bus struct BusConfig { - uint8_t type = TYPE_WS2812_RGB; + uint8_t type; uint16_t count; uint16_t start; uint8_t colorOrder; bool reversed; uint8_t skipAmount; bool refreshReq; + uint8_t autoWhite; uint8_t pins[5] = {LEDPIN, 255, 255, 255, 255}; - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0) { + uint16_t frequency; + bool doubleBuffer; + + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, bool dblBfr=false) + : count(len) + , start(pstart) + , colorOrder(pcolorOrder) + , reversed(rev) + , skipAmount(skip) + , autoWhite(aw) + , frequency(clock_kHz) + , doubleBuffer(dblBfr) + { refreshReq = (bool) GET_BIT(busType,7); type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) - count = len; start = pstart; colorOrder = pcolorOrder; reversed = rev; skipAmount = skip; - uint8_t nPins = 1; + size_t nPins = 1; if (type >= TYPE_NET_DDP_RGB && type < 96) nPins = 4; //virtual network bus. 4 "pins" store IP address else if (type > 47) nPins = 2; else if (type > 40 && type < 46) nPins = NUM_PWM_PINS(type); - for (uint8_t i = 0; i < nPins; i++) pins[i] = ppins[i]; + for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; } //validates start and length and extends total if needed @@ -73,6 +68,7 @@ struct BusConfig { } }; + // Defines an LED Strip and its color ordering. struct ColorOrderMapEntry { uint16_t start; @@ -81,633 +77,268 @@ struct ColorOrderMapEntry { }; struct ColorOrderMap { - void add(uint16_t start, uint16_t len, uint8_t colorOrder) { - if (_count >= WLED_MAX_COLOR_ORDER_MAPPINGS) { - return; - } - if (len == 0) { - return; - } - if (colorOrder > COL_ORDER_MAX) { - return; - } - _mappings[_count].start = start; - _mappings[_count].len = len; - _mappings[_count].colorOrder = colorOrder; - _count++; - } + void add(uint16_t start, uint16_t len, uint8_t colorOrder); - uint8_t count() const { - return _count; - } - - void reset() { - _count = 0; - memset(_mappings, 0, sizeof(_mappings)); - } + uint8_t count() const { return _count; } - const ColorOrderMapEntry* get(uint8_t n) const { - if (n > _count) { - return nullptr; + void reset() { + _count = 0; + memset(_mappings, 0, sizeof(_mappings)); } - return &(_mappings[n]); - } - - inline uint8_t IRAM_ATTR getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { - if (_count == 0) return defaultColorOrder; - for (uint8_t i = 0; i < _count; i++) { - if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { - return _mappings[i].colorOrder; + const ColorOrderMapEntry* get(uint8_t n) const { + if (n > _count) { + return nullptr; } + return &(_mappings[n]); } - return defaultColorOrder; - } + + uint8_t getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const; private: - uint8_t _count; - ColorOrderMapEntry _mappings[WLED_MAX_COLOR_ORDER_MAPPINGS]; + uint8_t _count; + ColorOrderMapEntry _mappings[WLED_MAX_COLOR_ORDER_MAPPINGS]; }; + //parent class of BusDigital, BusPwm, and BusNetwork class Bus { public: - Bus(uint8_t type, uint16_t start) { - _type = type; - _start = start; + Bus(uint8_t type, uint16_t start, uint8_t aw, uint16_t len = 1, bool reversed = false, bool refresh = false) + : _type(type) + , _bri(255) + , _start(start) + , _len(len) + , _reversed(reversed) + , _valid(false) + , _needsRefresh(refresh) + , _data(nullptr) // keep data access consistent across all types of buses + { + _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; virtual ~Bus() {} //throw the bus under the bus - virtual void show() {} - virtual bool canShow() { return true; } - virtual void setStatusPixel(uint32_t c) {} - virtual void setPixelColor(uint16_t pix, uint32_t c) {} + virtual void show() = 0; + virtual bool canShow() { return true; } + virtual void setStatusPixel(uint32_t c) {} + virtual void setPixelColor(uint16_t pix, uint32_t c) = 0; virtual uint32_t getPixelColor(uint16_t pix) { return 0; } - virtual void setBrightness(uint8_t b) {} - virtual void cleanup() {} - virtual uint8_t getPins(uint8_t* pinArray) { return 0; } - virtual uint16_t getLength() { return _len; } - virtual void setColorOrder() {} - virtual uint8_t getColorOrder() { return COL_ORDER_RGB; } - virtual uint8_t skippedLeds() { return 0; } - inline uint16_t getStart() { return _start; } - inline void setStart(uint16_t start) { _start = start; } - inline uint8_t getType() { return _type; } - inline bool isOk() { return _valid; } - inline bool isOffRefreshRequired() { return _needsRefresh; } + virtual void setBrightness(uint8_t b) { _bri = b; }; + virtual void cleanup() = 0; + virtual uint8_t getPins(uint8_t* pinArray) { return 0; } + virtual uint16_t getLength() { return _len; } + virtual void setColorOrder() {} + virtual uint8_t getColorOrder() { return COL_ORDER_RGB; } + virtual uint8_t skippedLeds() { return 0; } + virtual uint16_t getFrequency() { return 0U; } + inline void setReversed(bool reversed) { _reversed = reversed; } + inline uint16_t getStart() { return _start; } + inline void setStart(uint16_t start) { _start = start; } + inline uint8_t getType() { return _type; } + inline bool isOk() { return _valid; } + inline bool isReversed() { return _reversed; } + inline bool isOffRefreshRequired() { return _needsRefresh; } bool containsPixel(uint16_t pix) { return pix >= _start && pix < _start+_len; } - virtual bool isRgbw() { return Bus::isRgbw(_type); } - static bool isRgbw(uint8_t type) { - if (type == TYPE_SK6812_RGBW || type == TYPE_TM1814) return true; - if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; + virtual bool hasRGB(void) { return Bus::hasRGB(_type); } + static bool hasRGB(uint8_t type) { + if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF) return false; + return true; + } + virtual bool hasWhite(void) { return Bus::hasWhite(_type); } + static bool hasWhite(uint8_t type) { + if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904) return true; // digital types with white channel + if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; // analog types with white channel + if (type == TYPE_NET_DDP_RGBW) return true; // network types with white channel + return false; + } + virtual bool hasCCT(void) { return Bus::hasCCT(_type); } + static bool hasCCT(uint8_t type) { + if (type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || + type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH) return true; return false; } static void setCCT(uint16_t cct) { _cct = cct; } - static void setCCTBlend(uint8_t b) { - if (b > 100) b = 100; - _cctBlend = (b * 127) / 100; - //compile-time limiter for hardware that can't power both white channels at max - #ifdef WLED_MAX_CCT_BLEND - if (_cctBlend > WLED_MAX_CCT_BLEND) _cctBlend = WLED_MAX_CCT_BLEND; - #endif - } - inline static void setAutoWhiteMode(uint8_t m) { if (m < 4) _autoWhiteMode = m; } - inline static uint8_t getAutoWhiteMode() { return _autoWhiteMode; } - - bool reversed = false; + static void setCCTBlend(uint8_t b) { + if (b > 100) b = 100; + _cctBlend = (b * 127) / 100; + //compile-time limiter for hardware that can't power both white channels at max + #ifdef WLED_MAX_CCT_BLEND + if (_cctBlend > WLED_MAX_CCT_BLEND) _cctBlend = WLED_MAX_CCT_BLEND; + #endif + } + inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } + inline uint8_t getAutoWhiteMode() { return _autoWhiteMode; } + inline static void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } + inline static uint8_t getGlobalAWMode() { return _gAWM; } protected: - uint8_t _type = TYPE_NONE; - uint8_t _bri = 255; - uint16_t _start = 0; - uint16_t _len = 1; - bool _valid = false; - bool _needsRefresh = false; - static uint8_t _autoWhiteMode; + uint8_t _type; + uint8_t _bri; + uint16_t _start; + uint16_t _len; + bool _reversed; + bool _valid; + bool _needsRefresh; + uint8_t _autoWhiteMode; + uint8_t *_data; + static uint8_t _gAWM; static int16_t _cct; - static uint8_t _cctBlend; - - uint32_t autoWhiteCalc(uint32_t c) { - if (_autoWhiteMode == RGBW_MODE_MANUAL_ONLY) return c; - uint8_t w = W(c); - //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) - if (w > 0 && _autoWhiteMode == RGBW_MODE_DUAL) return c; - uint8_t r = R(c); - uint8_t g = G(c); - uint8_t b = B(c); - w = r < g ? (r < b ? r : b) : (g < b ? g : b); - if (_autoWhiteMode == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode - return RGBW32(r, g, b, w); - } + static uint8_t _cctBlend; + + uint32_t autoWhiteCalc(uint32_t c); + uint8_t *allocData(size_t size = 1); + void freeData() { if (_data != nullptr) free(_data); _data = nullptr; } }; class BusDigital : public Bus { public: - BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) : Bus(bc.type, bc.start), _colorOrderMap(com) { - if (!IS_DIGITAL(bc.type) || !bc.count) return; - if (!pinManager.allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; - _pins[0] = bc.pins[0]; - if (IS_2PIN(bc.type)) { - if (!pinManager.allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { - cleanup(); return; - } - _pins[1] = bc.pins[1]; - } - reversed = bc.reversed; - _needsRefresh = bc.refreshReq || bc.type == TYPE_TM1814; - _skip = bc.skipAmount; //sacrificial pixels - _len = bc.count + _skip; - _iType = PolyBus::getI(bc.type, _pins, nr); - if (_iType == I_NONE) return; - _busPtr = PolyBus::create(_iType, _pins, _len, nr); - _valid = (_busPtr != nullptr); - _colorOrder = bc.colorOrder; - DEBUG_PRINTF("Successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u)\n",nr, _len, bc.type, _pins[0],_pins[1],_iType); - }; - - inline void show() { - PolyBus::show(_busPtr, _iType); - } + BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com); + ~BusDigital() { cleanup(); } + + void show(); + bool canShow(); + void setBrightness(uint8_t b); + void setStatusPixel(uint32_t c); + void setPixelColor(uint16_t pix, uint32_t c); + void setColorOrder(uint8_t colorOrder); + uint32_t getPixelColor(uint16_t pix); + uint8_t getColorOrder() { return _colorOrder; } + uint8_t getPins(uint8_t* pinArray); + uint8_t skippedLeds() { return _skip; } + uint16_t getFrequency() { return _frequencykHz; } + void reinit(); + void cleanup(); - inline bool canShow() { - return PolyBus::canShow(_busPtr, _iType); - } - - void setBrightness(uint8_t b) { - //Fix for turning off onboard LED breaking bus - #ifdef LED_BUILTIN - if (_bri == 0 && b > 0) { - if (_pins[0] == LED_BUILTIN || _pins[1] == LED_BUILTIN) PolyBus::begin(_busPtr, _iType, _pins); - } - #endif - _bri = b; - PolyBus::setBrightness(_busPtr, _iType, b); - } - - //If LEDs are skipped, it is possible to use the first as a status LED. - //TODO only show if no new show due in the next 50ms - void setStatusPixel(uint32_t c) { - if (_skip && canShow()) { - PolyBus::setPixelColor(_busPtr, _iType, 0, c, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); - PolyBus::show(_busPtr, _iType); + private: + uint8_t _skip; + uint8_t _colorOrder; + uint8_t _pins[2]; + uint8_t _iType; + uint16_t _frequencykHz; + void * _busPtr; + const ColorOrderMap &_colorOrderMap; + bool _buffering; // temporary until we figure out why comparison "_data != nullptr" causes severe FPS drop + + inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) { + if (restoreBri < 255) { + uint8_t* chan = (uint8_t*) &c; + for (uint_fast8_t i=0; i<4; i++) { + uint_fast16_t val = chan[i]; + chan[i] = ((val << 8) + restoreBri) / (restoreBri + 1); //adding _bri slightly improves recovery / stops degradation on re-scale + } + } + return c; } - } - - void setPixelColor(uint16_t pix, uint32_t c) { - if (_type == TYPE_SK6812_RGBW || _type == TYPE_TM1814) c = autoWhiteCalc(c); - if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT - if (reversed) pix = _len - pix -1; - else pix += _skip; - PolyBus::setPixelColor(_busPtr, _iType, pix, c, _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder)); - } - - uint32_t getPixelColor(uint16_t pix) { - if (reversed) pix = _len - pix -1; - else pix += _skip; - return PolyBus::getPixelColor(_busPtr, _iType, pix, _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder)); - } - - inline uint8_t getColorOrder() { - return _colorOrder; - } - - uint16_t getLength() { - return _len - _skip; - } - - uint8_t getPins(uint8_t* pinArray) { - uint8_t numPins = IS_2PIN(_type) ? 2 : 1; - for (uint8_t i = 0; i < numPins; i++) pinArray[i] = _pins[i]; - return numPins; - } - - void setColorOrder(uint8_t colorOrder) { - if (colorOrder > 5) return; - _colorOrder = colorOrder; - } - - inline uint8_t skippedLeds() { - return _skip; - } - - inline void reinit() { - PolyBus::begin(_busPtr, _iType, _pins); - } - - void cleanup() { - DEBUG_PRINTLN(F("Digital Cleanup.")); - PolyBus::cleanup(_busPtr, _iType); - _iType = I_NONE; - _valid = false; - _busPtr = nullptr; - pinManager.deallocatePin(_pins[1], PinOwner::BusDigital); - pinManager.deallocatePin(_pins[0], PinOwner::BusDigital); - } - - ~BusDigital() { - cleanup(); - } - - private: - uint8_t _colorOrder = COL_ORDER_GRB; - uint8_t _pins[2] = {255, 255}; - uint8_t _iType = I_NONE; - uint8_t _skip = 0; - void * _busPtr = nullptr; - const ColorOrderMap &_colorOrderMap; }; class BusPwm : public Bus { public: - BusPwm(BusConfig &bc) : Bus(bc.type, bc.start) { - _valid = false; - if (!IS_PWM(bc.type)) return; - uint8_t numPins = NUM_PWM_PINS(bc.type); - - #ifdef ESP8266 - analogWriteRange(255); //same range as one RGB channel - analogWriteFreq(WLED_PWM_FREQ); - #else - _ledcStart = pinManager.allocateLedc(numPins); - if (_ledcStart == 255) { //no more free LEDC channels - deallocatePins(); return; - } - #endif + BusPwm(BusConfig &bc); + ~BusPwm() { cleanup(); } - for (uint8_t i = 0; i < numPins; i++) { - uint8_t currentPin = bc.pins[i]; - if (!pinManager.allocatePin(currentPin, true, PinOwner::BusPwm)) { - deallocatePins(); return; - } - _pins[i] = currentPin; //store only after allocatePin() succeeds - #ifdef ESP8266 - pinMode(_pins[i], OUTPUT); - #else - ledcSetup(_ledcStart + i, WLED_PWM_FREQ, 8); - ledcAttachPin(_pins[i], _ledcStart + i); - #endif - } - reversed = bc.reversed; - _valid = true; - }; - - void setPixelColor(uint16_t pix, uint32_t c) { - if (pix != 0 || !_valid) return; //only react to first pixel - if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c); - if (_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { - c = colorBalanceFromKelvin(_cct, c); //color correction from CCT - } - uint8_t r = R(c); - uint8_t g = G(c); - uint8_t b = B(c); - uint8_t w = W(c); - uint8_t cct = 0; //0 - full warm white, 255 - full cold white - if (_cct > -1) { - if (_cct >= 1900) cct = (_cct - 1900) >> 5; - else if (_cct < 256) cct = _cct; - } else { - cct = (approximateKelvinFromRGB(c) - 1900) >> 5; - } - - uint8_t ww, cw; - #ifdef WLED_USE_IC_CCT - ww = w; - cw = cct; - #else - //0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold) - if (cct < _cctBlend) ww = 255; - else ww = ((255-cct) * 255) / (255 - _cctBlend); - - if ((255-cct) < _cctBlend) cw = 255; - else cw = (cct * 255) / (255 - _cctBlend); - - ww = (w * ww) / 255; //brightness scaling - cw = (w * cw) / 255; - #endif - - switch (_type) { - case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation - _data[0] = w; - break; - case TYPE_ANALOG_2CH: //warm white + cold white - _data[1] = cw; - _data[0] = ww; - break; - case TYPE_ANALOG_5CH: //RGB + warm white + cold white - // perhaps a non-linear adjustment would be in order. need to test - _data[4] = cw; - w = ww; - case TYPE_ANALOG_4CH: //RGBW - _data[3] = w; - case TYPE_ANALOG_3CH: //standard dumb RGB - _data[0] = r; _data[1] = g; _data[2] = b; - break; - } - } - - //does no index check - uint32_t getPixelColor(uint16_t pix) { - if (!_valid) return 0; - return RGBW32(_data[0], _data[1], _data[2], _data[3]); - } + void setPixelColor(uint16_t pix, uint32_t c); + uint32_t getPixelColor(uint16_t pix); //does no index check + uint8_t getPins(uint8_t* pinArray); + uint16_t getFrequency() { return _frequency; } + void show(); + void cleanup() { deallocatePins(); } - void show() { - if (!_valid) return; - uint8_t numPins = NUM_PWM_PINS(_type); - for (uint8_t i = 0; i < numPins; i++) { - uint8_t scaled = (_data[i] * _bri) / 255; - if (reversed) scaled = 255 - scaled; - #ifdef ESP8266 - analogWrite(_pins[i], scaled); - #else - ledcWrite(_ledcStart + i, scaled); - #endif - } - } - - inline void setBrightness(uint8_t b) { - _bri = b; - } - - uint8_t getPins(uint8_t* pinArray) { - if (!_valid) return 0; - uint8_t numPins = NUM_PWM_PINS(_type); - for (uint8_t i = 0; i < numPins; i++) { - pinArray[i] = _pins[i]; - } - return numPins; - } - - inline void cleanup() { - deallocatePins(); - } - - ~BusPwm() { - cleanup(); - } - - private: - uint8_t _pins[5] = {255, 255, 255, 255, 255}; - uint8_t _data[5] = {0}; - #ifdef ARDUINO_ARCH_ESP32 - uint8_t _ledcStart = 255; - #endif - - void deallocatePins() { - uint8_t numPins = NUM_PWM_PINS(_type); - for (uint8_t i = 0; i < numPins; i++) { - pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); - if (!pinManager.isPinOk(_pins[i])) continue; - #ifdef ESP8266 - digitalWrite(_pins[i], LOW); //turn off PWM interrupt - #else - if (_ledcStart < 16) ledcDetachPin(_pins[i]); - #endif - } + private: + uint8_t _pins[5]; + uint8_t _pwmdata[5]; #ifdef ARDUINO_ARCH_ESP32 - pinManager.deallocateLedc(_ledcStart, numPins); + uint8_t _ledcStart; #endif - } + uint16_t _frequency; + + void deallocatePins(); }; -class BusNetwork : public Bus { +class BusOnOff : public Bus { public: - BusNetwork(BusConfig &bc) : Bus(bc.type, bc.start) { - _valid = false; -// switch (bc.type) { -// case TYPE_NET_ARTNET_RGB: -// _rgbw = false; -// _UDPtype = 2; -// break; -// case TYPE_NET_E131_RGB: -// _rgbw = false; -// _UDPtype = 1; -// break; -// case TYPE_NET_DDP_RGB: -// _rgbw = false; -// _UDPtype = 0; -// break; -// default: - _rgbw = false; - _UDPtype = bc.type - TYPE_NET_DDP_RGB; -// break; -// } - _UDPchannels = _rgbw ? 4 : 3; - _data = (byte *)malloc(bc.count * _UDPchannels); - if (_data == nullptr) return; - memset(_data, 0, bc.count * _UDPchannels); - _len = bc.count; - _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); - _broadcastLock = false; - _valid = true; - }; + BusOnOff(BusConfig &bc); + ~BusOnOff() { cleanup(); } - void setPixelColor(uint16_t pix, uint32_t c) { - if (!_valid || pix >= _len) return; - if (isRgbw()) c = autoWhiteCalc(c); - if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT - uint16_t offset = pix * _UDPchannels; - _data[offset] = R(c); - _data[offset+1] = G(c); - _data[offset+2] = B(c); - if (_rgbw) _data[offset+3] = W(c); - } + void setPixelColor(uint16_t pix, uint32_t c); + uint32_t getPixelColor(uint16_t pix); + uint8_t getPins(uint8_t* pinArray); + void show(); + void cleanup() { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } - uint32_t getPixelColor(uint16_t pix) { - if (!_valid || pix >= _len) return 0; - uint16_t offset = pix * _UDPchannels; - return RGBW32(_data[offset], _data[offset+1], _data[offset+2], _rgbw ? (_data[offset+3] << 24) : 0); - } - - void show() { - if (!_valid || !canShow()) return; - _broadcastLock = true; - realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw); - _broadcastLock = false; - } - - inline bool canShow() { - // this should be a return value from UDP routine if it is still sending data out - return !_broadcastLock; - } - - inline void setBrightness(uint8_t b) { - _bri = b; - } - - uint8_t getPins(uint8_t* pinArray) { - for (uint8_t i = 0; i < 4; i++) { - pinArray[i] = _client[i]; - } - return 4; - } - - inline bool isRgbw() { - return _rgbw; - } - - inline uint16_t getLength() { - return _len; - } + private: + uint8_t _pin; + uint8_t _onoffdata; +}; - void cleanup() { - _type = I_NONE; - _valid = false; - if (_data != nullptr) free(_data); - _data = nullptr; - } - ~BusNetwork() { - cleanup(); - } +class BusNetwork : public Bus { + public: + BusNetwork(BusConfig &bc); + ~BusNetwork() { cleanup(); } + + bool hasRGB() { return true; } + bool hasWhite() { return _rgbw; } + bool canShow() { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out + void setPixelColor(uint16_t pix, uint32_t c); + uint32_t getPixelColor(uint16_t pix); + uint8_t getPins(uint8_t* pinArray); + void show(); + void cleanup(); private: IPAddress _client; - uint8_t _bri = 255; uint8_t _UDPtype; uint8_t _UDPchannels; bool _rgbw; bool _broadcastLock; - byte *_data; }; class BusManager { public: - BusManager() { - - }; - - //utility to get the approx. memory usage of a given BusConfig - static uint32_t memUsage(BusConfig &bc) { - uint8_t type = bc.type; - uint16_t len = bc.count; - if (type > 15 && type < 32) { - #ifdef ESP8266 - if (bc.pins[0] == 3) { //8266 DMA uses 5x the mem - if (type > 29) return len*20; //RGBW - return len*15; - } - if (type > 29) return len*4; //RGBW - return len*3; - #else //ESP32 RMT uses double buffer? - if (type > 29) return len*8; //RGBW - return len*6; - #endif - } - if (type > 31 && type < 48) return 5; - if (type == 44 || type == 45) return len*4; //RGBW - return len*3; //RGB - } - - int add(BusConfig &bc) { - if (numBusses >= WLED_MAX_BUSSES) return -1; - if (bc.type >= TYPE_NET_DDP_RGB && bc.type < 96) { - busses[numBusses] = new BusNetwork(bc); - } else if (IS_DIGITAL(bc.type)) { - busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap); - } else { - busses[numBusses] = new BusPwm(bc); - } - return numBusses++; - } + BusManager() : numBusses(0) {}; - //do not call this method from system context (network callback) - void removeAll() { - DEBUG_PRINTLN(F("Removing all.")); - //prevents crashes due to deleting busses while in use. - while (!canAllShow()) yield(); - for (uint8_t i = 0; i < numBusses; i++) delete busses[i]; - numBusses = 0; - } + //utility to get the approx. memory usage of a given BusConfig + static uint32_t memUsage(BusConfig &bc); - void show() { - for (uint8_t i = 0; i < numBusses; i++) { - busses[i]->show(); - } - } + int add(BusConfig &bc); - void setStatusPixel(uint32_t c) { - for (uint8_t i = 0; i < numBusses; i++) { - busses[i]->setStatusPixel(c); - } - } - - void IRAM_ATTR setPixelColor(uint16_t pix, uint32_t c, int16_t cct=-1) { - for (uint8_t i = 0; i < numBusses; i++) { - Bus* b = busses[i]; - uint16_t bstart = b->getStart(); - if (pix < bstart || pix >= bstart + b->getLength()) continue; - busses[i]->setPixelColor(pix - bstart, c); - } - } + //do not call this method from system context (network callback) + void removeAll(); - void setBrightness(uint8_t b) { - for (uint8_t i = 0; i < numBusses; i++) { - busses[i]->setBrightness(b); - } - } + void show(); + bool canAllShow(); + void setStatusPixel(uint32_t c); + void setPixelColor(uint16_t pix, uint32_t c); + void setBrightness(uint8_t b); + void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); + uint32_t getPixelColor(uint16_t pix); - void setSegmentCCT(int16_t cct, bool allowWBCorrection = false) { - if (cct > 255) cct = 255; - if (cct >= 0) { - //if white balance correction allowed, save as kelvin value instead of 0-255 - if (allowWBCorrection) cct = 1900 + (cct << 5); - } else cct = -1; - Bus::setCCT(cct); - } + Bus* getBus(uint8_t busNr); - uint32_t getPixelColor(uint16_t pix) { - for (uint8_t i = 0; i < numBusses; i++) { - Bus* b = busses[i]; - uint16_t bstart = b->getStart(); - if (pix < bstart || pix >= bstart + b->getLength()) continue; - return b->getPixelColor(pix - bstart); - } - return 0; - } + //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) + uint16_t getTotalLength(); + inline uint8_t getNumBusses() const { return numBusses; } - bool canAllShow() { - for (uint8_t i = 0; i < numBusses; i++) { - if (!busses[i]->canShow()) return false; - } - return true; - } - - Bus* getBus(uint8_t busNr) { - if (busNr >= numBusses) return nullptr; - return busses[busNr]; - } - - inline uint8_t getNumBusses() { - return numBusses; - } - - //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) - uint16_t getTotalLength() { - uint16_t len = 0; - for (uint8_t i=0; igetLength(); - return len; - } - - void updateColorOrderMap(const ColorOrderMap &com) { - memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); - } - - const ColorOrderMap& getColorOrderMap() const { - return colorOrderMap; - } + inline void updateColorOrderMap(const ColorOrderMap &com) { memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); } + inline const ColorOrderMap& getColorOrderMap() const { return colorOrderMap; } private: - uint8_t numBusses = 0; - Bus* busses[WLED_MAX_BUSSES]; - ColorOrderMap colorOrderMap; + uint8_t numBusses; + Bus* busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; + ColorOrderMap colorOrderMap; + + inline uint8_t getNumVirtualBusses() { + int j = 0; + for (int i=0; igetType() >= TYPE_NET_DDP_RGB && busses[i]->getType() < 96) j++; + return j; + } }; #endif diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 436239395c..45ace99d85 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -1,7 +1,22 @@ #ifndef BusWrapper_h #define BusWrapper_h -#include "NeoPixelBrightnessBus.h" +#include "NeoPixelBusLg.h" + +// temporary - these defines should actually be set in platformio.ini +// C3: I2S0 and I2S1 methods not supported (has one I2S bus) +// S2: I2S1 methods not supported (has one I2S bus) +// S3: I2S0 and I2S1 methods not supported yet (has two I2S buses) +// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/Esp32_i2s.h#L4 +// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/NeoEsp32RmtMethod.h#L857 + +#if !defined(WLED_NO_I2S0_PIXELBUS) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) +#define WLED_NO_I2S0_PIXELBUS +#endif +#if !defined(WLED_NO_I2S1_PIXELBUS) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2)) +#define WLED_NO_I2S1_PIXELBUS +#endif +// temporary end //Hardware SPI Pins #define P_8266_HS_MOSI 13 @@ -33,127 +48,239 @@ #define I_8266_U1_TM1_4 14 #define I_8266_DM_TM1_4 15 #define I_8266_BB_TM1_4 16 +//TM1829 (RGB) +#define I_8266_U0_TM2_3 17 +#define I_8266_U1_TM2_3 18 +#define I_8266_DM_TM2_3 19 +#define I_8266_BB_TM2_3 20 +//UCS8903 (RGB) +#define I_8266_U0_UCS_3 49 +#define I_8266_U1_UCS_3 50 +#define I_8266_DM_UCS_3 51 +#define I_8266_BB_UCS_3 52 +//UCS8904 (RGBW) +#define I_8266_U0_UCS_4 53 +#define I_8266_U1_UCS_4 54 +#define I_8266_DM_UCS_4 55 +#define I_8266_BB_UCS_4 56 /*** ESP32 Neopixel methods ***/ //RGB -#define I_32_RN_NEO_3 17 -#define I_32_I0_NEO_3 18 -#define I_32_I1_NEO_3 19 +#define I_32_RN_NEO_3 21 +#define I_32_I0_NEO_3 22 +#define I_32_I1_NEO_3 23 +#define I_32_BB_NEO_3 24 // bitbanging on ESP32 not recommended //RGBW -#define I_32_RN_NEO_4 20 -#define I_32_I0_NEO_4 21 -#define I_32_I1_NEO_4 22 +#define I_32_RN_NEO_4 25 +#define I_32_I0_NEO_4 26 +#define I_32_I1_NEO_4 27 +#define I_32_BB_NEO_4 28 // bitbanging on ESP32 not recommended //400Kbps -#define I_32_RN_400_3 23 -#define I_32_I0_400_3 24 -#define I_32_I1_400_3 25 +#define I_32_RN_400_3 29 +#define I_32_I0_400_3 30 +#define I_32_I1_400_3 31 +#define I_32_BB_400_3 32 // bitbanging on ESP32 not recommended //TM1814 (RGBW) -#define I_32_RN_TM1_4 26 -#define I_32_I0_TM1_4 27 -#define I_32_I1_TM1_4 28 +#define I_32_RN_TM1_4 33 +#define I_32_I0_TM1_4 34 +#define I_32_I1_TM1_4 35 +//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +//TM1829 (RGB) +#define I_32_RN_TM2_3 36 +#define I_32_I0_TM2_3 37 +#define I_32_I1_TM2_3 38 +//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +//UCS8903 (RGB) +#define I_32_RN_UCS_3 57 +#define I_32_I0_UCS_3 58 +#define I_32_I1_UCS_3 59 +//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +//UCS8904 (RGBW) +#define I_32_RN_UCS_4 60 +#define I_32_I0_UCS_4 61 +#define I_32_I1_UCS_4 62 //Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //APA102 -#define I_HS_DOT_3 29 //hardware SPI -#define I_SS_DOT_3 30 //soft SPI +#define I_HS_DOT_3 39 //hardware SPI +#define I_SS_DOT_3 40 //soft SPI //LPD8806 -#define I_HS_LPD_3 31 -#define I_SS_LPD_3 32 +#define I_HS_LPD_3 41 +#define I_SS_LPD_3 42 //WS2801 -#define I_HS_WS1_3 33 -#define I_SS_WS1_3 34 +#define I_HS_WS1_3 43 +#define I_SS_WS1_3 44 //P9813 -#define I_HS_P98_3 35 -#define I_SS_P98_3 36 +#define I_HS_P98_3 45 +#define I_SS_P98_3 46 + +//LPD6803 +#define I_HS_LPO_3 47 +#define I_SS_LPO_3 48 +// In the following NeoGammaNullMethod can be replaced with NeoGammaWLEDMethod to perform Gamma correction implicitly +// unfortunately that may apply Gamma correction to pre-calculated palettes which is undesired + /*** ESP8266 Neopixel methods ***/ #ifdef ESP8266 //RGB -#define B_8266_U0_NEO_3 NeoPixelBrightnessBus //3 chan, esp8266, gpio1 -#define B_8266_U1_NEO_3 NeoPixelBrightnessBus //3 chan, esp8266, gpio2 -#define B_8266_DM_NEO_3 NeoPixelBrightnessBus //3 chan, esp8266, gpio3 -#define B_8266_BB_NEO_3 NeoPixelBrightnessBus //3 chan, esp8266, bb (any pin but 16) +#define B_8266_U0_NEO_3 NeoPixelBusLg //3 chan, esp8266, gpio1 +#define B_8266_U1_NEO_3 NeoPixelBusLg //3 chan, esp8266, gpio2 +#define B_8266_DM_NEO_3 NeoPixelBusLg //3 chan, esp8266, gpio3 +#define B_8266_BB_NEO_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin but 16) //RGBW -#define B_8266_U0_NEO_4 NeoPixelBrightnessBus //4 chan, esp8266, gpio1 -#define B_8266_U1_NEO_4 NeoPixelBrightnessBus //4 chan, esp8266, gpio2 -#define B_8266_DM_NEO_4 NeoPixelBrightnessBus //4 chan, esp8266, gpio3 -#define B_8266_BB_NEO_4 NeoPixelBrightnessBus //4 chan, esp8266, bb (any pin) +#define B_8266_U0_NEO_4 NeoPixelBusLg //4 chan, esp8266, gpio1 +#define B_8266_U1_NEO_4 NeoPixelBusLg //4 chan, esp8266, gpio2 +#define B_8266_DM_NEO_4 NeoPixelBusLg //4 chan, esp8266, gpio3 +#define B_8266_BB_NEO_4 NeoPixelBusLg //4 chan, esp8266, bb (any pin) //400Kbps -#define B_8266_U0_400_3 NeoPixelBrightnessBus //3 chan, esp8266, gpio1 -#define B_8266_U1_400_3 NeoPixelBrightnessBus //3 chan, esp8266, gpio2 -#define B_8266_DM_400_3 NeoPixelBrightnessBus //3 chan, esp8266, gpio3 -#define B_8266_BB_400_3 NeoPixelBrightnessBus //3 chan, esp8266, bb (any pin) +#define B_8266_U0_400_3 NeoPixelBusLg //3 chan, esp8266, gpio1 +#define B_8266_U1_400_3 NeoPixelBusLg //3 chan, esp8266, gpio2 +#define B_8266_DM_400_3 NeoPixelBusLg //3 chan, esp8266, gpio3 +#define B_8266_BB_400_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin) //TM1814 (RGBW) -#define B_8266_U0_TM1_4 NeoPixelBrightnessBus -#define B_8266_U1_TM1_4 NeoPixelBrightnessBus -#define B_8266_DM_TM1_4 NeoPixelBrightnessBus -#define B_8266_BB_TM1_4 NeoPixelBrightnessBus +#define B_8266_U0_TM1_4 NeoPixelBusLg +#define B_8266_U1_TM1_4 NeoPixelBusLg +#define B_8266_DM_TM1_4 NeoPixelBusLg +#define B_8266_BB_TM1_4 NeoPixelBusLg +//TM1829 (RGB) +#define B_8266_U0_TM2_4 NeoPixelBusLg +#define B_8266_U1_TM2_4 NeoPixelBusLg +#define B_8266_DM_TM2_4 NeoPixelBusLg +#define B_8266_BB_TM2_4 NeoPixelBusLg +//UCS8903 +#define B_8266_U0_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio1 +#define B_8266_U1_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio2 +#define B_8266_DM_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio3 +#define B_8266_BB_UCS_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin but 16) +//UCS8904 RGBW +#define B_8266_U0_UCS_4 NeoPixelBusLg //4 chan, esp8266, gpio1 +#define B_8266_U1_UCS_4 NeoPixelBusLg //4 chan, esp8266, gpio2 +#define B_8266_DM_UCS_4 NeoPixelBusLg //4 chan, esp8266, gpio3 +#define B_8266_BB_UCS_4 NeoPixelBusLg //4 chan, esp8266, bb (any pin) #endif /*** ESP32 Neopixel methods ***/ #ifdef ARDUINO_ARCH_ESP32 //RGB -#define B_32_RN_NEO_3 NeoPixelBrightnessBus -#ifndef CONFIG_IDF_TARGET_ESP32C3 -#define B_32_I0_NEO_3 NeoPixelBrightnessBus +#define B_32_RN_NEO_3 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_NEO_3 NeoPixelBusLg #endif -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) -#define B_32_I1_NEO_3 NeoPixelBrightnessBus +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_NEO_3 NeoPixelBusLg #endif +//#define B_32_BB_NEO_3 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod //RGBW -#define B_32_RN_NEO_4 NeoPixelBrightnessBus -#ifndef CONFIG_IDF_TARGET_ESP32C3 -#define B_32_I0_NEO_4 NeoPixelBrightnessBus +#define B_32_RN_NEO_4 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_NEO_4 NeoPixelBusLg #endif -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) -#define B_32_I1_NEO_4 NeoPixelBrightnessBus +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_NEO_4 NeoPixelBusLg #endif +//#define B_32_BB_NEO_4 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod //400Kbps -#define B_32_RN_400_3 NeoPixelBrightnessBus -#ifndef CONFIG_IDF_TARGET_ESP32C3 -#define B_32_I0_400_3 NeoPixelBrightnessBus +#define B_32_RN_400_3 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_400_3 NeoPixelBusLg #endif -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) -#define B_32_I1_400_3 NeoPixelBrightnessBus +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_400_3 NeoPixelBusLg #endif +//#define B_32_BB_400_3 NeoPixelBusLg // NeoEsp8266BitBang400KbpsMethod //TM1814 (RGBW) -#define B_32_RN_TM1_4 NeoPixelBrightnessBus -#ifndef CONFIG_IDF_TARGET_ESP32C3 -#define B_32_I0_TM1_4 NeoPixelBrightnessBus +#define B_32_RN_TM1_4 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_TM1_4 NeoPixelBusLg #endif -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) -#define B_32_I1_TM1_4 NeoPixelBrightnessBus +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_TM1_4 NeoPixelBusLg +#endif +//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +//TM1829 (RGB) +#define B_32_RN_TM2_3 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_TM2_3 NeoPixelBusLg +#endif +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_TM2_3 NeoPixelBusLg +#endif +//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +//UCS8903 +#define B_32_RN_UCS_3 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_UCS_3 NeoPixelBusLg +#endif +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_UCS_3 NeoPixelBusLg +#endif +//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +//UCS8904 +#define B_32_RN_UCS_4 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_UCS_4 NeoPixelBusLg +#endif +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_UCS_4 NeoPixelBusLg #endif //Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) #endif //APA102 -#define B_HS_DOT_3 NeoPixelBrightnessBus //hardware SPI -#define B_SS_DOT_3 NeoPixelBrightnessBus //soft SPI +#ifdef WLED_USE_ETHERNET +// fix for #2542 (by @BlackBird77) +#define B_HS_DOT_3 NeoPixelBusLg //hardware HSPI (was DotStarEsp32DmaHspi5MhzMethod in NPB @ 2.6.9) +#else +#define B_HS_DOT_3 NeoPixelBusLg //hardware VSPI +#endif +#define B_SS_DOT_3 NeoPixelBusLg //soft SPI //LPD8806 -#define B_HS_LPD_3 NeoPixelBrightnessBus -#define B_SS_LPD_3 NeoPixelBrightnessBus +#define B_HS_LPD_3 NeoPixelBusLg +#define B_SS_LPD_3 NeoPixelBusLg + +//LPD6803 +#define B_HS_LPO_3 NeoPixelBusLg +#define B_SS_LPO_3 NeoPixelBusLg //WS2801 -//#define B_HS_WS1_3 NeoPixelBrightnessBus -//#define B_HS_WS1_3 NeoPixelBrightnessBus -//#define B_HS_WS1_3 NeoPixelBrightnessBus // 10MHz -#define B_HS_WS1_3 NeoPixelBrightnessBus //slower, more compatible -#define B_SS_WS1_3 NeoPixelBrightnessBus +#ifdef WLED_USE_ETHERNET +#define B_HS_WS1_3 NeoPixelBusLg>, NeoGammaNullMethod> +#else +#define B_HS_WS1_3 NeoPixelBusLg +#endif +#define B_SS_WS1_3 NeoPixelBusLg //P9813 -#define B_HS_P98_3 NeoPixelBrightnessBus -#define B_SS_P98_3 NeoPixelBrightnessBus +#define B_HS_P98_3 NeoPixelBusLg +#define B_SS_P98_3 NeoPixelBusLg + +// 48bit & 64bit to 24bit & 32bit RGB(W) conversion +#define toRGBW32(c) (RGBW32((c>>40)&0xFF, (c>>24)&0xFF, (c>>8)&0xFF, (c>>56)&0xFF)) +#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) //handles pointer type conversion for all possible bus types class PolyBus { public: + // initialize SPI bus speed for DotStar methods + template + static void beginDotStar(void* busPtr, int8_t sck, int8_t miso, int8_t mosi, int8_t ss, uint16_t clock_kHz = 0U) { + T dotStar_strip = static_cast(busPtr); + #ifdef ESP8266 + dotStar_strip->Begin(); + #else + if (sck == -1 && mosi == -1) dotStar_strip->Begin(); + else dotStar_strip->Begin(sck, miso, mosi, ss); + #endif + if (clock_kHz) dotStar_strip->SetMethodSettings(NeoSpiSettings((uint32_t)clock_kHz*1000)); + } + // Begin & initialize the PixelSettings for TM1814 strips. template static void beginTM1814(void* busPtr) { @@ -162,7 +289,8 @@ class PolyBus { // Max current for each LED (22.5 mA). tm1814_strip->SetPixelSettings(NeoTm1814Settings(/*R*/225, /*G*/225, /*B*/225, /*W*/225)); } - static void begin(void* busPtr, uint8_t busType, uint8_t* pins) { + + static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz = 0U) { switch (busType) { case I_NONE: break; #ifdef ESP8266 @@ -182,53 +310,91 @@ class PolyBus { case I_8266_U1_TM1_4: beginTM1814(busPtr); break; case I_8266_DM_TM1_4: beginTM1814(busPtr); break; case I_8266_BB_TM1_4: beginTM1814(busPtr); break; - case I_HS_DOT_3: (static_cast(busPtr))->Begin(); break; - case I_HS_LPD_3: (static_cast(busPtr))->Begin(); break; - case I_HS_WS1_3: (static_cast(busPtr))->Begin(); break; - case I_HS_P98_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_HS_DOT_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; + case I_HS_LPD_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; + case I_HS_LPO_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; + case I_HS_WS1_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; + case I_HS_P98_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; + case I_8266_U0_UCS_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_UCS_3: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_UCS_3: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_UCS_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_UCS_4: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_UCS_4: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_UCS_4: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_UCS_4: (static_cast(busPtr))->Begin(); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: (static_cast(busPtr))->Begin(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_3: (static_cast(busPtr))->Begin(); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: (static_cast(busPtr))->Begin(); break; #endif +// case I_32_BB_NEO_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_NEO_4: (static_cast(busPtr))->Begin(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: (static_cast(busPtr))->Begin(); break; #endif +// case I_32_BB_NEO_4: (static_cast(busPtr))->Begin(); break; case I_32_RN_400_3: (static_cast(busPtr))->Begin(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: (static_cast(busPtr))->Begin(); break; #endif +// case I_32_BB_400_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_TM1_4: beginTM1814(busPtr); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_RN_TM2_3: (static_cast(busPtr))->Begin(); break; + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1_4: beginTM1814(busPtr); break; + case I_32_I0_TM2_3: (static_cast(busPtr))->Begin(); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_TM1_4: beginTM1814(busPtr); break; - #endif - // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() - case I_HS_DOT_3: (static_cast(busPtr))->Begin(pins[1], -1, pins[0], -1); break; - case I_HS_LPD_3: (static_cast(busPtr))->Begin(pins[1], -1, pins[0], -1); break; - case I_HS_WS1_3: (static_cast(busPtr))->Begin(pins[1], -1, pins[0], -1); break; - case I_HS_P98_3: (static_cast(busPtr))->Begin(pins[1], -1, pins[0], -1); break; + case I_32_I1_TM2_3: (static_cast(busPtr))->Begin(); break; + #endif + case I_32_RN_UCS_3: (static_cast(busPtr))->Begin(); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_3: (static_cast(busPtr))->Begin(); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_3: (static_cast(busPtr))->Begin(); break; + #endif +// case I_32_BB_UCS_3: (static_cast(busPtr))->Begin(); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_4: (static_cast(busPtr))->Begin(); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_4: (static_cast(busPtr))->Begin(); break; + #endif +// case I_32_BB_UCS_4: (static_cast(busPtr))->Begin(); break; + // ESP32 can (and should, to avoid inadvertently driving the chip select signal) specify the pins used for SPI, but only in begin() + case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; + case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; + case I_HS_LPO_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; + case I_HS_WS1_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; + case I_HS_P98_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; #endif case I_SS_DOT_3: (static_cast(busPtr))->Begin(); break; case I_SS_LPD_3: (static_cast(busPtr))->Begin(); break; + case I_SS_LPO_3: (static_cast(busPtr))->Begin(); break; case I_SS_WS1_3: (static_cast(busPtr))->Begin(); break; case I_SS_P98_3: (static_cast(busPtr))->Begin(); break; } - }; - static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel) { + } + + static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel, uint16_t clock_kHz = 0U) { void* busPtr = nullptr; switch (busType) { case I_NONE: break; @@ -249,111 +415,185 @@ class PolyBus { case I_8266_U1_TM1_4: busPtr = new B_8266_U1_TM1_4(len, pins[0]); break; case I_8266_DM_TM1_4: busPtr = new B_8266_DM_TM1_4(len, pins[0]); break; case I_8266_BB_TM1_4: busPtr = new B_8266_BB_TM1_4(len, pins[0]); break; + case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_4(len, pins[0]); break; + case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_4(len, pins[0]); break; + case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_4(len, pins[0]); break; + case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_4(len, pins[0]); break; + case I_8266_U0_UCS_3: busPtr = new B_8266_U0_UCS_3(len, pins[0]); break; + case I_8266_U1_UCS_3: busPtr = new B_8266_U1_UCS_3(len, pins[0]); break; + case I_8266_DM_UCS_3: busPtr = new B_8266_DM_UCS_3(len, pins[0]); break; + case I_8266_BB_UCS_3: busPtr = new B_8266_BB_UCS_3(len, pins[0]); break; + case I_8266_U0_UCS_4: busPtr = new B_8266_U0_UCS_4(len, pins[0]); break; + case I_8266_U1_UCS_4: busPtr = new B_8266_U1_UCS_4(len, pins[0]); break; + case I_8266_DM_UCS_4: busPtr = new B_8266_DM_UCS_4(len, pins[0]); break; + case I_8266_BB_UCS_4: busPtr = new B_8266_BB_UCS_4(len, pins[0]); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: busPtr = new B_32_I1_NEO_3(len, pins[0]); break; #endif +// case I_32_BB_NEO_3: busPtr = new B_32_BB_NEO_3(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)channel); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: busPtr = new B_32_I1_NEO_4(len, pins[0]); break; #endif +// case I_32_BB_NEO_4: busPtr = new B_32_BB_NEO_4(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: busPtr = new B_32_I1_400_3(len, pins[0]); break; #endif +// case I_32_BB_400_3: busPtr = new B_32_BB_400_3(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)channel); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)channel); break; + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1_4: busPtr = new B_32_I0_TM1_4(len, pins[0]); break; + case I_32_I0_TM2_3: busPtr = new B_32_I0_TM2_3(len, pins[0]); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_TM1_4: busPtr = new B_32_I1_TM1_4(len, pins[0]); break; + case I_32_I1_TM2_3: busPtr = new B_32_I1_TM2_3(len, pins[0]); break; + #endif + case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)channel); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_3: busPtr = new B_32_I0_UCS_3(len, pins[0]); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_3: busPtr = new B_32_I1_UCS_3(len, pins[0]); break; + #endif +// case I_32_BB_UCS_3: busPtr = new B_32_BB_UCS_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)channel); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_4: busPtr = new B_32_I0_UCS_4(len, pins[0]); break; #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_4: busPtr = new B_32_I1_UCS_4(len, pins[0]); break; + #endif +// case I_32_BB_UCS_4: busPtr = new B_32_BB_UCS_4(len, pins[0], (NeoBusChannel)channel); break; #endif // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break; case I_SS_DOT_3: busPtr = new B_SS_DOT_3(len, pins[1], pins[0]); break; case I_HS_LPD_3: busPtr = new B_HS_LPD_3(len, pins[1], pins[0]); break; case I_SS_LPD_3: busPtr = new B_SS_LPD_3(len, pins[1], pins[0]); break; + case I_HS_LPO_3: busPtr = new B_HS_LPO_3(len, pins[1], pins[0]); break; + case I_SS_LPO_3: busPtr = new B_SS_LPO_3(len, pins[1], pins[0]); break; case I_HS_WS1_3: busPtr = new B_HS_WS1_3(len, pins[1], pins[0]); break; case I_SS_WS1_3: busPtr = new B_SS_WS1_3(len, pins[1], pins[0]); break; case I_HS_P98_3: busPtr = new B_HS_P98_3(len, pins[1], pins[0]); break; case I_SS_P98_3: busPtr = new B_SS_P98_3(len, pins[1], pins[0]); break; } - begin(busPtr, busType, pins); + begin(busPtr, busType, pins, clock_kHz); return busPtr; - }; - static void show(void* busPtr, uint8_t busType) { + } + + static void show(void* busPtr, uint8_t busType, bool consistent = true) { switch (busType) { case I_NONE: break; #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->Show(); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->Show(); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->Show(); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->Show(); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->Show(); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->Show(); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->Show(); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->Show(); break; - case I_8266_U0_400_3: (static_cast(busPtr))->Show(); break; - case I_8266_U1_400_3: (static_cast(busPtr))->Show(); break; - case I_8266_DM_400_3: (static_cast(busPtr))->Show(); break; - case I_8266_BB_400_3: (static_cast(busPtr))->Show(); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->Show(); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->Show(); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->Show(); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->Show(); break; + case I_8266_U0_NEO_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_NEO_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_NEO_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_NEO_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_NEO_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_NEO_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_NEO_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_NEO_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_400_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_400_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_400_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_400_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_TM1_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_TM1_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_TM1_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_TM1_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_UCS_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_UCS_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_UCS_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_UCS_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_UCS_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_UCS_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_UCS_4: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_UCS_4: (static_cast(busPtr))->Show(consistent); break; #endif #ifdef ARDUINO_ARCH_ESP32 - case I_32_RN_NEO_3: (static_cast(busPtr))->Show(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_NEO_3: (static_cast(busPtr))->Show(); break; + case I_32_RN_NEO_3: (static_cast(busPtr))->Show(consistent); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->Show(consistent); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: (static_cast(busPtr))->Show(consistent); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_NEO_3: (static_cast(busPtr))->Show(); break; +// case I_32_BB_NEO_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_NEO_4: (static_cast(busPtr))->Show(consistent); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_4: (static_cast(busPtr))->Show(consistent); break; #endif - case I_32_RN_NEO_4: (static_cast(busPtr))->Show(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_NEO_4: (static_cast(busPtr))->Show(); break; + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_4: (static_cast(busPtr))->Show(consistent); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_NEO_4: (static_cast(busPtr))->Show(); break; +// case I_32_BB_NEO_4: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_400_3: (static_cast(busPtr))->Show(consistent); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_400_3: (static_cast(busPtr))->Show(consistent); break; #endif - case I_32_RN_400_3: (static_cast(busPtr))->Show(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_400_3: (static_cast(busPtr))->Show(); break; + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_400_3: (static_cast(busPtr))->Show(consistent); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_400_3: (static_cast(busPtr))->Show(); break; +// case I_32_BB_400_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_TM1_4: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_TM2_3: (static_cast(busPtr))->Show(consistent); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1_4: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_TM2_3: (static_cast(busPtr))->Show(consistent); break; #endif - case I_32_RN_TM1_4: (static_cast(busPtr))->Show(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_TM1_4: (static_cast(busPtr))->Show(); break; + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1_4: (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_TM2_3: (static_cast(busPtr))->Show(consistent); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_TM1_4: (static_cast(busPtr))->Show(); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->Show(consistent); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_3: (static_cast(busPtr))->Show(consistent); break; #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_3: (static_cast(busPtr))->Show(consistent); break; + #endif +// case I_32_BB_UCS_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_4: (static_cast(busPtr))->Show(consistent); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_4: (static_cast(busPtr))->Show(consistent); break; + #endif +// case I_32_BB_UCS_4: (static_cast(busPtr))->Show(consistent); break; #endif - case I_HS_DOT_3: (static_cast(busPtr))->Show(); break; - case I_SS_DOT_3: (static_cast(busPtr))->Show(); break; - case I_HS_LPD_3: (static_cast(busPtr))->Show(); break; - case I_SS_LPD_3: (static_cast(busPtr))->Show(); break; - case I_HS_WS1_3: (static_cast(busPtr))->Show(); break; - case I_SS_WS1_3: (static_cast(busPtr))->Show(); break; - case I_HS_P98_3: (static_cast(busPtr))->Show(); break; - case I_SS_P98_3: (static_cast(busPtr))->Show(); break; + case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; + case I_SS_DOT_3: (static_cast(busPtr))->Show(consistent); break; + case I_HS_LPD_3: (static_cast(busPtr))->Show(consistent); break; + case I_SS_LPD_3: (static_cast(busPtr))->Show(consistent); break; + case I_HS_LPO_3: (static_cast(busPtr))->Show(consistent); break; + case I_SS_LPO_3: (static_cast(busPtr))->Show(consistent); break; + case I_HS_WS1_3: (static_cast(busPtr))->Show(consistent); break; + case I_SS_WS1_3: (static_cast(busPtr))->Show(consistent); break; + case I_HS_P98_3: (static_cast(busPtr))->Show(consistent); break; + case I_SS_P98_3: (static_cast(busPtr))->Show(consistent); break; } - }; + } + static bool canShow(void* busPtr, uint8_t busType) { switch (busType) { case I_NONE: return true; @@ -374,48 +614,84 @@ class PolyBus { case I_8266_U1_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_TM1_4: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_UCS_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_UCS_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_UCS_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_UCS_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_UCS_4: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_UCS_4: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_UCS_4: return (static_cast(busPtr))->CanShow(); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_3: return (static_cast(busPtr))->CanShow(); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: return (static_cast(busPtr))->CanShow(); break; #endif +// case I_32_BB_NEO_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_NEO_4: return (static_cast(busPtr))->CanShow(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: return (static_cast(busPtr))->CanShow(); break; #endif +// case I_32_BB_NEO_4: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_400_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: return (static_cast(busPtr))->CanShow(); break; #endif +// case I_32_BB_400_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_TM1_4: return (static_cast(busPtr))->CanShow(); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_RN_TM2_3: return (static_cast(busPtr))->CanShow(); break; + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM2_3: return (static_cast(busPtr))->CanShow(); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_TM1_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM2_3: return (static_cast(busPtr))->CanShow(); break; #endif + case I_32_RN_UCS_3: return (static_cast(busPtr))->CanShow(); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_3: return (static_cast(busPtr))->CanShow(); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_3: return (static_cast(busPtr))->CanShow(); break; + #endif +// case I_32_BB_UCS_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_4: return (static_cast(busPtr))->CanShow(); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_4: return (static_cast(busPtr))->CanShow(); break; + #endif +// case I_32_BB_UCS_4: return (static_cast(busPtr))->CanShow(); break; #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_DOT_3: return (static_cast(busPtr))->CanShow(); break; case I_HS_LPD_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_LPD_3: return (static_cast(busPtr))->CanShow(); break; + case I_HS_LPO_3: return (static_cast(busPtr))->CanShow(); break; + case I_SS_LPO_3: return (static_cast(busPtr))->CanShow(); break; case I_HS_WS1_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_WS1_3: return (static_cast(busPtr))->CanShow(); break; case I_HS_P98_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_P98_3: return (static_cast(busPtr))->CanShow(); break; } return true; - }; + } + static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co) { uint8_t r = c >> 16; uint8_t g = c >> 8; @@ -423,146 +699,220 @@ class PolyBus { uint8_t w = c >> 24; RgbwColor col; - //TODO make color order override possible on a per-strip basis - #ifdef COLOR_ORDER_OVERRIDE - if (pix >= COO_MIN && pix < COO_MAX) co = COO_ORDER; - #endif - - //reorder channels to selected order - switch (co) - { - case 0: col.G = g; col.R = r; col.B = b; break; //0 = GRB, default + // reorder channels to selected order + switch (co & 0x0F) { + default: col.G = g; col.R = r; col.B = b; break; //0 = GRB, default case 1: col.G = r; col.R = g; col.B = b; break; //1 = RGB, common for WS2811 case 2: col.G = b; col.R = r; col.B = g; break; //2 = BRG case 3: col.G = r; col.R = b; col.B = g; break; //3 = RBG case 4: col.G = b; col.R = g; col.B = r; break; //4 = BGR - default: col.G = g; col.R = b; col.B = r; break; //5 = GBR + case 5: col.G = g; col.R = b; col.B = r; break; //5 = GBR + } + // upper nibble contains W swap information + switch (co >> 4) { + default: col.W = w; break; // no swapping + case 1: col.W = col.B; col.B = w; break; // swap W & B + case 2: col.W = col.G; col.G = w; break; // swap W & G + case 3: col.W = col.R; col.R = w; break; // swap W & R } - col.W = w; switch (busType) { case I_NONE: break; #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; + case I_8266_U0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_DM_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_U1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_DM_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_BB_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_8266_U1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_8266_DM_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_8266_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; + case I_8266_U0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_DM_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_U1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_DM_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_BB_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_8266_U1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_8266_DM_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_8266_BB_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_8266_U0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + case I_8266_U1_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + case I_8266_DM_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + case I_8266_BB_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; #endif #ifdef ARDUINO_ARCH_ESP32 - case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; + case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif +// case I_32_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; #endif - case I_32_RN_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; +// case I_32_BB_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_RN_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif +// case I_32_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(colB)); break; case I_32_RN_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_RN_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #endif + case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + #endif +// case I_32_BB_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + #endif +// case I_32_BB_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; #endif - case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_HS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_SS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_HS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_SS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_HS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; - case I_SS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; + case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_HS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_SS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_HS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_SS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_HS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_SS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_HS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_SS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; } - }; + } + static void setBrightness(void* busPtr, uint8_t busType, uint8_t b) { switch (busType) { case I_NONE: break; #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_U0_400_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_U1_400_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_DM_400_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_BB_400_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->SetBrightness(b); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->SetBrightness(b); break; + case I_8266_U0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_400_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_400_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_400_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_400_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; #endif #ifdef ARDUINO_ARCH_ESP32 - case I_32_RN_NEO_3: (static_cast(busPtr))->SetBrightness(b); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_NEO_3: (static_cast(busPtr))->SetBrightness(b); break; + case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + #endif +// case I_32_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + #endif +// case I_32_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_400_3: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_NEO_3: (static_cast(busPtr))->SetBrightness(b); break; + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_400_3: (static_cast(busPtr))->SetLuminance(b); break; #endif - case I_32_RN_NEO_4: (static_cast(busPtr))->SetBrightness(b); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_NEO_4: (static_cast(busPtr))->SetBrightness(b); break; +// case I_32_BB_400_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_NEO_4: (static_cast(busPtr))->SetBrightness(b); break; + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; #endif - case I_32_RN_400_3: (static_cast(busPtr))->SetBrightness(b); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_400_3: (static_cast(busPtr))->SetBrightness(b); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_400_3: (static_cast(busPtr))->SetBrightness(b); break; + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; #endif - case I_32_RN_TM1_4: (static_cast(busPtr))->SetBrightness(b); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I0_TM1_4: (static_cast(busPtr))->SetBrightness(b); break; +// case I_32_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case I_32_I1_TM1_4: (static_cast(busPtr))->SetBrightness(b); break; + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; #endif +// case I_32_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; #endif - case I_HS_DOT_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_SS_DOT_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_HS_LPD_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_SS_LPD_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_HS_WS1_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_SS_WS1_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_HS_P98_3: (static_cast(busPtr))->SetBrightness(b); break; - case I_SS_P98_3: (static_cast(busPtr))->SetBrightness(b); break; + case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_HS_LPD_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_SS_LPD_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_HS_LPO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_SS_LPO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_HS_WS1_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_SS_WS1_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_HS_P98_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_SS_P98_3: (static_cast(busPtr))->SetLuminance(b); break; } - }; + } + static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { - RgbwColor col(0,0,0,0); + RgbwColor col(0,0,0,0); switch (busType) { case I_NONE: break; #ifdef ESP8266 @@ -582,55 +932,93 @@ class PolyBus { case I_8266_U1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_DM_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_BB_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + case I_8266_U1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + case I_8266_DM_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + case I_8266_BB_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + case I_8266_U0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + case I_8266_U1_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + case I_8266_DM_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + case I_8266_BB_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif +// case I_32_BB_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif +// case I_32_BB_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif +// case I_32_BB_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_RN_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + #endif + case I_32_RN_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + #endif +// case I_32_BB_UCS_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + #endif +// case I_32_BB_UCS_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_HS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_HS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_SS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_HS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_HS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; } - - #ifdef COLOR_ORDER_OVERRIDE - if (pix >= COO_MIN && pix < COO_MAX) co = COO_ORDER; - #endif - switch (co) - { + // upper nibble contains W swap information + uint8_t w = col.W; + switch (co >> 4) { + case 1: col.W = col.B; col.B = w; break; // swap W & B + case 2: col.W = col.G; col.G = w; break; // swap W & G + case 3: col.W = col.R; col.R = w; break; // swap W & R + } + switch (co & 0x0F) { // W G R B - case 0: return ((col.W << 24) | (col.G << 8) | (col.R << 16) | (col.B)); //0 = GRB, default + default: return ((col.W << 24) | (col.G << 8) | (col.R << 16) | (col.B)); //0 = GRB, default case 1: return ((col.W << 24) | (col.R << 8) | (col.G << 16) | (col.B)); //1 = RGB, common for WS2811 case 2: return ((col.W << 24) | (col.B << 8) | (col.R << 16) | (col.G)); //2 = BRG case 3: return ((col.W << 24) | (col.B << 8) | (col.G << 16) | (col.R)); //3 = RBG @@ -661,41 +1049,77 @@ class PolyBus { case I_8266_U1_TM1_4: delete (static_cast(busPtr)); break; case I_8266_DM_TM1_4: delete (static_cast(busPtr)); break; case I_8266_BB_TM1_4: delete (static_cast(busPtr)); break; + case I_8266_U0_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_U1_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_DM_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_BB_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_U0_UCS_3: delete (static_cast(busPtr)); break; + case I_8266_U1_UCS_3: delete (static_cast(busPtr)); break; + case I_8266_DM_UCS_3: delete (static_cast(busPtr)); break; + case I_8266_BB_UCS_3: delete (static_cast(busPtr)); break; + case I_8266_U0_UCS_4: delete (static_cast(busPtr)); break; + case I_8266_U1_UCS_4: delete (static_cast(busPtr)); break; + case I_8266_DM_UCS_4: delete (static_cast(busPtr)); break; + case I_8266_BB_UCS_4: delete (static_cast(busPtr)); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: delete (static_cast(busPtr)); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_3: delete (static_cast(busPtr)); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: delete (static_cast(busPtr)); break; #endif +// case I_32_BB_NEO_3: delete (static_cast(busPtr)); break; case I_32_RN_NEO_4: delete (static_cast(busPtr)); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: delete (static_cast(busPtr)); break; #endif +// case I_32_BB_NEO_4: delete (static_cast(busPtr)); break; case I_32_RN_400_3: delete (static_cast(busPtr)); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: delete (static_cast(busPtr)); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: delete (static_cast(busPtr)); break; #endif +// case I_32_BB_400_3: delete (static_cast(busPtr)); break; case I_32_RN_TM1_4: delete (static_cast(busPtr)); break; - #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_RN_TM2_3: delete (static_cast(busPtr)); break; + #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1_4: delete (static_cast(busPtr)); break; + case I_32_I0_TM2_3: delete (static_cast(busPtr)); break; #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_TM1_4: delete (static_cast(busPtr)); break; + case I_32_I1_TM2_3: delete (static_cast(busPtr)); break; + #endif + case I_32_RN_UCS_3: delete (static_cast(busPtr)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_3: delete (static_cast(busPtr)); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_3: delete (static_cast(busPtr)); break; #endif +// case I_32_BB_UCS_3: delete (static_cast(busPtr)); break; + case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_4: delete (static_cast(busPtr)); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_4: delete (static_cast(busPtr)); break; + #endif +// case I_32_BB_UCS_4: delete (static_cast(busPtr)); break; #endif case I_HS_DOT_3: delete (static_cast(busPtr)); break; case I_SS_DOT_3: delete (static_cast(busPtr)); break; case I_HS_LPD_3: delete (static_cast(busPtr)); break; case I_SS_LPD_3: delete (static_cast(busPtr)); break; + case I_HS_LPO_3: delete (static_cast(busPtr)); break; + case I_SS_LPO_3: delete (static_cast(busPtr)); break; case I_HS_WS1_3: delete (static_cast(busPtr)); break; case I_SS_WS1_3: delete (static_cast(busPtr)); break; case I_HS_P98_3: delete (static_cast(busPtr)); break; @@ -703,7 +1127,7 @@ class PolyBus { } } - //gives back the internal type index (I_XX_XXX_X above) for the input + //gives back the internal type index (I_XX_XXX_X above) for the input static uint8_t getI(uint8_t busType, uint8_t* pins, uint8_t num = 0) { if (!IS_DIGITAL(busType)) return I_NONE; if (IS_2PIN(busType)) { //SPI LED chips @@ -711,12 +1135,15 @@ class PolyBus { #ifdef ESP8266 if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true; #else - if(!num) isHSPI = true; // temporary hack to limit use of hardware SPI to a single SPI peripheral: only allow ESP32 hardware serial on segment 0 + // temporary hack to limit use of hardware SPI to a single SPI peripheral (HSPI): only allow ESP32 hardware serial on segment 0 + // SPI global variable is normally linked to VSPI on ESP32 (or FSPI C3, S3) + if (!num) isHSPI = true; #endif uint8_t t = I_NONE; switch (busType) { case TYPE_APA102: t = I_SS_DOT_3; break; case TYPE_LPD8806: t = I_SS_LPD_3; break; + case TYPE_LPD6803: t = I_SS_LPO_3; break; case TYPE_WS2801: t = I_SS_WS1_3; break; case TYPE_P9813: t = I_SS_P98_3; break; default: t=I_NONE; @@ -728,6 +1155,8 @@ class PolyBus { uint8_t offset = pins[0] -1; //for driver: 0 = uart0, 1 = uart1, 2 = dma, 3 = bitbang if (offset > 3) offset = 3; switch (busType) { + case TYPE_WS2812_1CH_X3: + case TYPE_WS2812_2CH_X3: case TYPE_WS2812_RGB: case TYPE_WS2812_WWA: return I_8266_U0_NEO_3 + offset; @@ -737,17 +1166,35 @@ class PolyBus { return I_8266_U0_400_3 + offset; case TYPE_TM1814: return I_8266_U0_TM1_4 + offset; + case TYPE_TM1829: + return I_8266_U0_TM2_3 + offset; + case TYPE_UCS8903: + return I_8266_U0_UCS_3 + offset; + case TYPE_UCS8904: + return I_8266_U0_UCS_4 + offset; } #else //ESP32 uint8_t offset = 0; //0 = RMT (num 0-7) 8 = I2S0 9 = I2S1 - #ifndef CONFIG_IDF_TARGET_ESP32S2 + #if defined(CONFIG_IDF_TARGET_ESP32S2) + // ESP32-S2 only has 4 RMT channels + if (num > 4) return I_NONE; + if (num > 3) offset = 1; // only one I2S + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + // On ESP32-C3 only the first 2 RMT channels are usable for transmitting + if (num > 1) return I_NONE; + //if (num > 1) offset = 1; // I2S not supported yet (only 1 I2S) + #elif defined(CONFIG_IDF_TARGET_ESP32S3) + // On ESP32-S3 only the first 4 RMT channels are usable for transmitting + if (num > 3) return I_NONE; + //if (num > 3) offset = num -4; // I2S not supported yet + #else + // standard ESP32 has 8 RMT and 2 I2S channels if (num > 9) return I_NONE; if (num > 7) offset = num -7; - #else //ESP32 S2 only has 4 RMT channels - if (num > 5) return I_NONE; - if (num > 4) offset = num -4; #endif switch (busType) { + case TYPE_WS2812_1CH_X3: + case TYPE_WS2812_2CH_X3: case TYPE_WS2812_RGB: case TYPE_WS2812_WWA: return I_32_RN_NEO_3 + offset; @@ -757,6 +1204,12 @@ class PolyBus { return I_32_RN_400_3 + offset; case TYPE_TM1814: return I_32_RN_TM1_4 + offset; + case TYPE_TM1829: + return I_32_RN_TM2_3 + offset; + case TYPE_UCS8903: + return I_32_RN_UCS_3 + offset; + case TYPE_UCS8904: + return I_32_RN_UCS_4 + offset; } #endif } @@ -764,4 +1217,4 @@ class PolyBus { } }; -#endif +#endif \ No newline at end of file diff --git a/wled00/button.cpp b/wled00/button.cpp index 60fccfc3c5..c1d0337667 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -18,18 +18,21 @@ void shortPressAction(uint8_t b) if (!macroButton[b]) { switch (b) { case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break; - case 1: ++effectCurrent %= strip.getModeCount(); colorUpdated(CALL_MODE_BUTTON); break; + case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break; } } else { + unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); } +#ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { char subuf[64]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "short"); } +#endif } void longPressAction(uint8_t b) @@ -40,15 +43,18 @@ void longPressAction(uint8_t b) case 1: bri += 8; stateUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action } } else { + unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); } +#ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { char subuf[64]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "long"); } +#endif } void doublePressAction(uint8_t b) @@ -59,15 +65,18 @@ void doublePressAction(uint8_t b) case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; } } else { + unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET); } +#ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { char subuf[64]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "double"); } +#endif } bool isButtonPressed(uint8_t i) @@ -105,20 +114,21 @@ void handleSwitch(uint8_t b) } if (buttonLongPressed[b] == buttonPressedBefore[b]) return; - + if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) if (!buttonPressedBefore[b]) { // on -> off if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); else { //turn on if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);} - } + } } else { // off -> on if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); else { //turn off if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);} - } + } } +#ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { char subuf[64]; @@ -126,26 +136,48 @@ void handleSwitch(uint8_t b) else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on"); } +#endif buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state } } +#define ANALOG_BTN_READ_CYCLE 250 // min time between two analog reading cycles +#define STRIP_WAIT_TIME 6 // max wait time in case of strip.isUpdating() +#define POT_SMOOTHING 0.25f // smoothing factor for raw potentiometer readings +#define POT_SENSITIVITY 4 // changes below this amount are noise (POT scratching, or ADC noise) + void handleAnalog(uint8_t b) { - static uint8_t oldRead[WLED_MAX_BUTTONS]; + static uint8_t oldRead[WLED_MAX_BUTTONS] = {0}; + static float filteredReading[WLED_MAX_BUTTONS] = {0.0f}; + uint16_t rawReading; // raw value from analogRead, scaled to 12bit + #ifdef ESP8266 - uint16_t aRead = analogRead(A0) >> 2; // convert 10bit read to 8bit + rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit #else - uint16_t aRead = analogRead(btnPin[b]) >> 4; // convert 12bit read to 8bit + 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 #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] + uint16_t 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 (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead; // remove noise & reduce frequency of UI updates - aRead &= 0xFC; + if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading + + // Un-comment the next lines if you still see flickering related to potentiometer + // This waits until strip finishes updating (why: strip was not updating at the start of handleButton() but may have started during analogRead()?) + //unsigned long wait_started = millis(); + //while(strip.isUpdating() && (millis() - wait_started < STRIP_WAIT_TIME)) { + // delay(1); + //} - if (oldRead[b] == aRead) return; // no change in reading oldRead[b] = aRead; // if no macro for "short press" and "long press" is defined use brightness control @@ -156,7 +188,7 @@ void handleAnalog(uint8_t b) if (aRead == 0) { briLast = bri; bri = 0; - } else{ + } else { bri = aRead; } } else if (macroDoublePress[b] == 249) { @@ -168,17 +200,18 @@ void handleAnalog(uint8_t b) } else if (macroDoublePress[b] == 247) { // selected palette effectPalette = map(aRead, 0, 252, 0, strip.getPaletteCount()-1); + effectPalette = constrain(effectPalette, 0, strip.getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result } else if (macroDoublePress[b] == 200) { // primary color, hue, full saturation colorHStoRGB(aRead*256,255,col); } else { // otherwise use "double press" for segment selection - WS2812FX::Segment& seg = strip.getSegment(macroDoublePress[b]); + Segment& seg = strip.getSegment(macroDoublePress[b]); if (aRead == 0) { - seg.setOption(SEG_OPTION_ON, 0); // off + seg.setOption(SEG_OPTION_ON, false); // off (use transition) } else { - seg.setOpacity(aRead, macroDoublePress[b]); - seg.setOption(SEG_OPTION_ON, 1); + 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); @@ -193,8 +226,12 @@ void handleAnalog(uint8_t b) void handleButton() { - static unsigned long lastRead = 0UL; - bool analog = false; + static unsigned long lastAnalogRead = 0UL; + static unsigned long lastRun = 0UL; + unsigned long now = millis(); + + 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 (uint8_t b=0; b 250) { // button is not a button but a potentiometer - analog = true; - handleAnalog(b); continue; + if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer + if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) { + handleAnalog(b); + } + continue; } - //button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0) + // 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_PIR_SENSOR) { - handleSwitch(b); continue; + handleSwitch(b); + continue; } - //momentary button logic - if (isButtonPressed(b)) { //pressed + // momentary button logic + 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) + continue; + } - if (!buttonPressedBefore[b]) buttonPressedTime[b] = millis(); + if (!buttonPressedBefore[b]) buttonPressedTime[b] = now; buttonPressedBefore[b] = true; - if (millis() - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press + if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press if (!buttonLongPressed[b]) longPressAction(b); else if (b) { //repeatable action (~3 times per s) on button > 0 longPressAction(b); - buttonPressedTime[b] = millis() - WLED_LONG_REPEATED_ACTION; //300ms + buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //333ms } buttonLongPressed[b] = true; } } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + long dur = now - buttonPressedTime[b]; - long dur = millis() - buttonPressedTime[b]; - if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} //too short "press", debounce + // 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 + 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 (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released) if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds WLED_FS.format(); + #ifdef WLED_ADD_EEPROM_SUPPORT clearEEPROM(); + #endif doReboot = true; } else { WLED::instance().initAP(true); } } else if (!buttonLongPressed[b]) { //short press + //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 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] = millis(); + buttonWaitTime[b] = now; } } } @@ -261,24 +319,52 @@ void handleButton() } //if 350ms elapsed since last short press release it is a short press - if (buttonWaitTime[b] && millis() - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) { + if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) { buttonWaitTime[b] = 0; shortPressAction(b); } } - if (analog) lastRead = millis(); + if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) { + lastAnalogRead = now; + } +} + +// If enabled, RMT idle level is set to HIGH when off +// to prevent leakage current when using an N-channel MOSFET to toggle LED power +#ifdef ESP32_DATA_IDLE_HIGH +void esp32RMTInvertIdle() +{ + bool idle_out; + for (uint8_t u = 0; u < busses.getNumBusses(); u++) + { + if (u > 7) return; // only 8 RMT channels, TODO: ESP32 variants have less RMT channels + Bus *bus = busses.getBus(u); + if (!bus || bus->getLength()==0 || !IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType())) continue; + //assumes that bus number to rmt channel mapping stays 1:1 + rmt_channel_t ch = static_cast(u); + rmt_idle_level_t lvl; + rmt_get_idle_level(ch, &idle_out, &lvl); + if (lvl == RMT_IDLE_LEVEL_HIGH) lvl = RMT_IDLE_LEVEL_LOW; + else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH; + else continue; + rmt_set_idle_level(ch, idle_out, lvl); + } } +#endif void handleIO() { handleButton(); - + //set relay when LEDs turn on if (strip.getBrightness()) { lastOnTime = millis(); if (offMode) { + #ifdef ESP32_DATA_IDLE_HIGH + esp32RMTInvertIdle(); + #endif if (rlyPin>=0) { pinMode(rlyPin, OUTPUT); digitalWrite(rlyPin, rlyMde); @@ -290,13 +376,16 @@ void handleIO() if (!offMode) { #ifdef ESP8266 // turn off built-in LED if strip is turned off - // this will break digital bus so will need to be reinitialised on On + // this will break digital bus so will need to be re-initialised on On PinOwner ledPinOwner = pinManager.getPinOwner(LED_BUILTIN); if (!strip.isOffRefreshRequired() && (ledPinOwner == PinOwner::None || ledPinOwner == PinOwner::BusDigital)) { pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); } #endif + #ifdef ESP32_DATA_IDLE_HIGH + esp32RMTInvertIdle(); + #endif if (rlyPin>=0) { pinMode(rlyPin, OUTPUT); digitalWrite(rlyPin, !rlyMde); @@ -304,4 +393,4 @@ void handleIO() } offMode = true; } -} \ No newline at end of file +} diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index cdc80d5d43..4bcd28bc0a 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -31,6 +31,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { getStringFromJson(cmDNS, id[F("mdns")], 33); getStringFromJson(serverDescription, id[F("name")], 33); getStringFromJson(alexaInvocationName, id[F("inv")], 33); +#ifdef WLED_ENABLE_SIMPLE_UI + CJSON(simplifiedUI, id[F("sui")]); +#endif JsonObject nw_ins_0 = doc["nw"]["ins"][0]; getStringFromJson(clientSSID, nw_ins_0[F("ssid")], 33); @@ -61,7 +64,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (apHide > 1) apHide = 1; CJSON(apBehavior, ap[F("behav")]); - + /* JsonArray ap_ip = ap["ip"]; for (byte i = 0; i < 4; i++) { @@ -71,7 +74,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { noWifiSleep = doc[F("wifi")][F("sleep")] | !noWifiSleep; // inverted noWifiSleep = !noWifiSleep; - //int wifi_phy = doc[F("wifi")][F("phy")]; //force phy mode n? + force802_3g = doc[F("wifi")][F("phy")] | force802_3g; //force phy mode g? JsonObject hw = doc[F("hw")]; @@ -80,23 +83,61 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(strip.ablMilliampsMax, hw_led[F("maxpwr")]); CJSON(strip.milliampsPerLed, hw_led[F("ledma")]); - CJSON(strip.autoWhiteMode, hw_led[F("rgbwm")]); - Bus::setAutoWhiteMode(strip.autoWhiteMode); - strip.fixInvalidSegments(); // refreshes segment light capabilities (in case auto white mode changed) + Bus::setGlobalAWMode(hw_led[F("rgbwm")] | AW_GLOBAL_DISABLED); CJSON(correctWB, hw_led["cct"]); CJSON(cctFromRgb, hw_led[F("cr")]); CJSON(strip.cctBlending, hw_led[F("cb")]); Bus::setCCTBlend(strip.cctBlending); strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS + CJSON(useGlobalLedBuffer, hw_led[F("ld")]); + + #ifndef WLED_DISABLE_2D + // 2D Matrix Settings + JsonObject matrix = hw_led[F("matrix")]; + if (!matrix.isNull()) { + strip.isMatrix = true; + CJSON(strip.panels, matrix[F("mpc")]); + strip.panel.clear(); + JsonArray panels = matrix[F("panels")]; + uint8_t s = 0; + if (!panels.isNull()) { + strip.panel.reserve(max(1U,min((size_t)strip.panels,(size_t)WLED_MAX_PANELS))); // pre-allocate memory for panels + for (JsonObject pnl : panels) { + WS2812FX::Panel p; + CJSON(p.bottomStart, pnl["b"]); + CJSON(p.rightStart, pnl["r"]); + CJSON(p.vertical, pnl["v"]); + CJSON(p.serpentine, pnl["s"]); + CJSON(p.xOffset, pnl["x"]); + CJSON(p.yOffset, pnl["y"]); + CJSON(p.height, pnl["h"]); + CJSON(p.width, pnl["w"]); + strip.panel.push_back(p); + if (++s >= WLED_MAX_PANELS || s >= strip.panels) break; // max panels reached + } + } else { + // fallback + WS2812FX::Panel p; + strip.panels = 1; + p.height = p.width = 8; + p.xOffset = p.yOffset = 0; + p.options = 0; + strip.panel.push_back(p); + } + // cannot call strip.setUpMatrix() here due to already locked JSON buffer + } + #endif JsonArray ins = hw_led["ins"]; - + if (fromFS || !ins.isNull()) { uint8_t s = 0; // bus iterator if (fromFS) busses.removeAll(); // can't safely manipulate busses directly in network callback - uint32_t mem = 0; + uint32_t mem = 0, globalBufMem = 0; + uint16_t maxlen = 0; + bool busesChanged = false; for (JsonObject elm : ins) { - if (s >= WLED_MAX_BUSSES) break; + if (s >= WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES) break; uint8_t pins[5] = {255, 255, 255, 255, 255}; JsonArray pinArr = elm["pin"]; if (pinArr.size() == 0) continue; @@ -108,28 +149,35 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } uint16_t length = elm["len"] | 1; - uint8_t colorOrder = (int)elm[F("order")]; + uint8_t colorOrder = (int)elm[F("order")]; // contains white channel swap option in upper nibble uint8_t skipFirst = elm[F("skip")]; uint16_t start = elm["start"] | 0; if (length==0 || start + length > MAX_LEDS) continue; // zero length or we reached max. number of LEDs, just stop uint8_t ledType = elm["type"] | TYPE_WS2812_RGB; bool reversed = elm["rev"]; bool refresh = elm["ref"] | false; + uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM (not yet implemented fully) ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh - s++; + uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY; if (fromFS) { - BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst); + BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer); mem += BusManager::memUsage(bc); - if (mem <= MAX_LED_MEMORY && busses.getNumBusses() <= WLED_MAX_BUSSES) busses.add(bc); // finalization will be done in WLED::beginStrip() + if (useGlobalLedBuffer && start + length > maxlen) { + maxlen = start + length; + globalBufMem = maxlen * 4; + } + if (mem + globalBufMem <= MAX_LED_MEMORY) if (busses.add(bc) == -1) break; // finalization will be done in WLED::beginStrip() } else { if (busConfigs[s] != nullptr) delete busConfigs[s]; - busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst); - doInitBusses = true; + busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer); + busesChanged = true; } + s++; } + doInitBusses = busesChanged; // finalization done in beginStrip() } - if (hw_led["rev"]) busses.getBus(0)->reversed = true; //set 0.11 global reversed setting for first bus + if (hw_led["rev"]) busses.getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus // read color order map configuration JsonArray hw_com = hw[F("com")]; @@ -149,15 +197,43 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // read multiple button configuration JsonObject btn_obj = hw["btn"]; + bool pull = btn_obj[F("pull")] | (!disablePullUp); // if true, pullup is enabled + disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj[F("ins")]; if (!hw_btn_ins.isNull()) { + for (uint8_t b = 0; b < WLED_MAX_BUTTONS; b++) { // deallocate existing button pins + pinManager.deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button + } uint8_t s = 0; for (JsonObject btn : hw_btn_ins) { CJSON(buttonType[s], btn["type"]); int8_t pin = btn["pin"][0] | -1; if (pin > -1 && pinManager.allocatePin(pin, false, PinOwner::Button)) { btnPin[s] = pin; - pinMode(btnPin[s], INPUT_PULLUP); + #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)) && (digitalPinToAnalogChannel(btnPin[s]) < 0)) + { + // not an ADC analog pin + DEBUG_PRINT(F("PIN ALLOC error: GPIO")); DEBUG_PRINT(btnPin[s]); + DEBUG_PRINT(F("for analog button #")); DEBUG_PRINT(s); + DEBUG_PRINTLN(F(" is not an analog pin!")); + btnPin[s] = -1; + pinManager.deallocatePin(pin,PinOwner::Button); + } + else + #endif + { + 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 + } + } } else { btnPin[s] = -1; } @@ -181,7 +257,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // relies upon only being called once with fromFS == true, which is currently true. uint8_t s = 0; if (pinManager.allocatePin(btnPin[0], false, PinOwner::Button)) { // initialized to #define value BTNPIN, or zero if not defined(!) - ++s; // do not clear default button if allocated successfully + ++s; // do not clear default button if allocated successfully } for (; s -2) { + pinManager.deallocatePin(irPin, PinOwner::IR); if (pinManager.allocatePin(hw_ir_pin, false, PinOwner::IR)) { irPin = hw_ir_pin; } else { @@ -209,6 +286,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject relay = hw[F("relay")]; int hw_relay_pin = relay["pin"] | -2; if (hw_relay_pin > -2) { + pinManager.deallocatePin(rlyPin, PinOwner::Relay); if (pinManager.allocatePin(hw_relay_pin,true, PinOwner::Relay)) { rlyPin = hw_relay_pin; pinMode(rlyPin, OUTPUT); @@ -224,6 +302,38 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (serialBaud < 96 || serialBaud > 40000) serialBaud = 1152; updateBaudRate(serialBaud *100); + JsonArray hw_if_i2c = hw[F("if")][F("i2c-pin")]; + CJSON(i2c_sda, hw_if_i2c[0]); + CJSON(i2c_scl, hw_if_i2c[1]); + PinManagerPinType i2c[2] = { { i2c_sda, true }, { i2c_scl, true } }; + if (i2c_scl >= 0 && i2c_sda >= 0 && pinManager.allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { + #ifdef ESP32 + if (!Wire.setPins(i2c_sda, i2c_scl)) { i2c_scl = i2c_sda = -1; } // this will fail if Wire is initialised (Wire.begin() called prior) + else Wire.begin(); + #else + Wire.begin(i2c_sda, i2c_scl); + #endif + } else { + i2c_sda = -1; + i2c_scl = -1; + } + JsonArray hw_if_spi = hw[F("if")][F("spi-pin")]; + CJSON(spi_mosi, hw_if_spi[0]); + CJSON(spi_sclk, hw_if_spi[1]); + CJSON(spi_miso, hw_if_spi[2]); + PinManagerPinType spi[3] = { { spi_mosi, true }, { spi_miso, true }, { spi_sclk, true } }; + if (spi_mosi >= 0 && spi_sclk >= 0 && pinManager.allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { + #ifdef ESP32 + SPI.begin(spi_sclk, spi_miso, spi_mosi); // SPI global uses VSPI on ESP32 and FSPI on C3, S3 + #else + SPI.begin(); + #endif + } else { + spi_mosi = -1; + spi_miso = -1; + spi_sclk = -1; + } + //int hw_status_pin = hw[F("status")]["pin"]; // -1 JsonObject light = doc[F("light")]; @@ -231,18 +341,29 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(strip.paletteBlend, light[F("pal-mode")]); CJSON(autoSegments, light[F("aseg")]); + CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.8 float light_gc_bri = light["gc"]["bri"]; - float light_gc_col = light["gc"]["col"]; // 2.8 - if (light_gc_bri > 1.5) strip.gammaCorrectBri = true; - else if (light_gc_bri > 0.5) strip.gammaCorrectBri = false; - if (light_gc_col > 1.5) strip.gammaCorrectCol = true; - else if (light_gc_col > 0.5) strip.gammaCorrectCol = false; + float light_gc_col = light["gc"]["col"]; + if (light_gc_bri > 1.0f) gammaCorrectBri = true; + else gammaCorrectBri = false; + if (light_gc_col > 1.0f) gammaCorrectCol = true; + else gammaCorrectCol = false; + if (gammaCorrectVal > 1.0f && gammaCorrectVal <= 3) { + if (gammaCorrectVal != 2.8f) NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); + } else { + gammaCorrectVal = 1.0f; // no gamma correction + gammaCorrectBri = false; + gammaCorrectCol = false; + } JsonObject light_tr = light["tr"]; CJSON(fadeTransition, light_tr["mode"]); + CJSON(modeBlending, light_tr["fx"]); int tdd = light_tr["dur"] | -1; - if (tdd >= 0) transitionDelayDefault = tdd * 100; + if (tdd >= 0) transitionDelay = transitionDelayDefault = tdd * 100; + strip.setTransition(fadeTransition ? transitionDelayDefault : 0); CJSON(strip.paletteFade, light_tr["pal"]); + CJSON(randomPaletteChangeTime, light_tr[F("rpc")]); JsonObject light_nl = light["nl"]; CJSON(nightlightMode, light_nl["mode"]); @@ -282,8 +403,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(notifyAlexa, if_sync_send["va"]); CJSON(notifyHue, if_sync_send["hue"]); CJSON(notifyMacro, if_sync_send["macro"]); - CJSON(notifyTwice, if_sync_send[F("twice")]); CJSON(syncGroups, if_sync_send["grp"]); + if (if_sync_send[F("twice")]) udpNumRetries = 1; // import setting from 0.13 and earlier + CJSON(udpNumRetries, if_sync_send["ret"]); JsonObject if_nodes = interfaces["nodes"]; CJSON(nodeListEnabled, if_nodes[F("list")]); @@ -291,6 +413,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject if_live = interfaces["live"]; CJSON(receiveDirect, if_live["en"]); + CJSON(useMainSegmentOnly, if_live[F("mso")]); CJSON(e131Port, if_live["port"]); // 5568 if (e131Port == DDP_DEFAULT_PORT) e131Port = E131_DEFAULT_PORT; // prevent double DDP port allocation CJSON(e131Multicast, if_live[F("mc")]); @@ -299,6 +422,11 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(e131Universe, if_live_dmx[F("uni")]); CJSON(e131SkipOutOfSequence, if_live_dmx[F("seqskip")]); CJSON(DMXAddress, if_live_dmx[F("addr")]); + if (!DMXAddress || DMXAddress > 510) DMXAddress = 1; + CJSON(DMXSegmentSpacing, if_live_dmx[F("dss")]); + if (DMXSegmentSpacing > 150) DMXSegmentSpacing = 0; + CJSON(e131Priority, if_live_dmx[F("e131prio")]); + if (e131Priority > 200) e131Priority = 200; CJSON(DMXMode, if_live_dmx["mode"]); tdd = if_live[F("timeout")] | -1; @@ -312,30 +440,29 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(macroAlexaOn, interfaces["va"]["macros"][0]); CJSON(macroAlexaOff, interfaces["va"]["macros"][1]); -#ifndef WLED_DISABLE_BLYNK - const char* apikey = interfaces["blynk"][F("token")] | "Hidden"; - tdd = strnlen(apikey, 36); - if (tdd > 20 || tdd == 0) - getStringFromJson(blynkApiKey, apikey, 36); //normally not present due to security - - JsonObject if_blynk = interfaces["blynk"]; - getStringFromJson(blynkHost, if_blynk[F("host")], 33); - CJSON(blynkPort, if_blynk["port"]); -#endif + CJSON(alexaNumPresets, interfaces["va"]["p"]); #ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; CJSON(mqttEnabled, if_mqtt["en"]); - getStringFromJson(mqttServer, if_mqtt[F("broker")], 33); + getStringFromJson(mqttServer, if_mqtt[F("broker")], MQTT_MAX_SERVER_LEN+1); CJSON(mqttPort, if_mqtt["port"]); // 1883 getStringFromJson(mqttUser, if_mqtt[F("user")], 41); getStringFromJson(mqttPass, if_mqtt["psk"], 65); //normally not present due to security getStringFromJson(mqttClientID, if_mqtt[F("cid")], 41); - getStringFromJson(mqttDeviceTopic, if_mqtt[F("topics")][F("device")], 33); // "wled/test" - getStringFromJson(mqttGroupTopic, if_mqtt[F("topics")][F("group")], 33); // "" + getStringFromJson(mqttDeviceTopic, if_mqtt[F("topics")][F("device")], MQTT_MAX_TOPIC_LEN+1); // "wled/test" + getStringFromJson(mqttGroupTopic, if_mqtt[F("topics")][F("group")], MQTT_MAX_TOPIC_LEN+1); // "" + CJSON(retainMqttMsg, if_mqtt[F("rtn")]); #endif +#ifndef WLED_DISABLE_ESPNOW + JsonObject remote = doc["remote"]; + CJSON(enable_espnow_remote, remote[F("remote_enabled")]); + getStringFromJson(linked_remote, remote[F("linked_remote")], 13); +#endif + + #ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces["hue"]; CJSON(huePollingEnabled, if_hue["en"]); @@ -390,7 +517,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint8_t it = 0; for (JsonObject timer : timers) { if (it > 9) break; - if (it<8 && timer[F("hour")]==255) it=8; // hour==255 -> sunrise/sunset + if (it<8 && timer[F("hour")]==255) it=8; // hour==255 -> sunrise/sunset CJSON(timerHours[it], timer[F("hour")]); CJSON(timerMinutes[it], timer["min"]); CJSON(timerMacro[it], timer["macro"]); @@ -455,29 +582,41 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } if (fromFS) return needsSave; + // if from /json/cfg doReboot = doc[F("rb")] | doReboot; + if (doInitBusses) return false; // no save needed, will do after bus init in wled.cpp loop return (doc["sv"] | true); } void deserializeConfigFromFS() { bool success = deserializeConfigSec(); if (!success) { //if file does not exist, try reading from EEPROM + #ifdef WLED_ADD_EEPROM_SUPPORT deEEPSettings(); return; + #endif } - #ifdef WLED_USE_DYNAMIC_JSON - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - #else if (!requestJSONBufferLock(1)) return; - #endif DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); success = readObjectFromFile("/cfg.json", nullptr, &doc); - if (!success) { //if file does not exist, try reading from EEPROM - deEEPSettings(); + if (!success) { // if file does not exist, optionally try reading from EEPROM and then save defaults to FS releaseJSONBufferLock(); + #ifdef WLED_ADD_EEPROM_SUPPORT + deEEPSettings(); + #endif + + // save default values to /cfg.json + // call readFromConfig() with an empty object so that usermods can initialize to defaults prior to saving + JsonObject empty = JsonObject(); + usermods.readFromConfig(empty); + serializeConfig(); + // init Ethernet (in case default type is set at compile time) + #ifdef WLED_USE_ETHERNET + WLED::instance().initEthernet(); + #endif return; } @@ -486,7 +625,7 @@ void deserializeConfigFromFS() { bool needsSave = deserializeConfig(doc.as(), true); releaseJSONBufferLock(); - if (needsSave) serializeConfig(); // usermods required new prameters + if (needsSave) serializeConfig(); // usermods required new parameters } void serializeConfig() { @@ -494,11 +633,7 @@ void serializeConfig() { DEBUG_PRINTLN(F("Writing settings to /cfg.json...")); - #ifdef WLED_USE_DYNAMIC_JSON - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - #else if (!requestJSONBufferLock(2)) return; - #endif JsonArray rev = doc.createNestedArray("rev"); rev.add(1); //major settings revision @@ -510,6 +645,9 @@ void serializeConfig() { id[F("mdns")] = cmDNS; id[F("name")] = serverDescription; id[F("inv")] = alexaInvocationName; +#ifdef WLED_ENABLE_SIMPLE_UI + id[F("sui")] = simplifiedUI; +#endif JsonObject nw = doc.createNestedObject("nw"); @@ -544,7 +682,7 @@ void serializeConfig() { JsonObject wifi = doc.createNestedObject("wifi"); wifi[F("sleep")] = !noWifiSleep; - //wifi[F("phy")] = 1; + wifi[F("phy")] = force802_3g; #ifdef WLED_USE_ETHERNET JsonObject ethernet = doc.createNestedObject("eth"); @@ -580,7 +718,28 @@ void serializeConfig() { hw_led[F("cr")] = cctFromRgb; hw_led[F("cb")] = strip.cctBlending; hw_led["fps"] = strip.getTargetFps(); - hw_led[F("rgbwm")] = strip.autoWhiteMode; + hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override + hw_led[F("ld")] = useGlobalLedBuffer; + + #ifndef WLED_DISABLE_2D + // 2D Matrix Settings + if (strip.isMatrix) { + JsonObject matrix = hw_led.createNestedObject(F("matrix")); + matrix[F("mpc")] = strip.panels; + JsonArray panels = matrix.createNestedArray(F("panels")); + for (uint8_t i=0; igetPins(pins); for (uint8_t i = 0; i < nPins; i++) ins_pin.add(pins[i]); ins[F("order")] = bus->getColorOrder(); - ins["rev"] = bus->reversed; + ins["rev"] = bus->isReversed(); ins[F("skip")] = bus->skippedLeds(); ins["type"] = bus->getType() & 0x7F; ins["ref"] = bus->isOffRefreshRequired(); - //ins[F("rgbw")] = bus->isRgbw(); + ins[F("rgbwm")] = bus->getAutoWhiteMode(); + ins[F("freq")] = bus->getFrequency(); } JsonArray hw_com = hw.createNestedArray(F("com")); @@ -617,6 +777,7 @@ void serializeConfig() { // button(s) JsonObject hw_btn = hw.createNestedObject("btn"); hw_btn["max"] = WLED_MAX_BUTTONS; // just information about max number of buttons (not actually used) + hw_btn[F("pull")] = !disablePullUp; JsonArray hw_btn_ins = hw_btn.createNestedArray("ins"); // configuration for all buttons @@ -645,6 +806,15 @@ void serializeConfig() { hw[F("baud")] = serialBaud; + JsonObject hw_if = hw.createNestedObject(F("if")); + JsonArray hw_if_i2c = hw_if.createNestedArray("i2c-pin"); + hw_if_i2c.add(i2c_sda); + hw_if_i2c.add(i2c_scl); + JsonArray hw_if_spi = hw_if.createNestedArray("spi-pin"); + hw_if_spi.add(spi_mosi); + hw_if_spi.add(spi_sclk); + hw_if_spi.add(spi_miso); + //JsonObject hw_status = hw.createNestedObject("status"); //hw_status["pin"] = -1; @@ -654,13 +824,16 @@ void serializeConfig() { light[F("aseg")] = autoSegments; JsonObject light_gc = light.createNestedObject("gc"); - light_gc["bri"] = (strip.gammaCorrectBri) ? 2.8 : 1.0; - light_gc["col"] = (strip.gammaCorrectCol) ? 2.8 : 1.0; + light_gc["bri"] = (gammaCorrectBri) ? gammaCorrectVal : 1.0f; // keep compatibility + light_gc["col"] = (gammaCorrectCol) ? gammaCorrectVal : 1.0f; // keep compatibility + light_gc["val"] = gammaCorrectVal; JsonObject light_tr = light.createNestedObject("tr"); light_tr["mode"] = fadeTransition; + light_tr["fx"] = modeBlending; light_tr["dur"] = transitionDelayDefault / 100; light_tr["pal"] = strip.paletteFade; + light_tr[F("rpc")] = randomPaletteChangeTime; JsonObject light_nl = light.createNestedObject("nl"); light_nl["mode"] = nightlightMode; @@ -693,8 +866,8 @@ void serializeConfig() { if_sync_send["va"] = notifyAlexa; if_sync_send["hue"] = notifyHue; if_sync_send["macro"] = notifyMacro; - if_sync_send[F("twice")] = notifyTwice; if_sync_send["grp"] = syncGroups; + if_sync_send["ret"] = udpNumRetries; JsonObject if_nodes = interfaces.createNestedObject("nodes"); if_nodes[F("list")] = nodeListEnabled; @@ -702,13 +875,16 @@ void serializeConfig() { JsonObject if_live = interfaces.createNestedObject("live"); if_live["en"] = receiveDirect; + if_live[F("mso")] = useMainSegmentOnly; if_live["port"] = e131Port; if_live[F("mc")] = e131Multicast; JsonObject if_live_dmx = if_live.createNestedObject("dmx"); if_live_dmx[F("uni")] = e131Universe; if_live_dmx[F("seqskip")] = e131SkipOutOfSequence; + if_live_dmx[F("e131prio")] = e131Priority; if_live_dmx[F("addr")] = DMXAddress; + if_live_dmx[F("dss")] = DMXSegmentSpacing; if_live_dmx["mode"] = DMXMode; if_live[F("timeout")] = realtimeTimeoutMs / 100; @@ -716,6 +892,7 @@ void serializeConfig() { if_live[F("no-gc")] = arlsDisableGammaCorrection; if_live[F("offset")] = arlsOffset; +#ifndef WLED_DISABLE_ALEXA JsonObject if_va = interfaces.createNestedObject("va"); if_va[F("alexa")] = alexaEnabled; @@ -723,11 +900,7 @@ void serializeConfig() { if_va_macros.add(macroAlexaOn); if_va_macros.add(macroAlexaOff); -#ifndef WLED_DISABLE_BLYNK - JsonObject if_blynk = interfaces.createNestedObject("blynk"); - if_blynk[F("token")] = strlen(blynkApiKey) ? "Hidden":""; - if_blynk[F("host")] = blynkHost; - if_blynk["port"] = blynkPort; + if_va["p"] = alexaNumPresets; #endif #ifdef WLED_ENABLE_MQTT @@ -738,12 +911,20 @@ void serializeConfig() { if_mqtt[F("user")] = mqttUser; if_mqtt[F("pskl")] = strlen(mqttPass); if_mqtt[F("cid")] = mqttClientID; + if_mqtt[F("rtn")] = retainMqttMsg; JsonObject if_mqtt_topics = if_mqtt.createNestedObject(F("topics")); if_mqtt_topics[F("device")] = mqttDeviceTopic; if_mqtt_topics[F("group")] = mqttGroupTopic; #endif +#ifndef WLED_DISABLE_ESPNOW + JsonObject remote = doc.createNestedObject(F("remote")); + remote[F("remote_enabled")] = enable_espnow_remote; + remote[F("linked_remote")] = linked_remote; +#endif + + #ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces.createNestedObject("hue"); if_hue["en"] = huePollingEnabled; @@ -836,17 +1017,15 @@ void serializeConfig() { if (f) serializeJson(doc, f); f.close(); releaseJSONBufferLock(); + + doSerializeConfig = false; } //settings in /wsec.json, not accessible via webserver, for passwords and tokens bool deserializeConfigSec() { DEBUG_PRINTLN(F("Reading settings from /wsec.json...")); - #ifdef WLED_USE_DYNAMIC_JSON - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - #else if (!requestJSONBufferLock(3)) return false; - #endif bool success = readObjectFromFile("/wsec.json", nullptr, &doc); if (!success) { @@ -860,14 +1039,7 @@ bool deserializeConfigSec() { JsonObject ap = doc["ap"]; getStringFromJson(apPass, ap["psk"] , 65); - JsonObject interfaces = doc["if"]; - -#ifndef WLED_DISABLE_BLYNK - const char* apikey = interfaces["blynk"][F("token")] | "Hidden"; - int tdd = strnlen(apikey, 36); - if (tdd > 20 || tdd == 0) - getStringFromJson(blynkApiKey, apikey, 36); -#endif + [[maybe_unused]] JsonObject interfaces = doc["if"]; #ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; @@ -878,6 +1050,9 @@ bool deserializeConfigSec() { getStringFromJson(hueApiKey, interfaces["hue"][F("key")], 47); #endif + getStringFromJson(settingsPIN, doc["pin"], 5); + correctPIN = !strlen(settingsPIN); + JsonObject ota = doc["ota"]; getStringFromJson(otaPass, ota[F("pwd")], 33); CJSON(otaLock, ota[F("lock")]); @@ -891,11 +1066,7 @@ bool deserializeConfigSec() { void serializeConfigSec() { DEBUG_PRINTLN(F("Writing settings to /wsec.json...")); - #ifdef WLED_USE_DYNAMIC_JSON - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - #else if (!requestJSONBufferLock(4)) return; - #endif JsonObject nw = doc.createNestedObject("nw"); @@ -907,11 +1078,7 @@ void serializeConfigSec() { JsonObject ap = doc.createNestedObject("ap"); ap["psk"] = apPass; - JsonObject interfaces = doc.createNestedObject("if"); -#ifndef WLED_DISABLE_BLYNK - JsonObject if_blynk = interfaces.createNestedObject("blynk"); - if_blynk[F("token")] = blynkApiKey; -#endif + [[maybe_unused]] JsonObject interfaces = doc.createNestedObject("if"); #ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); if_mqtt["psk"] = mqttPass; @@ -921,6 +1088,8 @@ void serializeConfigSec() { if_hue[F("key")] = hueApiKey; #endif + doc["pin"] = settingsPIN; + JsonObject ota = doc.createNestedObject("ota"); ota[F("pwd")] = otaPass; ota[F("lock")] = otaLock; diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 25cce032ef..21c27d651c 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -1,52 +1,136 @@ #include "wled.h" /* - * Color conversion methods + * Color conversion & utility methods */ +/* + * color blend function + */ +uint32_t color_blend(uint32_t color1, uint32_t color2, uint16_t blend, bool b16) { + if(blend == 0) return color1; + uint16_t blendmax = b16 ? 0xFFFF : 0xFF; + if(blend == blendmax) return color2; + uint8_t shift = b16 ? 16 : 8; + + uint32_t w1 = W(color1); + uint32_t r1 = R(color1); + uint32_t g1 = G(color1); + uint32_t b1 = B(color1); + + uint32_t w2 = W(color2); + uint32_t r2 = R(color2); + uint32_t g2 = G(color2); + uint32_t b2 = B(color2); + + uint32_t w3 = ((w2 * blend) + (w1 * (blendmax - blend))) >> shift; + uint32_t r3 = ((r2 * blend) + (r1 * (blendmax - blend))) >> shift; + uint32_t g3 = ((g2 * blend) + (g1 * (blendmax - blend))) >> shift; + uint32_t b3 = ((b2 * blend) + (b1 * (blendmax - blend))) >> shift; + + return RGBW32(r3, g3, b3, w3); +} + +/* + * color add function that preserves ratio + * idea: https://github.com/Aircoookie/WLED/pull/2465 by https://github.com/Proto-molecule + */ +uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) +{ + if (fast) { + uint8_t r = R(c1); + uint8_t g = G(c1); + uint8_t b = B(c1); + uint8_t w = W(c1); + r = qadd8(r, R(c2)); + g = qadd8(g, G(c2)); + b = qadd8(b, B(c2)); + w = qadd8(w, W(c2)); + return RGBW32(r,g,b,w); + } else { + uint32_t r = R(c1) + R(c2); + uint32_t g = G(c1) + G(c2); + uint32_t b = B(c1) + B(c2); + uint32_t w = W(c1) + W(c2); + uint16_t max = r; + if (g > max) max = g; + if (b > max) max = b; + if (w > max) max = w; + if (max < 256) return RGBW32(r, g, b, w); + else return RGBW32(r * 255 / max, g * 255 / max, b * 255 / max, w * 255 / max); + } +} + +/* + * fades color toward black + * if using "video" method the resulting color will never become black unless it is already black + */ +uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) +{ + uint8_t r = R(c1); + uint8_t g = G(c1); + uint8_t b = B(c1); + uint8_t w = W(c1); + if (video) { + r = scale8_video(r, amount); + g = scale8_video(g, amount); + b = scale8_video(b, amount); + w = scale8_video(w, amount); + } else { + r = scale8(r, amount); + g = scale8(g, amount); + b = scale8(b, amount); + w = scale8(w, amount); + } + return RGBW32(r, g, b, w); +} + void setRandomColor(byte* rgb) { - lastRandomIndex = strip.get_random_wheel_index(lastRandomIndex); + lastRandomIndex = get_random_wheel_index(lastRandomIndex); colorHStoRGB(lastRandomIndex*256,255,rgb); } void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) //hue, sat to rgb { - float h = ((float)hue)/65535.0; - float s = ((float)sat)/255.0; - byte i = floor(h*6); - float f = h * 6-i; - float p = 255 * (1-s); - float q = 255 * (1-f*s); - float t = 255 * (1-(1-f)*s); + float h = ((float)hue)/65535.0f; + float s = ((float)sat)/255.0f; + int i = floorf(h*6); + float f = h * 6.0f - i; + int p = int(255.0f * (1.0f-s)); + int q = int(255.0f * (1.0f-f*s)); + int t = int(255.0f * (1.0f-(1.0f-f)*s)); + p = constrain(p, 0, 255); + q = constrain(q, 0, 255); + t = constrain(t, 0, 255); switch (i%6) { - case 0: rgb[0]=255,rgb[1]=t,rgb[2]=p;break; - case 1: rgb[0]=q,rgb[1]=255,rgb[2]=p;break; - case 2: rgb[0]=p,rgb[1]=255,rgb[2]=t;break; - case 3: rgb[0]=p,rgb[1]=q,rgb[2]=255;break; - case 4: rgb[0]=t,rgb[1]=p,rgb[2]=255;break; - case 5: rgb[0]=255,rgb[1]=p,rgb[2]=q; + case 0: rgb[0]=255,rgb[1]=t, rgb[2]=p; break; + case 1: rgb[0]=q, rgb[1]=255,rgb[2]=p; break; + case 2: rgb[0]=p, rgb[1]=255,rgb[2]=t; break; + case 3: rgb[0]=p, rgb[1]=q, rgb[2]=255;break; + case 4: rgb[0]=t, rgb[1]=p, rgb[2]=255;break; + case 5: rgb[0]=255,rgb[1]=p, rgb[2]=q; break; } } //get RGB values from color temperature in K (https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html) void colorKtoRGB(uint16_t kelvin, byte* rgb) //white spectrum to rgb, calc { - float r = 0, g = 0, b = 0; - float temp = kelvin / 100; - if (temp <= 66) { + int r = 0, g = 0, b = 0; + float temp = kelvin / 100.0f; + if (temp <= 66.0f) { r = 255; - g = round(99.4708025861 * log(temp) - 161.1195681661); - if (temp <= 19) { + g = roundf(99.4708025861f * logf(temp) - 161.1195681661f); + if (temp <= 19.0f) { b = 0; } else { - b = round(138.5177312231 * log((temp - 10)) - 305.0447927307); + b = roundf(138.5177312231f * logf((temp - 10.0f)) - 305.0447927307f); } } else { - r = round(329.698727446 * pow((temp - 60), -0.1332047592)); - g = round(288.1221695283 * pow((temp - 60), -0.0755148492)); + r = roundf(329.698727446f * powf((temp - 60.0f), -0.1332047592f)); + g = roundf(288.1221695283f * powf((temp - 60.0f), -0.0755148492f)); b = 255; - } + } //g += 12; //mod by Aircoookie, a bit less accurate but visibly less pinkish rgb[0] = (uint8_t) constrain(r, 0, 255); rgb[1] = (uint8_t) constrain(g, 0, 255); @@ -102,9 +186,9 @@ void colorXYtoRGB(float x, float y, byte* rgb) //coordinates to rgb (https://www b = 1.0f; } // Apply gamma correction - r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * pow(r, (1.0f / 2.4f)) - 0.055f; - g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f; - b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f; + r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * powf(r, (1.0f / 2.4f)) - 0.055f; + g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * powf(g, (1.0f / 2.4f)) - 0.055f; + b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * powf(b, (1.0f / 2.4f)) - 0.055f; if (r > b && r > g) { // red is biggest @@ -128,9 +212,9 @@ void colorXYtoRGB(float x, float y, byte* rgb) //coordinates to rgb (https://www b = 1.0f; } } - rgb[0] = 255.0*r; - rgb[1] = 255.0*g; - rgb[2] = 255.0*b; + rgb[0] = byte(255.0f*r); + rgb[1] = byte(255.0f*g); + rgb[2] = byte(255.0f*b); } void colorRGBtoXY(byte* rgb, float* xy) //rgb to coordinates (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy) @@ -149,7 +233,7 @@ void colorFromDecOrHexString(byte* rgb, char* in) if (in[0] == 0) return; char first = in[0]; uint32_t c = 0; - + if (first == '#' || first == 'h' || first == 'H') //is HEX encoded { c = strtoul(in +1, NULL, 16); @@ -197,35 +281,13 @@ float maxf (float v, float w) return v; } -/* -uint32_t colorRGBtoRGBW(uint32_t c) -{ - byte rgb[4]; - rgb[0] = R(c); - rgb[1] = G(c); - rgb[2] = B(c); - rgb[3] = W(c); - colorRGBtoRGBW(rgb); - return RGBW32(rgb[0], rgb[1], rgb[2], rgb[3]); -} - -void colorRGBtoRGBW(byte* rgb) //rgb to rgbw (http://codewelt.com/rgbw). (RGBW_MODE_LEGACY) -{ - float low = minf(rgb[0],minf(rgb[1],rgb[2])); - float high = maxf(rgb[0],maxf(rgb[1],rgb[2])); - if (high < 0.1f) return; - float sat = 100.0f * ((high - low) / high); // maximum saturation is 100 (corrected from 255) - rgb[3] = (byte)((255.0f - sat) / 255.0f * (rgb[0] + rgb[1] + rgb[2]) / 3); -} -*/ - -byte correctionRGB[4] = {0,0,0,0}; -uint16_t lastKelvin = 0; - // adjust RGB values based on color temperature in K (range [2800-10200]) (https://en.wikipedia.org/wiki/Color_balance) +// called from bus manager when color correction is enabled! uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb) { //remember so that slow colorKtoRGB() doesn't have to run for every setPixelColor() + static byte correctionRGB[4] = {0,0,0,0}; + static uint16_t lastKelvin = 0; if (lastKelvin != kelvin) colorKtoRGB(kelvin, correctionRGB); // convert Kelvin to RGB lastKelvin = kelvin; byte rgbw[4]; @@ -274,3 +336,51 @@ uint16_t approximateKelvinFromRGB(uint32_t rgb) { return (k > 10091) ? 10091 : k; } } + +//gamma 2.8 lookup table used for color correction +uint8_t NeoGammaWLEDMethod::gammaT[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, + 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, + 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, + 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, + 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, + 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, + 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, + 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, + 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, + 90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114, + 115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142, + 144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175, + 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, + 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 }; + +// re-calculates & fills gamma table +void NeoGammaWLEDMethod::calcGammaTable(float gamma) +{ + for (size_t i = 0; i < 256; i++) { + gammaT[i] = (int)(powf((float)i / 255.0f, gamma) * 255.0f + 0.5f); + } +} + +uint8_t NeoGammaWLEDMethod::Correct(uint8_t value) +{ + if (!gammaCorrectCol) return value; + return gammaT[value]; +} + +// used for color gamma correction +uint32_t NeoGammaWLEDMethod::Correct32(uint32_t color) +{ + if (!gammaCorrectCol) return color; + uint8_t w = W(color); + uint8_t r = R(color); + uint8_t g = G(color); + uint8_t b = B(color); + w = gammaT[w]; + r = gammaT[r]; + g = gammaT[g]; + b = gammaT[b]; + return RGBW32(r, g, b, w); +} diff --git a/wled00/const.h b/wled00/const.h index d961452f3c..388b64c820 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -5,10 +5,14 @@ * Readability defines and their associated numerical values + compile-time constants */ +#define GRADIENT_PALETTE_COUNT 58 + //Defaults #define DEFAULT_CLIENT_SSID "Your_Network" +#define DEFAULT_AP_SSID "WLED-AP" #define DEFAULT_AP_PASS "wled1234" #define DEFAULT_OTA_PASS "wledota" +#define DEFAULT_MDNS_NAME "x" //increase if you need more #ifndef WLED_MAX_USERMODS @@ -22,12 +26,43 @@ #ifndef WLED_MAX_BUSSES #ifdef ESP8266 #define WLED_MAX_BUSSES 3 + #define WLED_MIN_VIRTUAL_BUSSES 2 #else - #ifdef CONFIG_IDF_TARGET_ESP32S2 - #define WLED_MAX_BUSSES 5 + #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM + #define WLED_MAX_BUSSES 3 // will allow 2 digital & 1 analog (or the other way around) + #define WLED_MIN_VIRTUAL_BUSSES 3 + #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB + #if defined(USERMOD_AUDIOREACTIVE) // requested by @softhack007 https://github.com/blazoncek/WLED/issues/33 + #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog + #define WLED_MIN_VIRTUAL_BUSSES 4 + #else + #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog + #define WLED_MIN_VIRTUAL_BUSSES 3 + #endif + #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB does not support them ATM + #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog + #define WLED_MIN_VIRTUAL_BUSSES 4 #else - #define WLED_MAX_BUSSES 10 + #if defined(USERMOD_AUDIOREACTIVE) // requested by @softhack007 https://github.com/blazoncek/WLED/issues/33 + #define WLED_MAX_BUSSES 8 + #define WLED_MIN_VIRTUAL_BUSSES 2 + #else + #define WLED_MAX_BUSSES 10 + #define WLED_MIN_VIRTUAL_BUSSES 0 + #endif + #endif + #endif +#else + #ifdef ESP8266 + #if WLED_MAX_BUSES > 5 + #error Maximum number of buses is 5. + #endif + #define WLED_MIN_VIRTUAL_BUSSES (5-WLED_MAX_BUSSES) + #else + #if WLED_MAX_BUSES > 10 + #error Maximum number of buses is 10. #endif + #define WLED_MIN_VIRTUAL_BUSSES (10-WLED_MAX_BUSSES) #endif #endif @@ -45,6 +80,32 @@ #define WLED_MAX_COLOR_ORDER_MAPPINGS 10 #endif +#if defined(WLED_MAX_LEDMAPS) && (WLED_MAX_LEDMAPS > 32 || WLED_MAX_LEDMAPS < 10) + #undef WLED_MAX_LEDMAPS +#endif +#ifndef WLED_MAX_LEDMAPS + #ifdef ESP8266 + #define WLED_MAX_LEDMAPS 10 + #else + #define WLED_MAX_LEDMAPS 16 + #endif +#endif + +#ifndef WLED_MAX_SEGNAME_LEN + #ifdef ESP8266 + #define WLED_MAX_SEGNAME_LEN 32 + #else + #define WLED_MAX_SEGNAME_LEN 64 + #endif +#else + #if WLED_MAX_SEGNAME_LEN<32 + #undef WLED_MAX_SEGNAME_LEN + #define WLED_MAX_SEGNAME_LEN 32 + #else + #warning WLED UI does not support modified maximum segment name length! + #endif +#endif + //Usermod IDs #define USERMOD_ID_RESERVED 0 //Unused. Might indicate no usermod present #define USERMOD_ID_UNSPECIFIED 1 //Default value for a general user mod that does not specify a custom ID @@ -64,7 +125,7 @@ #define USERMOD_ID_RTC 15 //Usermod "usermod_rtc.h" #define USERMOD_ID_ELEKSTUBE_IPS 16 //Usermod "usermod_elekstube_ips.h" #define USERMOD_ID_SN_PHOTORESISTOR 17 //Usermod "usermod_sn_photoresistor.h" -#define USERMOD_ID_BATTERY_STATUS_BASIC 18 //Usermod "usermod_v2_battery_status_basic.h" +#define USERMOD_ID_BATTERY 18 //Usermod "usermod_v2_battery.h" #define USERMOD_ID_PWM_FAN 19 //Usermod "usermod_PWM_fan.h" #define USERMOD_ID_BH1750 20 //Usermod "usermod_bh1750.h" #define USERMOD_ID_SEVEN_SEGMENT_DISPLAY 21 //Usermod "usermod_v2_seven_segment_display.h" @@ -72,6 +133,25 @@ #define USERMOD_ID_QUINLED_AN_PENTA 23 //Usermod "quinled-an-penta.h" #define USERMOD_ID_SSDR 24 //Usermod "usermod_v2_seven_segment_display_reloaded.h" #define USERMOD_ID_CRONIXIE 25 //Usermod "usermod_cronixie.h" +#define USERMOD_ID_WIZLIGHTS 26 //Usermod "wizlights.h" +#define USERMOD_ID_WORDCLOCK 27 //Usermod "usermod_v2_word_clock.h" +#define USERMOD_ID_MY9291 28 //Usermod "usermod_MY9291.h" +#define USERMOD_ID_SI7021_MQTT_HA 29 //Usermod "usermod_si7021_mqtt_ha.h" +#define USERMOD_ID_BME280 30 //Usermod "usermod_bme280.h +#define USERMOD_ID_SMARTNEST 31 //Usermod "usermod_smartnest.h" +#define USERMOD_ID_AUDIOREACTIVE 32 //Usermod "audioreactive.h" +#define USERMOD_ID_ANALOG_CLOCK 33 //Usermod "Analog_Clock.h" +#define USERMOD_ID_PING_PONG_CLOCK 34 //Usermod "usermod_v2_ping_pong_clock.h" +#define USERMOD_ID_ADS1115 35 //Usermod "usermod_ads1115.h" +#define USERMOD_ID_BOBLIGHT 36 //Usermod "boblight.h" +#define USERMOD_ID_SD_CARD 37 //Usermod "usermod_sd_card.h" +#define USERMOD_ID_PWM_OUTPUTS 38 //Usermod "usermod_pwm_outputs.h +#define USERMOD_ID_SHT 39 //Usermod "usermod_sht.h +#define USERMOD_ID_KLIPPER 40 //Usermod Klipper percentage +#define USERMOD_ID_WIREGUARD 41 //Usermod "wireguard.h" +#define USERMOD_ID_INTERNAL_TEMPERATURE 42 //Usermod "usermod_internal_temperature.h" +#define USERMOD_ID_LDR_DUSK_DAWN 43 //Usermod "usermod_LDR_Dusk_Dawn_v2.h" +#define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -88,18 +168,20 @@ #define CALL_MODE_NO_NOTIFY 5 #define CALL_MODE_FX_CHANGED 6 //no longer used #define CALL_MODE_HUE 7 -#define CALL_MODE_PRESET_CYCLE 8 -#define CALL_MODE_BLYNK 9 +#define CALL_MODE_PRESET_CYCLE 8 //no longer used +#define CALL_MODE_BLYNK 9 //no longer used #define CALL_MODE_ALEXA 10 #define CALL_MODE_WS_SEND 11 //special call mode, not for notifier, updates websocket only #define CALL_MODE_BUTTON_PRESET 12 //button/IR JSON preset/macro //RGB to RGBW conversion mode -#define RGBW_MODE_MANUAL_ONLY 0 //No automatic white channel calculation. Manual white channel slider -#define RGBW_MODE_AUTO_BRIGHTER 1 //New algorithm. Adds as much white as the darkest RGBW channel -#define RGBW_MODE_AUTO_ACCURATE 2 //New algorithm. Adds as much white as the darkest RGBW channel and subtracts this amount from each RGB channel -#define RGBW_MODE_DUAL 3 //Manual slider + auto calculation. Automatically calculates only if manual slider is set to off (0) -#define RGBW_MODE_LEGACY 4 //Old floating algorithm. Too slow for realtime and palette support +#define RGBW_MODE_MANUAL_ONLY 0 // No automatic white channel calculation. Manual white channel slider +#define RGBW_MODE_AUTO_BRIGHTER 1 // New algorithm. Adds as much white as the darkest RGBW channel +#define RGBW_MODE_AUTO_ACCURATE 2 // New algorithm. Adds as much white as the darkest RGBW channel and subtracts this amount from each RGB channel +#define RGBW_MODE_DUAL 3 // Manual slider + auto calculation. Automatically calculates only if manual slider is set to off (0) +#define RGBW_MODE_MAX 4 // Sets white to the value of the brightest RGB channel (good for white-only LEDs without any RGB) +//#define RGBW_MODE_LEGACY 4 // Old floating algorithm. Too slow for realtime and palette support (unused) +#define AW_GLOBAL_DISABLED 255 // Global auto white mode override disabled. Per-bus setting is used //realtime modes #define REALTIME_MODE_INACTIVE 0 @@ -121,10 +203,14 @@ #define DMX_MODE_DISABLED 0 //not used #define DMX_MODE_SINGLE_RGB 1 //all LEDs same RGB color (3 channels) #define DMX_MODE_SINGLE_DRGB 2 //all LEDs same RGB color and master dimmer (4 channels) -#define DMX_MODE_EFFECT 3 //trigger standalone effects of WLED (11 channels) +#define DMX_MODE_EFFECT 3 //trigger standalone effects of WLED (15 channels) +#define DMX_MODE_EFFECT_W 7 //trigger standalone effects of WLED (18 channels) #define DMX_MODE_MULTIPLE_RGB 4 //every LED is addressed with its own RGB (ledCount * 3 channels) #define DMX_MODE_MULTIPLE_DRGB 5 //every LED is addressed with its own RGB and share a master dimmer (ledCount * 3 + 1 channels) #define DMX_MODE_MULTIPLE_RGBW 6 //every LED is addressed with its own RGBW (ledCount * 4 channels) +#define DMX_MODE_EFFECT_SEGMENT 8 //trigger standalone effects of WLED (15 channels per segment) +#define DMX_MODE_EFFECT_SEGMENT_W 9 //trigger standalone effects of WLED (18 channels per segment) +#define DMX_MODE_PRESET 10 //apply presets (1 channel) //Light capability byte (unused) 0bRCCCTTTT //bits 0/1/2/3: specifies a type of LED driver. A single "driver" may have different chip models but must have the same protocol/behavior @@ -141,11 +227,16 @@ #define TYPE_NONE 0 //light is not configured #define TYPE_RESERVED 1 //unused. Might indicate a "virtual" light //Digital types (data pin only) (16-31) -#define TYPE_WS2812_1CH 20 //white-only chips +#define TYPE_WS2812_1CH 18 //white-only chips (1 channel per IC) (unused) +#define TYPE_WS2812_1CH_X3 19 //white-only chips (3 channels per IC) +#define TYPE_WS2812_2CH_X3 20 //CCT chips (1st IC controls WW + CW of 1st zone and CW of 2nd zone, 2nd IC controls WW of 2nd zone and WW + CW of 3rd zone) #define TYPE_WS2812_WWA 21 //amber + warm + cold white #define TYPE_WS2812_RGB 22 #define TYPE_GS8608 23 //same driver as WS2812, but will require signal 2x per second (else displays test pattern) #define TYPE_WS2811_400KHZ 24 //half-speed WS2812 protocol, used by very old WS2811 units +#define TYPE_TM1829 25 +#define TYPE_UCS8903 26 +#define TYPE_UCS8904 29 #define TYPE_SK6812_RGBW 30 #define TYPE_TM1814 31 //"Analog" types (PWM) (32-47) @@ -160,10 +251,12 @@ #define TYPE_APA102 51 #define TYPE_LPD8806 52 #define TYPE_P9813 53 +#define TYPE_LPD6803 54 //Network types (master broadcast) (80-95) #define TYPE_NET_DDP_RGB 80 //network DDP RGB bus (master broadcast bus) -#define TYPE_NET_E131_RGB 81 //network E131 RGB bus (master broadcast bus) -#define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus) +#define TYPE_NET_E131_RGB 81 //network E131 RGB bus (master broadcast bus, unused) +#define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus, unused) +#define TYPE_NET_DDP_RGBW 88 //network DDP RGBW bus (master broadcast bus) #define IS_DIGITAL(t) ((t) & 0x10) //digital are 16-31 and 48-63 #define IS_PWM(t) ((t) > 40 && (t) < 46) @@ -192,7 +285,7 @@ #define BTN_TYPE_ANALOG_INVERTED 8 //Ethernet board types -#define WLED_NUM_ETH_TYPES 7 +#define WLED_NUM_ETH_TYPES 11 #define WLED_ETH_NONE 0 #define WLED_ETH_WT32_ETH01 1 @@ -201,6 +294,10 @@ #define WLED_ETH_QUINLED 4 #define WLED_ETH_TWILIGHTLORD 5 #define WLED_ETH_ESP32DEUX 6 +#define WLED_ETH_ESP32ETHKITVE 7 +#define WLED_ETH_QUINLED_OCTA 8 +#define WLED_ETH_ABCWLEDV43ETH 9 +#define WLED_ETH_SERG74 10 //Hue error codes #define HUE_ERROR_INACTIVE 0 @@ -216,45 +313,73 @@ #define SEG_OPTION_REVERSED 1 #define SEG_OPTION_ON 2 #define SEG_OPTION_MIRROR 3 //Indicates that the effect will be mirrored within the segment -#define SEG_OPTION_NONUNITY 4 //Indicates that the effect does not use FRAMETIME or needs getPixelColor -#define SEG_OPTION_FREEZE 5 //Segment contents will not be refreshed -#define SEG_OPTION_TRANSITIONAL 7 +#define SEG_OPTION_FREEZE 4 //Segment contents will not be refreshed +#define SEG_OPTION_RESET 5 //Segment runtime requires reset +#define SEG_OPTION_REVERSED_Y 6 +#define SEG_OPTION_MIRROR_Y 7 +#define SEG_OPTION_TRANSPOSED 8 //Segment differs return byte -#define SEG_DIFFERS_BRI 0x01 -#define SEG_DIFFERS_OPT 0x02 -#define SEG_DIFFERS_COL 0x04 -#define SEG_DIFFERS_FX 0x08 -#define SEG_DIFFERS_BOUNDS 0x10 -#define SEG_DIFFERS_GSO 0x20 -#define SEG_DIFFERS_SEL 0x80 +#define SEG_DIFFERS_BRI 0x01 // opacity +#define SEG_DIFFERS_OPT 0x02 // all segment options except: selected, reset & transitional +#define SEG_DIFFERS_COL 0x04 // colors +#define SEG_DIFFERS_FX 0x08 // effect/mode parameters +#define SEG_DIFFERS_BOUNDS 0x10 // segment start/stop bounds +#define SEG_DIFFERS_GSO 0x20 // grouping, spacing & offset +#define SEG_DIFFERS_SEL 0x80 // selected //Playlist option byte #define PL_OPTION_SHUFFLE 0x01 +// Segment capability byte +#define SEG_CAPABILITY_RGB 0x01 +#define SEG_CAPABILITY_W 0x02 +#define SEG_CAPABILITY_CCT 0x04 + // WLED Error modes #define ERR_NONE 0 // All good :) -#define ERR_EEP_COMMIT 2 // Could not commit to EEPROM (wrong flash layout?) +#define ERR_DENIED 1 // Permission denied +#define ERR_EEP_COMMIT 2 // Could not commit to EEPROM (wrong flash layout?) OBSOLETE +#define ERR_NOBUF 3 // JSON buffer was not released in time, request cannot be handled at this time #define ERR_JSON 9 // JSON parsing failed (input too large?) #define ERR_FS_BEGIN 10 // Could not init filesystem (no partition?) #define ERR_FS_QUOTA 11 // The FS is full or the maximum file size is reached #define ERR_FS_PLOAD 12 // It was attempted to load a preset that does not exist #define ERR_FS_IRLOAD 13 // It was attempted to load an IR JSON cmd, but the "ir.json" file does not exist -#define ERR_FS_GENERAL 19 // A general unspecified filesystem error occured +#define ERR_FS_RMLOAD 14 // It was attempted to load an remote JSON cmd, but the "remote.json" file does not exist +#define ERR_FS_GENERAL 19 // A general unspecified filesystem error occurred #define ERR_OVERTEMP 30 // An attached temperature sensor has measured above threshold temperature (not implemented) #define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented) #define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented) -//Timer mode types +// Timer mode types #define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness #define NL_MODE_FADE 1 //Fade to target brightness gradually #define NL_MODE_COLORFADE 2 //Fade to target brightness and secondary color gradually #define NL_MODE_SUN 3 //Sunrise/sunset. Target brightness is set immediately, then Sunrise effect is started. Max 60 min. - -#define NTP_PACKET_SIZE 48 - -//maximum number of rendered LEDs - this does not have to match max. physical LEDs, e.g. if there are virtual busses +// Settings sub page IDs +#define SUBPAGE_MENU 0 +#define SUBPAGE_WIFI 1 +#define SUBPAGE_LEDS 2 +#define SUBPAGE_UI 3 +#define SUBPAGE_SYNC 4 +#define SUBPAGE_TIME 5 +#define SUBPAGE_SEC 6 +#define SUBPAGE_DMX 7 +#define SUBPAGE_UM 8 +#define SUBPAGE_UPDATE 9 +#define SUBPAGE_2D 10 +#define SUBPAGE_LOCK 251 +#define SUBPAGE_PINREQ 252 +#define SUBPAGE_CSS 253 +#define SUBPAGE_JS 254 +#define SUBPAGE_WELCOME 255 + +#define NTP_PACKET_SIZE 48 // size of NTP receive buffer +#define NTP_MIN_PACKET_SIZE 48 // min expected size - NTP v4 allows for "extended information" appended to the standard fields + +//maximum number of rendered LEDs - this does not have to match max. physical LEDs, e.g. if there are virtual busses #ifndef MAX_LEDS #ifdef ESP8266 #define MAX_LEDS 1664 //can't rely on memory limit to limit this to 1600 LEDs @@ -264,22 +389,26 @@ #endif #ifndef MAX_LED_MEMORY -#ifdef ESP8266 -#define MAX_LED_MEMORY 4000 -#else -#define MAX_LED_MEMORY 64000 -#endif + #ifdef ESP8266 + #define MAX_LED_MEMORY 4000 + #else + #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) + #define MAX_LED_MEMORY 32000 + #else + #define MAX_LED_MEMORY 64000 + #endif + #endif #endif #ifndef MAX_LEDS_PER_BUS -#define MAX_LEDS_PER_BUS 4096 +#define MAX_LEDS_PER_BUS 2048 // may not be enough for fast LEDs (i.e. APA102) #endif // string temp buffer (now stored in stack locally) #ifdef ESP8266 #define SETTINGS_STACK_BUF_SIZE 2048 #else -#define SETTINGS_STACK_BUF_SIZE 3096 +#define SETTINGS_STACK_BUF_SIZE 3608 // warning: quite a large value for stack #endif #ifdef WLED_USE_ETHERNET @@ -292,7 +421,15 @@ #endif #endif -#define ABL_MILLIAMPS_DEFAULT 850 // auto lower brightness to stay close to milliampere limit +#ifndef ABL_MILLIAMPS_DEFAULT + #define ABL_MILLIAMPS_DEFAULT 850 // auto lower brightness to stay close to milliampere limit +#else + #if ABL_MILLIAMPS_DEFAULT == 0 // disable ABL + #elif ABL_MILLIAMPS_DEFAULT < 250 // make sure value is at least 250 + #warning "make sure value is at least 250" + #define ABL_MILLIAMPS_DEFAULT 250 + #endif +#endif // PWM settings #ifndef WLED_PWM_FREQ @@ -309,14 +446,11 @@ #ifdef ESP8266 #define JSON_BUFFER_SIZE 10240 #else - #define JSON_BUFFER_SIZE 20480 + #define JSON_BUFFER_SIZE 24576 #endif -#ifdef WLED_USE_DYNAMIC_JSON - #define MIN_HEAP_SIZE JSON_BUFFER_SIZE+512 -#else - #define MIN_HEAP_SIZE 4096 -#endif +//#define MIN_HEAP_SIZE (8k for AsyncWebServer) +#define MIN_HEAP_SIZE 8192 // Maximum size of node map (list of other WLED instances) #ifdef ESP8266 @@ -327,10 +461,10 @@ //this is merely a default now and can be changed at runtime #ifndef LEDPIN -#ifdef ESP8266 - #define LEDPIN 2 // GPIO2 (D4) on Wemod D1 mini compatible boards +#if defined(ESP8266) || (defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_PSRAM)) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ARDUINO_ESP32_PICO) + #define LEDPIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, and on boards where GPIO16 is not available #else - #define LEDPIN 2 // Changed from 16 to restore compatibility with ESP32-pico + #define LEDPIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards #endif #endif @@ -346,6 +480,64 @@ #define DEFAULT_LED_COUNT 30 #endif -#define INTERFACE_UPDATE_COOLDOWN 2000 //time in ms to wait between websockets, alexa, and MQTT updates +#define INTERFACE_UPDATE_COOLDOWN 1000 // time in ms to wait between websockets, alexa, and MQTT updates + +#define PIN_RETRY_COOLDOWN 3000 // time in ms after an incorrect attempt PIN and OTA pass will be rejected even if correct +#define PIN_TIMEOUT 900000 // time in ms after which the PIN will be required again, 15 minutes + +// HW_PIN_SCL & HW_PIN_SDA are used for information in usermods settings page and usermods themselves +// which GPIO pins are actually used in a hardware layout (controller board) +#if defined(I2CSCLPIN) && !defined(HW_PIN_SCL) + #define HW_PIN_SCL I2CSCLPIN +#endif +#if defined(I2CSDAPIN) && !defined(HW_PIN_SDA) + #define HW_PIN_SDA I2CSDAPIN +#endif +// you cannot change HW I2C pins on 8266 +#if defined(ESP8266) && defined(HW_PIN_SCL) + #undef HW_PIN_SCL +#endif +#if defined(ESP8266) && defined(HW_PIN_SDA) + #undef HW_PIN_SDA +#endif +// defaults for 1st I2C on ESP32 (Wire global) +#ifndef HW_PIN_SCL + #define HW_PIN_SCL SCL +#endif +#ifndef HW_PIN_SDA + #define HW_PIN_SDA SDA +#endif + +// HW_PIN_SCLKSPI & HW_PIN_MOSISPI & HW_PIN_MISOSPI are used for information in usermods settings page and usermods themselves +// which GPIO pins are actually used in a hardware layout (controller board) +#if defined(SPISCLKPIN) && !defined(HW_PIN_CLOCKSPI) + #define HW_PIN_CLOCKSPI SPISCLKPIN +#endif +#if defined(SPIMOSIPIN) && !defined(HW_PIN_MOSISPI) + #define HW_PIN_MOSISPI SPIMOSIPIN +#endif +#if defined(SPIMISOPIN) && !defined(HW_PIN_MISOSPI) + #define HW_PIN_MISOSPI SPIMISOPIN +#endif +// you cannot change HW SPI pins on 8266 +#if defined(ESP8266) && defined(HW_PIN_CLOCKSPI) + #undef HW_PIN_CLOCKSPI +#endif +#if defined(ESP8266) && defined(HW_PIN_DATASPI) + #undef HW_PIN_DATASPI +#endif +#if defined(ESP8266) && defined(HW_PIN_MISOSPI) + #undef HW_PIN_MISOSPI +#endif +// defaults for VSPI on ESP32 (SPI global, SPI.cpp) as HSPI is used by WLED (bus_wrapper.h) +#ifndef HW_PIN_CLOCKSPI + #define HW_PIN_CLOCKSPI SCK +#endif +#ifndef HW_PIN_DATASPI + #define HW_PIN_DATASPI MOSI +#endif +#ifndef HW_PIN_MISOSPI + #define HW_PIN_MISOSPI MISO +#endif #endif diff --git a/wled00/data/404.htm b/wled00/data/404.htm index 87244a94ff..ff41fa6e03 100644 --- a/wled00/data/404.htm +++ b/wled00/data/404.htm @@ -1,47 +1,47 @@ - - - - - Not found - - - - -

404 Not Found

-Akemi does not know where you are headed...

- - + img { + width: 400px; + max-width: 50%; + image-rendering: pixelated; + image-rendering: crisp-edges; + margin: 25px 0 -10px 0; + } + + button { + outline: none; + cursor: pointer; + padding: 8px; + margin: 10px; + width: 230px; + text-transform: uppercase; + font-family: helvetica; + font-size: 19px; + background-color: #333; + color: white; + border: 0px solid white; + border-radius: 25px; + } + + + + +

404 Not Found

+ Akemi does not know where you are headed...

+ + \ No newline at end of file diff --git a/wled00/data/cpal/cpal.htm b/wled00/data/cpal/cpal.htm new file mode 100644 index 0000000000..773c02d20d --- /dev/null +++ b/wled00/data/cpal/cpal.htm @@ -0,0 +1,719 @@ + + + + + + + + WLED Custom Palette Editor + + + + + +
+
+

+ + + + + + + WLED Custom Palette Editor +

+
+ +
+
+
+
+
+
+ Currently in use custom palettes +
+
+
+ +
+
+ Click on the gradient editor to add new color slider, then the colored box below the slider to change its color. + Click the red box below indicator (and confirm) to delete. + Once finished, click the arrow icon to upload into the desired slot. + To edit existing palette, click the pencil icon. +
+
+
+
+
+ Available static palettes +
+
+
+ + + + + diff --git a/wled00/data/icons-ui/Read Me.txt b/wled00/data/icons-ui/Read Me.txt new file mode 100644 index 0000000000..8491652f88 --- /dev/null +++ b/wled00/data/icons-ui/Read Me.txt @@ -0,0 +1,7 @@ +Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures. + +To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/#docs/local-fonts + +You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects. + +You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection. diff --git a/wled00/data/icons-ui/demo-files/demo.css b/wled00/data/icons-ui/demo-files/demo.css new file mode 100644 index 0000000000..39b8991da7 --- /dev/null +++ b/wled00/data/icons-ui/demo-files/demo.css @@ -0,0 +1,152 @@ +body { + padding: 0; + margin: 0; + font-family: sans-serif; + font-size: 1em; + line-height: 1.5; + color: #555; + background: #fff; +} +h1 { + font-size: 1.5em; + font-weight: normal; +} +small { + font-size: .66666667em; +} +a { + color: #e74c3c; + text-decoration: none; +} +a:hover, a:focus { + box-shadow: 0 1px #e74c3c; +} +.bshadow0, input { + box-shadow: inset 0 -2px #e7e7e7; +} +input:hover { + box-shadow: inset 0 -2px #ccc; +} +input, fieldset { + font-family: sans-serif; + font-size: 1em; + margin: 0; + padding: 0; + border: 0; +} +input { + color: inherit; + line-height: 1.5; + height: 1.5em; + padding: .25em 0; +} +input:focus { + outline: none; + box-shadow: inset 0 -2px #449fdb; +} +.glyph { + font-size: 16px; + width: 15em; + padding-bottom: 1em; + margin-right: 4em; + margin-bottom: 1em; + float: left; + overflow: hidden; +} +.liga { + width: 80%; + width: calc(100% - 2.5em); +} +.talign-right { + text-align: right; +} +.talign-center { + text-align: center; +} +.bgc1 { + background: #f1f1f1; +} +.fgc1 { + color: #999; +} +.fgc0 { + color: #000; +} +p { + margin-top: 1em; + margin-bottom: 1em; +} +.mvm { + margin-top: .75em; + margin-bottom: .75em; +} +.mtn { + margin-top: 0; +} +.mtl, .mal { + margin-top: 1.5em; +} +.mbl, .mal { + margin-bottom: 1.5em; +} +.mal, .mhl { + margin-left: 1.5em; + margin-right: 1.5em; +} +.mhmm { + margin-left: 1em; + margin-right: 1em; +} +.mls { + margin-left: .25em; +} +.ptl { + padding-top: 1.5em; +} +.pbs, .pvs { + padding-bottom: .25em; +} +.pvs, .pts { + padding-top: .25em; +} +.unit { + float: left; +} +.unitRight { + float: right; +} +.size1of2 { + width: 50%; +} +.size1of1 { + width: 100%; +} +.clearfix:before, .clearfix:after { + content: " "; + display: table; +} +.clearfix:after { + clear: both; +} +.hidden-true { + display: none; +} +.textbox0 { + width: 3em; + background: #f1f1f1; + padding: .25em .5em; + line-height: 1.5; + height: 1.5em; +} +#testDrive { + display: block; + padding-top: 24px; + line-height: 1.5; +} +.fs0 { + font-size: 16px; +} +.fs1 { + font-size: 32px; +} + diff --git a/wled00/data/icons-ui/demo-files/demo.js b/wled00/data/icons-ui/demo-files/demo.js new file mode 100644 index 0000000000..6f45f1c409 --- /dev/null +++ b/wled00/data/icons-ui/demo-files/demo.js @@ -0,0 +1,30 @@ +if (!('boxShadow' in document.body.style)) { + document.body.setAttribute('class', 'noBoxShadow'); +} + +document.body.addEventListener("click", function(e) { + var target = e.target; + if (target.tagName === "INPUT" && + target.getAttribute('class').indexOf('liga') === -1) { + target.select(); + } +}); + +(function() { + var fontSize = document.getElementById('fontSize'), + testDrive = document.getElementById('testDrive'), + testText = document.getElementById('testText'); + function updateTest() { + testDrive.innerHTML = testText.value || String.fromCharCode(160); + if (window.icomoonLiga) { + window.icomoonLiga(testDrive); + } + } + function updateSize() { + testDrive.style.fontSize = fontSize.value + 'px'; + } + fontSize.addEventListener('change', updateSize, false); + testText.addEventListener('input', updateTest, false); + testText.addEventListener('change', updateTest, false); + updateSize(); +}()); diff --git a/wled00/data/icons-ui/demo.html b/wled00/data/icons-ui/demo.html new file mode 100644 index 0000000000..0416231fb0 --- /dev/null +++ b/wled00/data/icons-ui/demo.html @@ -0,0 +1,360 @@ + + + + + IcoMoon Demo + + + + + +
+

Font Name: wled122 (Glyphs: 23)

+
+
+

Grid Size: Unknown

+
+
+ + i-pattern +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-segments +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-sun +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-palette +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-eye +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-speed +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-expand +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-power +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-settings +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-playlist +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-night +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-cancel +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-sync +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-confirm +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-brightness +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-nodes +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-add +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-edit +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-intensity +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-star +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-info +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-del +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-presets +
+
+ + +
+
+ liga: + +
+
+
+ + +
+

Font Test Drive

+ + +
  +
+
+ +
+

Generated by IcoMoon

+
+ + + + diff --git a/wled00/data/icons-ui/fonts/wled122.woff b/wled00/data/icons-ui/fonts/wled122.woff new file mode 100644 index 0000000000..cc389b3a5a Binary files /dev/null and b/wled00/data/icons-ui/fonts/wled122.woff differ diff --git a/wled00/data/icons-ui/fonts/wled122.woff2 b/wled00/data/icons-ui/fonts/wled122.woff2 new file mode 100644 index 0000000000..d13b37ac0c Binary files /dev/null and b/wled00/data/icons-ui/fonts/wled122.woff2 differ diff --git a/wled00/data/icons-ui/selection.json b/wled00/data/icons-ui/selection.json new file mode 100644 index 0000000000..5af8516b8b --- /dev/null +++ b/wled00/data/icons-ui/selection.json @@ -0,0 +1 @@ +{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M511.573 85.333c235.947 0 427.094 191.147 427.094 426.667s-191.147 426.667-427.094 426.667c-235.52 0-426.24-191.147-426.24-426.667s190.72-426.667 426.24-426.667zM512 853.333c188.587 0 341.333-152.746 341.333-341.333s-152.746-341.333-341.333-341.333-341.333 152.746-341.333 341.333 152.746 341.333 341.333 341.333zM661.333 469.333c-35.413 0-64-28.586-64-64 0-35.413 28.587-64 64-64 35.414 0 64 28.587 64 64 0 35.414-28.586 64-64 64zM362.667 469.333c-35.414 0-64-28.586-64-64 0-35.413 28.586-64 64-64 35.413 0 64 28.587 64 64 0 35.414-28.587 64-64 64zM512 746.667c-99.413 0-183.893-62.294-218.027-149.334h436.054c-34.134 87.040-118.614 149.334-218.027 149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE23D"],"defaultCode":57917,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":11,"order":26,"prevSize":32,"code":57917,"name":"pattern"},"setIdx":0,"setId":0,"iconIdx":10},{"icon":{"paths":["M511.573 791.040l314.88-244.907 69.547 54.187-384 298.667-384-298.667 69.12-53.76zM512 682.667l-384-298.667 384-298.667 384 298.667-69.973 54.187z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE34B"],"defaultCode":58187,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":14,"order":35,"ligatures":"","prevSize":32,"code":58187,"name":"segments"},"setIdx":0,"setId":0,"iconIdx":13},{"icon":{"paths":["M288.427 206.507l-60.587 60.16-76.373-76.374 60.16-60.16zM170.667 448v85.333h-128v-85.333h128zM554.667 23.467v125.866h-85.334v-125.866h85.334zM872.533 190.293l-76.373 76.374-60.16-60.16 76.373-76.374zM735.573 774.827l59.734-59.734 76.8 76.374-60.16 60.16zM853.333 448h128v85.333h-128v-85.333zM512 234.667c141.227 0 256 114.773 256 256 0 141.226-114.773 256-256 256s-256-114.774-256-256c0-141.227 114.773-256 256-256zM469.333 957.867v-125.867h85.334v125.867h-85.334zM151.467 791.040l76.373-76.8 60.16 60.16-76.373 76.8z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE333"],"defaultCode":58163,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":40,"order":73,"ligatures":"","prevSize":32,"code":58163,"name":"sun"},"setIdx":0,"setId":0,"iconIdx":39},{"icon":{"paths":["M512 128c212.053 0 384 152.747 384 341.333 0 117.76-95.573 213.334-213.333 213.334h-75.52c-35.414 0-64 28.586-64 64 0 16.213 6.4 31.146 16.213 42.24 10.24 11.52 16.64 26.453 16.64 43.093 0 35.413-28.587 64-64 64-212.053 0-384-171.947-384-384s171.947-384 384-384zM277.333 512c35.414 0 64-28.587 64-64s-28.586-64-64-64c-35.413 0-64 28.587-64 64s28.587 64 64 64zM405.333 341.333c35.414 0 64-28.586 64-64 0-35.413-28.586-64-64-64-35.413 0-64 28.587-64 64 0 35.414 28.587 64 64 64zM618.667 341.333c35.413 0 64-28.586 64-64 0-35.413-28.587-64-64-64-35.414 0-64 28.587-64 64 0 35.414 28.586 64 64 64zM746.667 512c35.413 0 64-28.587 64-64s-28.587-64-64-64c-35.414 0-64 28.587-64 64s28.586 64 64 64z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2B3"],"defaultCode":58035,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":75,"order":22,"prevSize":32,"code":58035,"name":"palette"},"setIdx":0,"setId":0,"iconIdx":74},{"icon":{"paths":["M512 192c213.333 0 395.52 132.693 469.333 320-73.813 187.307-256 320-469.333 320s-395.52-132.693-469.333-320c73.813-187.307 256-320 469.333-320zM512 725.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333-213.333 95.573-213.333 213.333 95.573 213.333 213.333 213.333zM512 384c70.827 0 128 57.173 128 128s-57.173 128-128 128-128-57.173-128-128 57.173-128 128-128z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0E8"],"defaultCode":57576,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":172,"order":74,"prevSize":32,"code":57576,"name":"eye"},"setIdx":0,"setId":0,"iconIdx":171},{"icon":{"paths":["M640 42.667v85.333h-256v-85.333h256zM469.333 597.333v-256h85.334v256h-85.334zM811.947 315.307c52.48 65.706 84.053 148.906 84.053 239.36 0 212.053-171.52 384-384 384s-384-171.947-384-384c0-212.054 171.947-384 384-384 90.453 0 173.653 31.573 239.787 84.48l60.586-60.587c21.76 17.92 41.814 38.4 60.16 60.16zM512 853.333c165.12 0 298.667-133.546 298.667-298.666s-133.547-298.667-298.667-298.667-298.667 133.547-298.667 298.667 133.547 298.666 298.667 298.666z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE325"],"defaultCode":58149,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":370,"order":21,"ligatures":"","prevSize":32,"code":58149,"name":"speed"},"setIdx":0,"setId":0,"iconIdx":369},{"icon":{"paths":["M707.84 366.507l60.16 60.16-256 256-256-256 60.16-60.16 195.84 195.413z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE395"],"defaultCode":58261,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":549,"order":69,"ligatures":"","prevSize":32,"code":58261,"name":"expand"},"setIdx":0,"setId":0,"iconIdx":548},{"icon":{"paths":["M554.667 128v426.667h-85.334v-426.667h85.334zM760.747 220.587c82.773 70.4 135.253 174.506 135.253 291.413 0 212.053-171.947 384-384 384s-384-171.947-384-384c0-116.907 52.48-221.013 135.253-291.413l60.16 60.16c-66.986 54.613-110.080 137.813-110.080 231.253 0 165.12 133.547 298.667 298.667 298.667s298.667-133.547 298.667-298.667c0-93.44-43.094-176.64-110.507-230.827z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE08F"],"defaultCode":57487,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":557,"order":19,"ligatures":"","prevSize":32,"code":57487,"name":"power"},"setIdx":0,"setId":0,"iconIdx":556},{"icon":{"paths":["M816.64 551.936l85.504 67.584c8.192 6.144 10.24 16.896 5.12 26.112l-81.92 141.824c-5.12 9.216-15.872 12.8-25.088 9.216l-101.888-40.96c-20.992 15.872-44.032 29.696-69.12 39.936l-15.36 108.544c-1.024 10.24-9.728 17.408-19.968 17.408h-163.84c-10.24 0-18.432-7.168-20.48-17.408l-15.36-108.544c-25.088-10.24-47.616-23.552-69.12-39.936l-101.888 40.96c-9.216 3.072-19.968 0-25.088-9.216l-81.92-141.824c-4.608-8.704-2.56-19.968 5.12-26.112l86.528-67.584c-2.048-12.8-3.072-26.624-3.072-39.936s1.536-27.136 3.584-39.936l-86.528-67.584c-8.192-6.144-10.24-16.896-5.12-26.112l81.92-141.824c5.12-9.216 15.872-12.8 25.088-9.216l101.888 40.96c20.992-15.872 44.032-29.696 69.12-39.936l15.36-108.544c1.536-10.24 9.728-17.408 19.968-17.408h163.84c10.24 0 18.944 7.168 20.48 17.408l15.36 108.544c25.088 10.24 47.616 23.552 69.12 39.936l101.888-40.96c9.216-3.072 19.968 0 25.088 9.216l81.92 141.824c4.608 8.704 2.56 19.968-5.12 26.112l-86.528 67.584c2.048 12.8 3.072 26.112 3.072 39.936s-1.024 27.136-2.56 39.936zM512 665.6c84.48 0 153.6-69.12 153.6-153.6s-69.12-153.6-153.6-153.6-153.6 69.12-153.6 153.6 69.12 153.6 153.6 153.6z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0A2"],"defaultCode":57506,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":562,"order":29,"ligatures":"","prevSize":32,"code":57506,"name":"settings"},"setIdx":0,"setId":0,"iconIdx":561},{"icon":{"paths":["M556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l-130.133-97.707v384l256-192zM469.333 173.653c-62.293 7.68-119.040 32.427-166.4 69.12l-60.586-61.013c63.146-51.627 141.226-85.76 226.986-94.293v86.186zM242.773 302.933c-36.693 47.36-61.44 104.107-69.12 166.4h-86.186c8.533-85.76 42.666-163.84 94.293-226.986zM173.653 554.667c7.68 62.293 32.427 119.040 69.12 165.973l-61.013 61.013c-51.627-63.146-85.76-141.226-94.293-226.986h86.186zM242.347 842.24l60.586-61.013c47.36 36.693 104.107 61.44 166.4 69.12v86.186c-85.333-8.533-163.84-42.666-226.986-94.293zM938.667 512c0 220.16-167.254 401.92-381.867 424.533v-86.186c167.253-22.187 296.533-165.547 296.533-338.347s-129.28-316.16-296.533-338.347v-86.186c214.613 22.613 381.867 204.373 381.867 424.533z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE139"],"defaultCode":57657,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":595,"order":46,"ligatures":"","prevSize":32,"code":57657,"name":"playlist"},"setIdx":0,"setId":0,"iconIdx":594},{"icon":{"paths":["M384 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667c-44.8 0-87.467-6.827-128-19.627 173.227-54.187 298.667-215.893 298.667-407.040s-125.44-352.853-298.667-407.040c40.533-12.8 83.2-19.627 128-19.627z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A2"],"defaultCode":58018,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":607,"order":34,"ligatures":"","prevSize":32,"code":58018,"name":"night"},"setIdx":0,"setId":0,"iconIdx":606},{"icon":{"paths":["M512 85.333c235.947 0 426.667 190.72 426.667 426.667s-190.72 426.667-426.667 426.667-426.667-190.72-426.667-426.667 190.72-426.667 426.667-426.667zM725.333 665.173l-153.173-153.173 153.173-153.173-60.16-60.16-153.173 153.173-153.173-153.173-60.16 60.16 153.173 153.173-153.173 153.173 60.16 60.16 153.173-153.173 153.173 153.173z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE38F"],"defaultCode":58255,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":662,"order":50,"ligatures":"","prevSize":32,"code":58255,"name":"cancel"},"setIdx":0,"setId":0,"iconIdx":661},{"icon":{"paths":["M512 170.667c188.587 0 341.333 152.746 341.333 341.333 0 66.987-19.626 129.28-52.906 181.76l-62.294-62.293c19.2-35.414 29.867-76.374 29.867-119.467 0-141.227-114.773-256-256-256v128l-170.667-170.667 170.667-170.666v128zM512 768v-128l170.667 170.667-170.667 170.666v-128c-188.587 0-341.333-152.746-341.333-341.333 0-66.987 19.626-129.28 52.906-181.76l62.294 62.293c-19.2 35.414-29.867 76.374-29.867 119.467 0 141.227 114.773 256 256 256z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE116"],"defaultCode":57622,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":709,"order":17,"ligatures":"","prevSize":32,"code":57622,"name":"sync"},"setIdx":0,"setId":0,"iconIdx":708},{"icon":{"paths":["M384 689.92l451.84-451.413 60.16 60.16-512 512-238.507-238.507 60.587-60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE390"],"defaultCode":58256,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":733,"order":56,"ligatures":"","prevSize":32,"code":58256,"name":"confirm"},"setIdx":0,"setId":0,"iconIdx":732},{"icon":{"paths":["M853.333 370.773l141.227 141.227-141.227 141.227v200.106h-200.106l-141.227 141.227-141.227-141.227h-200.106v-200.106l-141.227-141.227 141.227-141.227v-200.106h200.106l141.227-141.227 141.227 141.227h200.106v200.106zM512 768c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.773-256 256 114.773 256 256 256zM512 341.333c94.293 0 170.667 76.374 170.667 170.667s-76.374 170.667-170.667 170.667-170.667-76.374-170.667-170.667 76.374-170.667 170.667-170.667z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A6"],"defaultCode":58022,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":785,"order":15,"ligatures":"","prevSize":32,"code":58022,"name":"brightness"},"setIdx":0,"setId":0,"iconIdx":784},{"icon":{"paths":["M85.333 725.333v-42.666h128v170.666h-128v-42.666h85.334v-21.334h-42.667v-42.666h42.667v-21.334h-85.334zM128 341.333v-128h-42.667v-42.666h85.334v170.666h-42.667zM85.333 469.333v-42.666h128v38.4l-76.8 89.6h76.8v42.666h-128v-38.4l76.8-89.6h-76.8zM298.667 213.333h597.333v85.334h-597.333v-85.334zM298.667 810.667v-85.334h597.333v85.334h-597.333zM298.667 554.667v-85.334h597.333v85.334h-597.333z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE22D"],"defaultCode":57901,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":797,"order":58,"ligatures":"","prevSize":32,"code":57901,"name":"nodes"},"setIdx":0,"setId":0,"iconIdx":796},{"icon":{"paths":["M810.667 554.667h-256v256h-85.334v-256h-256v-85.334h256v-256h85.334v256h256v85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE18A"],"defaultCode":57738,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":803,"order":59,"ligatures":"","prevSize":32,"code":57738,"name":"add"},"setIdx":0,"setId":0,"iconIdx":802},{"icon":{"paths":["M128 736l471.893-471.893 160 160-471.893 471.893h-160v-160zM883.627 300.373l-78.080 78.080-160-160 78.080-78.080c16.64-16.64 43.52-16.64 60.16 0l99.84 99.84c16.64 16.64 16.64 43.52 0 60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2C6"],"defaultCode":58054,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":834,"order":72,"ligatures":"","prevSize":32,"code":58054,"name":"edit"},"setIdx":0,"setId":0,"iconIdx":833},{"icon":{"paths":["M576 28.587c166.827 133.546 277.333 338.773 277.333 568.746 0 188.587-152.746 341.334-341.333 341.334s-341.333-152.747-341.333-341.334c0-144.213 51.626-276.906 137.813-379.306l-1.28 15.36c0 87.893 66.56 159.146 154.88 159.146 87.893 0 145.493-71.253 145.493-159.146 0-91.734-31.573-204.8-31.573-204.8zM499.627 810.667c113.066 0 204.8-91.734 204.8-204.8 0-59.307-8.534-117.334-25.174-172.374-43.52 58.454-121.6 94.72-197.12 110.080-75.093 15.36-119.893 64-119.893 133.12 0 74.24 61.44 133.974 137.387 133.974z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE409"],"defaultCode":58377,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":871,"order":10,"ligatures":"","prevSize":32,"code":58377,"name":"intensity"},"setIdx":0,"setId":0,"iconIdx":870},{"icon":{"paths":["M938.667 394.24l-232.534 201.813 69.547 299.947-263.68-159.147-263.68 159.147 69.973-299.947-232.96-201.813 306.774-26.027 119.893-282.88 119.893 282.454zM512 657.067l160.853 97.28-42.666-182.614 141.653-122.88-186.88-16.213-72.96-172.373-72.533 171.946-186.88 16.214 141.653 122.88-42.667 182.613z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE410"],"defaultCode":58384,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":927,"order":9,"ligatures":"","prevSize":32,"code":58384,"name":"star"},"setIdx":0,"setId":0,"iconIdx":926},{"icon":{"paths":["M512 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667-426.667-191.147-426.667-426.667 191.147-426.667 426.667-426.667zM554.667 725.333v-256h-85.334v256h85.334zM554.667 384v-85.333h-85.334v85.333h85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE066"],"defaultCode":57446,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":952,"order":62,"ligatures":"","prevSize":32,"code":57446,"name":"info"},"setIdx":0,"setId":0,"iconIdx":951},{"icon":{"paths":["M256 810.667v-512h512v512c0 46.933-38.4 85.333-85.333 85.333h-341.334c-46.933 0-85.333-38.4-85.333-85.333zM810.667 170.667v85.333h-597.334v-85.333h149.334l42.666-42.667h213.334l42.666 42.667h149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE037"],"defaultCode":57399,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":969,"order":55,"ligatures":"","prevSize":32,"code":57399,"name":"del"},"setIdx":0,"setId":0,"iconIdx":968},{"icon":{"paths":["M704 128c131.413 0 234.667 103.253 234.667 234.667 0 161.28-145.067 292.693-364.8 491.946l-61.867 56.32-61.867-55.893c-219.733-199.68-364.8-331.093-364.8-492.373 0-131.414 103.254-234.667 234.667-234.667 74.24 0 145.493 34.56 192 89.173 46.507-54.613 117.76-89.173 192-89.173zM516.267 791.467c203.093-183.894 337.066-305.494 337.066-428.8 0-85.334-64-149.334-149.333-149.334-65.707 0-129.707 42.24-151.893 100.694h-79.787c-22.613-58.454-86.613-100.694-152.32-100.694-85.333 0-149.333 64-149.333 149.334 0 123.306 133.973 244.906 337.066 428.8l4.267 4.266z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE04C"],"defaultCode":57420,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":1034,"order":61,"ligatures":"","prevSize":32,"code":57420,"name":"presets"},"setIdx":0,"setId":0,"iconIdx":1033}],"height":1024,"metadata":{"name":"wled122"},"preferences":{"showGlyphs":true,"showCodes":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"i-","metadata":{"fontFamily":"wled122","majorVersion":1,"minorVersion":7},"metrics":{"emSize":1024,"baseline":20,"whitespace":0},"embed":false,"autoHost":true,"noie8":true,"ie7":false,"showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":true},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215},"historySize":50,"quickUsageToken":{"MainUI":false},"showLiga":false}} \ No newline at end of file diff --git a/wled00/data/icons-ui/style.css b/wled00/data/icons-ui/style.css new file mode 100644 index 0000000000..59982557d0 --- /dev/null +++ b/wled00/data/icons-ui/style.css @@ -0,0 +1,96 @@ +@font-face { + font-family: 'wled122'; + src: + url('fonts/wled122.woff2?e3eban') format('woff2'), + url('fonts/wled122.ttf?e3eban') format('truetype'), + url('fonts/wled122.woff?e3eban') format('woff'), + url('fonts/wled122.svg?e3eban#wled122') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + +[class^="i-"], [class*=" i-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'wled122' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.i-pattern:before { + content: "\e23d"; +} +.i-segments:before { + content: "\e34b"; +} +.i-sun:before { + content: "\e333"; +} +.i-palette:before { + content: "\e2b3"; +} +.i-eye:before { + content: "\e0e8"; +} +.i-speed:before { + content: "\e325"; +} +.i-expand:before { + content: "\e395"; +} +.i-power:before { + content: "\e08f"; +} +.i-settings:before { + content: "\e0a2"; +} +.i-playlist:before { + content: "\e139"; +} +.i-night:before { + content: "\e2a2"; +} +.i-cancel:before { + content: "\e38f"; +} +.i-sync:before { + content: "\e116"; +} +.i-confirm:before { + content: "\e390"; +} +.i-brightness:before { + content: "\e2a6"; +} +.i-nodes:before { + content: "\e22d"; +} +.i-add:before { + content: "\e18a"; +} +.i-edit:before { + content: "\e2c6"; +} +.i-intensity:before { + content: "\e409"; +} +.i-star:before { + content: "\e410"; +} +.i-info:before { + content: "\e066"; +} +.i-del:before { + content: "\e037"; +} +.i-presets:before { + content: "\e04c"; +} diff --git a/wled00/data/index.css b/wled00/data/index.css index c67b07e53e..1b34285dc0 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -16,7 +16,10 @@ --c-c: #ccc; --c-e: #eee; --c-d: #ddd; - --c-r: #831; + --c-r: #c32; + --c-g: #2c1; + --c-l: #48a; + --c-y: #a90; --t-b: 0.5; --c-o: rgba(34, 34, 34, 0.9); --c-tb : rgba(34, 34, 34, var(--t-b)); @@ -29,6 +32,7 @@ --tbp: 14px 14px 10px 14px; --bbp: 9px 0 7px 0; --bhd: none; + --sgp: "block"; --bmt: 0px; } @@ -44,8 +48,10 @@ body { color: var(--c-f); text-align: center; -webkit-touch-callout: none; - -webkit-user-select: none; - user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; -webkit-tap-highlight-color: transparent; scrollbar-width: 6px; scrollbar-color: var(--c-sb) transparent; @@ -73,8 +79,13 @@ body { p { margin: 10px 0 2px 0; +} +a, p, a:visited { color: var(--c-d); } +a, a:visited { + text-decoration: none; +} button { outline: none; @@ -89,10 +100,11 @@ button { #namelabel { position: fixed; bottom: calc(var(--bh) + 6px); - right: 4px; - color: var(--c-6); + right: 6px; + color: var(--c-8); /* set bright (--c-d) with dark text shadow (see below) to be legible on gray background (in image) */ cursor: pointer; writing-mode: vertical-rl; + /* transform: rotate(180deg); */ } .bri { @@ -111,9 +123,21 @@ button { .icons { font-family: 'WIcons'; font-style: normal; - font-size: 24px; - line-height: 1; + font-size: 24px !important; + line-height: 1 !important; display: inline-block; +} + +.on { + color: var(--c-g) !important; +} + +.off { + color: var(--c-6) !important; + /* cursor: default !important; */ +} + +.top .icons, .bot .icons { margin: -2px 0 4px 0; } @@ -121,75 +145,107 @@ button { font-size: 42px; } -.infot { +.segt, .plentry TABLE { table-layout: fixed; width: 100%; } -.segtd { +.segt TD { + padding: 2px 0 !important; text-align: center; - text-transform: uppercase; - font-size: 14px; + /*text-transform: uppercase;*/ +} +.segt TD, .plentry TD { + font-size: 13px; + padding: 0; + vertical-align: middle; } .keytd { text-align: left; - padding-bottom: 8px; } .valtd { text-align: right; - padding-bottom: 8px; } -.slider-icon -{ - transform: translate(6px,3px); - color: var(--c-d); +.valtd i { + font-size: small; } -.e-icon -{ - transform: translateY(3px); - color: var(--c-d); +.slider-icon { + position: absolute; + left: 8px; + bottom: 5px; } .sel-icon { transform: translateX(3px); +} + +.e-icon, .g-icon, .sel-icon, .slider-icon { + cursor: pointer; color: var(--c-d); } -.edit-icon { +.g-icon { + font-style: normal; position: absolute; - right: -26px; - top: 10px; - display: none; + top: 8px; + right: 8px; +} + +/* pop-up container */ +.pop { + position: absolute; + display: inline-block; + top: 0; + right: 0; } +/* pop-up content (segment sets) */ +.pop-c { + position: absolute; + background-color: var(--c-2); + border: 1px solid var(--c-8); + border-radius: 20px; + z-index: 1; + top: 3px; + right: 35px; + padding: 3px 8px 1px; + font-size: 24px; + line-height: 24px; +} +.pop-c span { + padding: 2px 6px; +} + .search-icon { position: absolute; - left: 8px; - top: 10px; - pointer-events: none; + top: 8px; + left: 12px; + width: 24px; + height: 24px; } -.search-cancel-icon { +.clear-icon { position: absolute; - right: 8px; - top: 9px; + top: 8px; + right: 9px; cursor: pointer; - display: none; } .flr { - float: right; - cursor: pointer; - margin: 0; color: var(--c-f); transform: rotate(0deg); transition: transform 0.3s; + position: absolute; + top: 0; + right: 0; + padding: 8px; } +.expanded .flr, .exp { transform: rotate(180deg); } @@ -201,11 +257,20 @@ button { #liveview { height: 4px; - display: none; width: 100%; border: 0px; } +#liveview2D { + height: 90%; + width: 90%; + border: 0px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); +} + .tab { background-color: transparent; color: var(--c-d); @@ -226,15 +291,18 @@ button { transition: color 0.3s, background-color 0.3s; font-size: 17px; color: var(--c-c); + min-width: 44px; } .top button { padding: var(--tbp); + margin: 0; } .bot button { padding: var(--bbp); width:25%; + margin: 0; } .tab button:hover { @@ -267,19 +335,37 @@ button { position: relative; width: 100%; width: calc(100%/var(--n)); - padding: 11px 0; box-sizing: border-box; border: 0px; overflow: auto; height: 100%; overscroll-behavior: none; + padding: 0 4px; + -webkit-overflow-scrolling: touch; } -#Effects { - padding-top: 0; - margin-top: 11px; - height: calc(100% - 11px); - -webkit-overflow-scrolling: touch; +#segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, +.fnd { + max-width: 280px; + font-size: 19px; +} + +#putil, #segutil, #segutil2 { + min-height: 42px; + margin: 13px auto 0; +} + +#segutil .segin { + padding-top: 12px; +} + +#fx, #pql, #segcont, #pcont, #sliders, #picker, #qcs-w, #hexw, #pall, #ledmap, +.slider, .filter, .option, .segname, .pname, .fnd { + margin: 0 auto; +} + +#putil { + padding: 5px 0 0; } .smooth { transition: transform calc(var(--f, 1)*.5s) ease-out } @@ -307,26 +393,129 @@ button { pointer-events: none; } -.staytop { +.staytop, .staybot { display: block; + position: -webkit-sticky; + position: sticky !important; + top: 0; + z-index: 2; + margin: 0 auto auto; +} + +.staybot { + bottom: 5px; +} + +#sliders { position: -webkit-sticky; position: sticky; - background: var(--c-1); - top: -1px; + bottom: 0; + max-width: 300px; +} + +#sliders .labels { + padding-top: 3px; + font-size: small; +} + +.slider { + /*max-width: 300px;*/ + /* margin: 5px auto; add 5px; if you want some vertical space but looks ugly */ + border-radius: 24px; + position: relative; + padding-bottom: 2px; +} + +/* Slider wrapper div */ +.sliderwrap { + height: 30px; + width: 230px; + max-width: 230px; + position: relative; + z-index: 0; +} + +#sliders .slider { + padding-right: 64px; /* offset for bubble */ +} + +#sliders .slider, #info .slider { + background-color: var(--c-2); +} + +#sliders .sliderwrap, .sbs .sliderwrap { + left: 32px; /* offset for icon */ +} + +.filter, .option { + background-color: var(--c-4); + border-radius: 26px; + height: 26px; + max-width: 300px; + /* margin: 0 auto 4px; add 4-8px if you want space at the bottom */ + padding: 4px 2px; + position: relative; + opacity: 1; + transition: opacity 0.5s linear, height 0.5s, transform 0.5s; +} + +.filter { z-index: 1; - margin-top: 1px; - width: 272px; - margin: auto; - border-radius: 25px; + overflow: hidden; } -#staytop1 { - top: 28px; +/* Tooltip text */ +.slider .tooltiptext, .option .tooltiptext { + visibility: hidden; + background-color: var(--c-5); + /*border: 2px solid var(--c-2);*/ + box-shadow: 4px 4px 10px 4px var(--c-1); + color: var(--c-f); + text-align: center; + padding: 4px 8px; + border-radius: 6px; + + /* Position the tooltip text */ + width: 160px; + position: absolute; + z-index: 1; + bottom: 80%; + left: 50%; + margin-left: -92px; + + /* Ensure tooltip goes away when mouse leaves control */ + pointer-events: none; + + /* Fade in tooltip */ + opacity: 0; + transition: opacity 0.75s; +} +.option .tooltiptext { + bottom: 120%; +} +/* Tooltip arrow */ +.slider .tooltiptext::after, .option .tooltiptext::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: var(--c-5) transparent transparent transparent; +} +/* Show the tooltip text when you mouse over the tooltip container */ +.slider:hover .tooltiptext, .option .check:hover .tooltiptext { + visibility: visible; + opacity: 1; } -#fxb0 { - margin-bottom: 2px; - filter: drop-shadow(0 0 1px #000); +.fade { + visibility: hidden; /* hide it */ + opacity: 0; /* make it transparent */ + transform: scaleY(0); /* shrink content */ + height: 0px; /* force other elements to move */ + padding: 0; /* remove empty space */ } .first { @@ -336,11 +525,12 @@ button { #toast { opacity: 0; background-color: var(--c-5); + border: 1px solid var(--c-2); max-width: 90%; color: var(--c-f); text-align: center; border-radius: 5px; - padding: 16px; + padding: 22px; position: fixed; z-index: 5; left: 50%; @@ -375,31 +565,29 @@ button { overflow: auto; } -.modal button:hover { - background-color: var(--c-4); +.close { + position: -webkit-sticky; + position: sticky; + top: 0; + float: right; } -#info { - z-index: 3; +#info, #nodes { + z-index: 4; } -#rover, #nodes { - z-index: 2; +#rover { + z-index: 3; } #ndlt { margin: 12px 0; } -.valtd i { - font-size: 14px; -} - #roverstar { position: fixed; top: calc(var(--th) + 5px); left: 1px; - display: none; cursor: pointer; } @@ -413,18 +601,38 @@ button { z-index: -2; } -#imgw { - display: inline-block; - margin: 8px; +#info .slider { + max-width: 200px; + min-width: 145px; + float: right; + margin: 0; +} +#info .sliderwrap { + width: 200px; } -#kv, #kn { +#info table, #nodes table { + table-layout: fixed; + width: 100%; +} + +#info td, #nodes td { + padding-bottom: 8px; +} + +#info .btn { + margin: 5px; +} +#info table .btn, #nodes table .btn { + margin: 0; +} +#info div, #nodes div { max-width: 490px; - display: inline-block; + margin: 0 auto; } -#kn td { - padding-bottom: 12px; +#info #imgw { + margin: 8px auto; } #lv { @@ -457,44 +665,56 @@ img { .sliderdisplay { content:''; position: absolute; - top: 12.5px; bottom: 12.5px; - left: 13px; right: 13px; + top: 12px; left: 8px; right: 8px; + height: 5px; background: var(--c-4); - border-radius: 17px; + border-radius: 16px; pointer-events: none; z-index: -1; --bg: var(--c-f); } -#rwrap .sliderdisplay { --bg: #f00; } -#gwrap .sliderdisplay { --bg: #0f0; } -#bwrap .sliderdisplay { --bg: #00f; } -#wbal .sliderdisplay, #kwrap .sliderdisplay { - background: linear-gradient(90deg, #ff8f1f 0%, #fff 50%, #cbdbff); +#rwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #f00); } /* -15% since #000 is too dark */ +#gwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #0f0); } /* -15% since #000 is too dark */ +#bwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #00f); } /* -15% since #000 is too dark */ +#wwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #fff); } /* -15% since #000 is too dark */ +#kwrap .sliderdisplay, +#wbal .sliderdisplay { background: linear-gradient(90deg, #ff8f1f 0%, #fff 50%, #cbdbff); } + +/* wrapper divs hidden by default */ +#liveview, #liveview2D, #roverstar, #pql +#rgbwrap, #swrap, #hwrap, #kwrap, #wwrap, #wbal, #qcs-w, #hexw, +.clear-icon, .edit-icon, .ptxt { + display: none; } .sliderbubble { - width: 36px; - line-height: 24px; - background: var(--c-3); + width: 24px; position: absolute; - transform: translateX(-50%); - border-radius: 12px; - margin-left: 12px; - margin-top: 3px; - padding: 0px; - display: inline; + display: inline-block; + border-radius: 16px; + background: var(--c-3); + color: var(--c-f); + padding: 4px; + font-size: 14px; + right: 6px; + transition: visibility .25s ease,opacity .25s ease; + opacity: 0; + visibility: hidden; + /* left: 8px; */ + top: 4px; } -.hidden { - display: none; +output.sliderbubbleshow { + visibility: visible; + opacity: 1; } input[type=range] { -webkit-appearance: none; - width: 220px; - padding: 0px; - margin: 0px 10px 0px 10px; + width: 100%; + padding: 0; + margin: 0; background-color: transparent; cursor: pointer; } @@ -511,7 +731,7 @@ input[type=range]::-webkit-slider-runnable-track { input[type=range]::-webkit-slider-thumb { height: 16px; width: 16px; - border-radius: 17px; + border-radius: 50%; background: var(--c-f); cursor: pointer; -webkit-appearance: none; @@ -526,34 +746,22 @@ input[type=range]::-moz-range-thumb { border: 0px solid rgba(0, 0, 0, 0); height: 16px; width: 16px; - border-radius: 17px; + border-radius: 50%; background: var(--c-f); - transform: translateY(7px); -} -input[type=range]:active + .sliderbubble { - display: inline; - transform: translateX(-50%); + transform: translateY(5px); } -/* hide color controls until enabled in updateUI() */ -#pwrap, #wwrap, #wbal, #rgbwrap, #palwrap { - display: none; +#Colors input[type=range]::-webkit-slider-thumb { + height: 18px; + width: 18px; + border: 2px solid var(--c-1); + margin-top: 5px; } - -/* Slider wrapper div */ -.sliderwrap { - height: 30px; - width: 240px; - position: relative; +#Colors input[type=range]::-moz-range-thumb { + border: 2px solid var(--c-1); } -/* Segment power button + brightness slider wrapper div */ -.sbs { - margin: 0px -20px 5px -6px; -} - -/* Segment brightness slider wrapper div */ -.sws { - margin-left: -7px; +#Colors .sliderwrap { + margin: 4px 0 0; } /* Dynamically hide brightness slider label */ @@ -562,192 +770,201 @@ input[type=range]:active + .sliderbubble { } #briwrap { + min-width: 267px; float: right; margin-top: var(--bmt); } #picker { - margin: 10px auto; - width: 260px; + margin-top: 8px !important; + max-width: 260px; } +/* buttons */ .btn { padding: 8px; - margin: 10px; + margin: 10px 4px; width: 230px; font-size: 19px; - background-color: var(--c-3); - color: var(--c-f); + color: var(--c-d); cursor: pointer; - border: 0px solid white; border-radius: 25px; - transition-duration: 0.5s; - /*-webkit-backface-visibility: hidden; - -webkit-transform:translate3d(0,0,0);*/ + transition-duration: 0.3s; + -webkit-backface-visibility: hidden; + -webkit-transform:translate3d(0,0,0); + backface-visibility: hidden; + transform:translate3d(0,0,0); + overflow: hidden; + text-overflow: ellipsis; + border: 1px solid var(--c-3); + background-color: var(--c-3); } - -/* Small round button (color selectors, icon-only round buttons) */ -.xxs { - width: 40px; - height: 40px; - margin: 6px; +#segutil .btn-s:hover, +#segutil2 .btn-s:hover, +#putil .btn-s:hover, +.btn:hover { + border: 1px solid var(--c-5) /*!important*/; + background-color: var(--c-5) /*!important*/; } - -/* Segments/presets auxiliary buttons (Add segment, Create preset, ...) */ .btn-s { - width: 276px; - background-color: var(--c-2); -} -.btn-i { - padding-bottom: 4px; + width: 100%; + margin: 0; } .btn-icon { - margin: 0px 8px 4px 0; + margin: -4px 4px -1px 0; vertical-align: middle; + display: inline-block; +} +.btn-n { + width: 230px; + margin: 0 8px 0 0; } - -/* Wide button used in presets (Save, playlist test, delete) */ .btn-p { - width: 216px; + width: 120px; + margin: 5px 0; +} +.btn-xs, .btn-pl-del, .btn-pl-add { + width: 42px !important; + height: 42px !important; + text-overflow: clip; +} +.btn-xs { + margin: 2px 0 0 0; +} +#putil .btn-xs { + margin: 0; +} +#info .btn-xs { + border: 1px solid var(--c-4); +} + +#putil .btn-s { + width: 135px; +} + +#nodes .infobtn { + margin: 0; } -/* Delete preset from playlist button */ -.btn-pl-del { - margin: 0 0 0 3px; +#segutil .btn-s, #segutil2 .btn-s, #putil .btn-s { + background-color: var(--c-3); + border: 1px solid var(--c-3); } -/* Add preset to playlist "+" button */ -.btn-pl-add { - margin: 3px 0 0 8px; +.btn-pl-del, .btn-pl-add { + margin: 0; + white-space: nowrap; } -/* Quick color select buttons wrapper div */ +/* Quick color select wrapper div */ #qcs-w { margin-top: 10px; - display: none; } /* Quick color select buttons */ .qcs { - padding: 14px; margin: 2px; border-radius: 14px; display: inline-block; + width: 28px; + height: 28px; + line-height: 28px; } /* Quick color select Black button (has white border) */ .qcsb { - padding: 13px; - border: 1px solid var(--c-f); + width: 26px; + height: 26px; + line-height: 26px; + border: 1px solid #fff; } /* Hex color input wrapper div */ #hexw { margin-top: 5px; - display: none; } -/* Transition time input */ +select { + padding: 4px 8px; + margin: 0; + font-size: 19px; + background-color: var(--c-3); + color: var(--c-d); + cursor: pointer; + border: 0 solid var(--c-2); + border-radius: 20px; + transition-duration: 0.5s; + -webkit-backface-visibility: hidden; + -webkit-transform:translate3d(0,0,0); + -webkit-appearance: none; + -moz-appearance: none; + backface-visibility: hidden; + transform:translate3d(0,0,0); + text-overflow: ellipsis; +} #tt { text-align: center; } - -/* Color slot select buttons (1,2,3) */ .cl { - width: 38.5px; - height: 38.5px; - margin: 7px; background-color: #000; - box-shadow: 0 0 0 1.5px #fff; } -.selected.cl { - box-shadow: 0 0 0 5px #fff; -} - -/* Playlist preset select */ -.sel-pl { - width: 192px; - background-position: 168px 16px; - margin: 8px 3px 0 0; +select.sel-p, select.sel-pl, select.sel-ple { + margin: 5px 0; + width: 100%; + height: 40px; } - -/* Playlist end preset select */ -.sel-ple { - width: 216px; - background-position: 192px 16px; +div.sel-p { + position: relative; } - -select { - -webkit-appearance: none; - appearance: none; - background: url("data:image/svg+xml;utf8,") no-repeat; - background-size: 12px; - background-position: 206px 16px; - padding-left: 12px !important; - background-repeat: no-repeat; - outline: none; +div.sel-p:after { + content: ""; + position: absolute; + right: 10px; + top: 22px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid var(--c-f); +} +select.sel-ple { + text-align: center; } -select:-moz-focusring { - transition-duration: 0s; - color: transparent; - text-shadow: 0 0 0 #fff; +select.sel-sg { + margin: 5px 0; + height: 40px; } option { background-color: var(--c-3); color: var(--c-f); } - -input[type=number], input[type=text] { +input[type=number], +input[type=text] { background: var(--c-3); color: var(--c-f); - border: 0px solid white; - border-radius: 25px; + border: 0px solid var(--c-2); + border-radius: 10px; padding: 8px; - margin: 6px 6px 6px 0; + /*margin: 6px 6px 6px 0;*/ font-size: 19px; transition: background-color 0.2s; outline: none; - width: 50px; -webkit-appearance: textfield; - appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; } -textarea { - background: var(--c-2); - color: var(--c-f); - width: 236px; - height: 90px; - border-radius: 5px; - border: 2px solid #555; - outline: none; - resize: none; - font-size: 19px; -} - -::selection { - background: var(--c-b); +input[type=number] { + text-align: right; + width: 50px; } input[type=text] { - width: 100px; text-align: center; } -input[type=text].ptxt { - width: 200px; - margin: 26px 0 6px 12px; -} - -input[type=text].stxt { - display: none; - margin-top: 6px; -} - -input[type=text].qltxt { - width: 50px; -} - -input[type=number]:focus, input[type=text]:focus { +input[type=number]:focus, +input[type=text]:focus { background: var(--c-6); } @@ -756,52 +973,70 @@ input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; } -.pln { - width: 67px !important; - margin: 0 2px 8px 0 !important; - text-align: center; +#hexw input[type=text] { + width: 6em; } -.plnl { - width: 86px; - margin: 0 2px 0 0; - display: inline-block; + +input[type=text].ptxt { + width: calc(100% - 24px); } -.pli { - width: 38px; - margin: 0 0 0 29px; - display: inline-block; + +textarea { + background: var(--c-2); + color: var(--c-f); + width: calc(100% - 14px); /* +padding=260px */ + height: 90px; + border-radius: 5px; + border: 2px solid var(--c-5); + outline: none; + resize: none; + font-size: 19px; + padding: 5px; } -.segn { - border-radius: 5px !important; - margin: 3px 0 6px 0 !important; +.apitxt { + height: 7em; } -.pname, .plname, .segname { - position: absolute; - left: 50%; - transform: translateX(-50%); - white-space: nowrap; - cursor: pointer; +::selection { + background: var(--c-b); } -.segntxt { - max-width: 160px; - overflow: hidden; - text-overflow: clip; +.ptxt { + margin: -1px 4px 8px !important; } -.segname { - top: 0px; - padding: 9px 0; +.stxt { + width: 50px !important; } -.pname, .plname { - width: 208px; - padding: 8px 0; + +.segname, .pname, .bname { + white-space: nowrap; text-align: center; overflow: hidden; - text-overflow: clip; + text-overflow: ellipsis; + line-height: 24px; + padding: 8px 24px; + max-width: 170px; + position: relative; +} +.bname { + padding: 0 24px; +} + +.segname .flr, .pname .flr { + transform: rotate(0deg); + right: -6px; } + +/* segment power wrapper */ +.sbs { + /*padding: 1px 0 1px 20px;*/ + display: var(--sgp); + width: 100%; + position: relative; +} + .pname { top: 1px; } @@ -809,13 +1044,12 @@ input[type=number]::-webkit-outer-spin-button { top:0; } +/* preset id number */ .pid { position: absolute; - top: 0px; - left: 0px; - padding: 11px 0px 0px 11px; + top: 8px; + left: 12px; font-size: 16px; - width: 20px; text-align: center; color: var(--c-b); } @@ -828,29 +1062,36 @@ input[type=number]::-webkit-outer-spin-button { padding: 6px 0 0 0; } -/* Quick preset select buttons */ -.psts { - background-color: var(--c-3); - color: var(--c-f); - cursor: pointer; - padding: 2px 0 0 0; - height: 40px; +.xxs { + width: 44px; + height: 44px; + margin: 5px; +} + +.xxs-w { + margin: 2px; + width: 50px; + height: 50px; +} + +#csl .xxs { + border: 2px solid var(--c-d) !important; +} +#csl .xxs-w { + border-width: 5px !important; +} + +.qcs, #namelabel { /* text shadow for name to be legible on grey backround */ + text-shadow: -1px -1px 0 var(--c-1), 1px -1px 0 var(--c-1), -1px 1px 0 var(--c-1), 1px 1px 0 var(--c-1); } -/* Segment apply button (checkmark) */ -.cnf { +.psts { color: var(--c-f); - cursor: pointer; - background: var(--c-3); - border-radius: 5px; - padding: 8.5px 21px 5px; - display: inline; + margin: 4px; } -/* Segment power button icon */ .pwr { color: var(--c-6); - transform: translate(2px, 3px); cursor: pointer; } @@ -864,30 +1105,30 @@ input[type=number]::-webkit-outer-spin-button { right: 8px; } -.check, .radio { - display: inline-block; - position: relative; - padding-bottom: 32px; - margin-bottom: 14px; +.frz { + left: 10px; + position: absolute; + top: 8px; cursor: pointer; - text-align: center; + z-index: 1; } -.schkl { - padding: 2px 5px 0px 35px; - margin: 0 0 0 2px; +/* radiobuttons and checkmarks */ +.check, .radio { + display: block; + position: relative; + cursor: pointer; } .revchkl { - padding: 2px 0px 0px 35px; + padding: 4px 0px 0px 35px; margin-bottom: 0px; margin-top: 8px; } -.fxchkl { - position: absolute; - top: 0px; - left: 8px; +TD .revchkl { + padding: 0 0 0 32px; + margin-top: 0; } .check input, .radio input { @@ -900,82 +1141,69 @@ input[type=number]::-webkit-outer-spin-button { .checkmark, .radiomark { position: absolute; - left: 0; height: 24px; width: 24px; - background-color: var(--c-4); - border-radius: 10px; - /*border: 1px solid var(--c-2);*/ -} - -.checkmark { - top: 6px; + top: 0; + bottom: 0; + left: 0; + background-color: var(--c-3); + border: 1px solid var(--c-2); } .radiomark { + top: 8px; + left: 8px; + height: 22px; + width: 22px; border-radius: 50%; background-color: transparent; - top: 7px; } -.schk { - top: 0; -} - -.psv { - left: initial; - bottom: initial; - top: 0; - right: 0; -} - -.psvl { - padding: 2px 35px 10px 0px; - margin-top: 10px; - margin-bottom: 0px; +.checkmark { + border-radius: 10px; } - +.radio:hover input ~ .radiomark, .check:hover input ~ .checkmark { background-color: var(--c-5); } -.check input:checked ~ .checkmark { - background-color: var(--c-6); -} - .checkmark:after, .radiomark:after { content: ""; position: absolute; display: none; } -.check input:checked ~ .checkmark:after, .radio input:checked ~ .radiomark:after { - display: block; -} - .check .checkmark:after { - left: 8px; + left: 9px; top: 4px; width: 5px; height: 10px; border: solid var(--c-f); border-width: 0 3px 3px 0; +} + +.rot45, +.check .checkmark:after { -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); } .radio .radiomark:after { - width: 12px; - height: 12px; + width: 14px; + height: 14px; top: 50%; left: 50%; - margin: -6px; + margin: -7px; border-radius: 50%; background: var(--c-f); } +TD .checkmark, TD .radiomark { + top: -6px; +} + .h { font-size: 13px; text-align: center; @@ -983,131 +1211,221 @@ input[type=number]::-webkit-outer-spin-button { } .bp { - margin-bottom: 5px; + margin-bottom: 8px; } +/* segment & preset wrapper */ .seg, .pres { - position: relative; - display: inline-block; - padding: 8px; - margin: 10px; - width: 260px; - font-size: 19px; background-color: var(--c-2); - color: var(--c-f); - border: 0px solid white; - border-radius: 20px; + /*color: var(--c-f);*/ /* seems to affect only the Add segment button, which should be same color as reset segments */ + border: 0px solid var(--c-f); text-align: left; transition: background-color 0.5s; - filter: brightness(1); /* required for slider background to render? */ + border-radius: 21px; } -.selected { - background-color: var(--c-4); +.seg { + top: auto !important; /* prevent sticky */ + bottom: auto !important; +} +/* checkmark labels */ +.seg .schkl { + position: absolute; + top: 7px; + left: 9px; +} +/* checkmark labels */ +.filter .fchkl, .option .ochkl { + display: inline-block; + min-width: 0.7em; + padding: 1px 4px 4px 32px; + text-align: left; + line-height: 24px; + vertical-align: middle; + -webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ + filter: grayscale(100%); } -/* "selected" CSS class is applied to the segment when it is the main segment. - By default, do not highlight. Can be overridden by skin.css */ -.selected.seg { - background-color: var(--c-2); /* var(--c-4); */ + +.lbl-l { + font-size: 13px; + text-align: center; + padding: 4px 0; } -.selected .checkmark, .selected .radiokmark { - background-color: var(--c-4); /* var(--c-6); */ + +.lbl-s { + display: inline-block; + margin-top: 6px; + font-size: 13px; + width: 48%; + text-align: center; } +/* list wrapper */ .list { position: relative; transition: background-color 0.5s; - margin: auto auto 10px; - padding-bottom: 10px; - width: 230px; + margin: auto auto 10px; + line-height: 24px; } +/* list item */ .lstI { - position: sticky; + align-items: center; + cursor: pointer; + background-color: var(--c-2); overflow: hidden; + position: -webkit-sticky; + position: sticky; + border-radius: 21px; + margin: 13px auto 0; + min-height: 40px; + border: 1px solid var(--c-2); } -.fxbtn { - margin: 20px auto; - padding: 8px 0; +#segutil .lstI { + margin-top: 0; } -.lstI:hover { +/* selected item/element */ +.selected { /* has to be after .lstI since !important is not ok */ background: var(--c-4); } -.lstI:last-child { - border: none; +#segcont .seg:hover:not([class*="expanded"]), +.lstI:hover:not([class*="expanded"]) { + background: var(--c-5); } -.lstI.sticky, .lstI.selected { +.selected .checkmark, +.selected .radiomark, +.selected input[type=number], +.selected input[type=text] { + background-color: var(--c-3); +} + +/* selected list item */ +.lstI.selected { + top: 0; + bottom: 0; + border: 1px solid var(--c-4); +} + +.lstI.sticky, +.lstI.selected { z-index: 1; + box-shadow: 0px 0px 10px 4px var(--c-1); } -#pallist .lstI.selected { - top: 27px; - bottom: -11px; +#pcont .selected:not([class*="expanded"]) { + bottom: 52px; + top: 42px; } -#pallist .lstI.sticky { - top: -11px; +#fxlist .lstI.selected { + top: 84px; } -.lstI.selected { - background: var(--c-5); - top: 95px; - bottom: -11px; +#fxlist .lstI.sticky { + top: 42px; +} + +#pallist .lstI.selected { + top: 84px; } -.lstI.sticky { - top: 57px; +#pallist .lstI.sticky { + top: 42px; } +/* list item content */ +.lstIcontent { + padding: 9px 0 7px; + position: relative; +} + +/* list item name (for sorting) */ .lstIname { - margin: 3px 0; white-space: nowrap; - cursor: pointer; - font-size: 19px; + text-overflow: ellipsis; + -webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ + filter: grayscale(100%); } +/* list item palette preview */ .lstIprev { - width: 220px; - height: 5px; - margin: auto; + width: 100%; + height: 6px; position: absolute; - bottom: 0px; - left: 5px; + bottom: 0; + left: 0; + z-index: -1; } -input[type="text"].search { +/* find/search element */ +.fnd { + position: relative; +} + +.fnd input[type="text"] { display: block; - width: 230px; + width: 100%; box-sizing: border-box; - padding: 8px 8px 9px 38px; - margin: 6px auto 0 auto; + padding: 8px 40px 8px 44px; + margin: 5px auto 0; text-align: left; - background-color: var(--c-3); + border-radius: 21px; + background: var(--c-2); + border: 1px solid var(--c-3); + -webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ + filter: grayscale(100%); } -input[type="text"].search:focus { +.fnd input[type="text"]:focus { background-color: var(--c-4); } -input[type="text"].search:not(:placeholder-shown) { - background-color: var(--c-5); +.fnd input[type="text"]:not(:placeholder-shown), +.fnd input[type="text"]:hover { + background-color: var(--c-3); +} + +/* segment & preset inner/expanded content */ +.segin, +.presin { + padding: 8px; + position: relative; } -.pres { - margin-bottom: 6px; +.btn-s, +.btn-n { + border: 1px solid var(--c-2); + background-color: var(--c-2); +} +.modal .btn:hover, +.segin .btn:hover { + border: 1px solid var(--c-5) /*!important*/; + background-color: var(--c-5) /*!important*/; } -.segin { - padding: 8px 8px 4px 8px; +/* hidden list items, must be after .expanded */ +.pres .lstIcontent, .segin { display: none; } +.check input:checked ~ .checkmark:after, +.radio input:checked ~ .radiomark:after, +.show, +.expanded .edit-icon, +.expanded .segin, .expanded .presin, .expanded .sbs, .expanded { - display: block; + display: inline-block !important; +} +.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .frz, .expanded .g-icon { + display: none !important; +} + +.m6 { + margin: 6px 0; } .c { @@ -1123,14 +1441,12 @@ input[type="text"].search:not(:placeholder-shown) { color: red; } +/* horizontal divider (playlist entries) */ .hrz { width: auto; height: 2px; background-color: var(--c-b); -} - -.no-margin { - margin: 0; + margin: 3px 0; } ::-webkit-scrollbar { @@ -1148,34 +1464,118 @@ input[type="text"].search:not(:placeholder-shown) { background: var(--c-sbh); } -@media all and (max-width: 335px) { - .sliderbubble { +@media not all and (hover: none) { + .sliderwrap:hover + output.sliderbubble { + visibility: visible; + opacity: 1; + } +} + +@media all and (max-width: 1023px) { + .top button { + width: 8%; + padding: 10px 0 8px 0; + } + #buttonPcm { display: none; } } +@media all and (max-width: 335px) { + .sliderbubble { + display: none; + } +} + @media all and (max-width: 550px) and (min-width: 374px) { - .infobtn { - width: 155px; + #info table .btn, #nodes table .btn { + width: 200px; + } + #info .infobtn, #nodes .infobtn { + width: 145px; + } + #info div, #nodes div { + max-width: 320px; + } +} + +@media all and (max-width: 420px) { + #buttonNodes { + display: none; } } -@media all and (max-width: 685px) { +@media all and (max-width: 639px) { .top button { width: 16.6%; padding: 8px 0 4px 0; } + #briwrap { + margin: 0 auto !important; + float: none; + display: inline-block; + } .hd { display: none !important; } - #briwrap { - margin-top: 0px !important; - float: none; +} + +@media all and (min-width: 420px) and (max-width: 639px) { + .top button { + width: 14.28%; + padding: 8px 0 4px 0; } } -@media all and (max-width: 1249px) { - #buttonPcm { +@media all and (min-width: 640px) and (max-width: 767px) { + #buttonNodes { display: none; } } + +/* small screen & tablet "PC mode" support */ +@media all and (min-width: 1024px) and (max-width: 1249px) { + #segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, #psFind, #sliders { + width: 100%; + max-width: 280px; + font-size: 18px; + } + #picker { + width: 230px; + } + #putil .btn-s { + width: 114px; + } + #sliders .sliderbubble { + display: none; + } + #sliders .sliderwrap, .sbs .sliderwrap { + width: calc(100% - 42px); + } + #sliders .slider { + padding-right: 0; + } + #sliders .sliderwrap { + left: 12px; + } + .segname { + max-width: calc(100% - 110px); + } + .segt TD { + padding: 0 !important; + } + input[type="number"], input[type=text], select, textarea { + font-size: 18px; + } + input[type="number"] { + width: 32px; + } + .lstIcontent { + padding-left: 8px; + } + .revchkl { + max-width: 183px; + text-overflow: ellipsis; + overflow-x: clip; + } +} diff --git a/wled00/data/index.htm b/wled00/data/index.htm index bbf7517a75..7665a6e5dc 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -7,10 +7,52 @@ WLED - + - +
Loading WLED UI...
@@ -23,19 +65,20 @@ - -
- - + + + +

Brightness

-
- +
+
+
@@ -44,49 +87,74 @@
-
-
-
-
- -
-

+
+
+
+ +
+
+ Hue +
+
+
+ +
+ Saturation
-
+
+
+ +
+
+ Value/Brightness +
+
+ Kelvin/Temperature
-

RGB color

-
- -
-

-
- -
-

-
- -
-

+ +
+
+ +
+
+ Red channel +
+
+
+ +
+
+ Green channel +
+
+
+ +
+
+ Blue channel +
-
-

White channel

-
- +
+ +
+
+ White channel
-
-

White balance

+
+
+ White balance
@@ -99,63 +167,151 @@
-
R
+
R
- - - + + +
+

+ - -
-
-

- - Color palette -

-
-
-
- + +
+

Color palette

+
+
+ + + +
+
+
+
+
+
+ + + +
-

Effect speed

-
- -
- - -
+
+

Effect mode

+
+ + +
-
-

Effect intensity

-
- -
- - -
+
+
+ +
-

Effect mode

-
- Loading... +
+
+ + + + + + +
+
+ +
+ +
+
+ + Effect speed +
+
+ +
+ +
+
+ + Effect intensity +
+
+ +
+ +
+
+ + Custom 1 +
+
+ +
+ +
+
+ + Custom 2 +
+
+ +
+ +
+
+ + Custom 3 +
+
+ + + +
@@ -163,24 +319,28 @@
Loading...
-
- +
-

Transition: s

+

Transition:  s

+

-
- -
-
-
- Loading... +

Presets

+
+ + + +
+
+ Loading... +
+
@@ -193,25 +353,35 @@
-
+
+
Loading...

+
+ + + + +
+
+ Made with ❤︎ by Aircoookie and the WLED community
+ + +
- - diff --git a/wled00/data/index.js b/wled00/data/index.js index f365cf6205..e6e4f64db0 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -1,95 +1,72 @@ //page js -var loc = false, locip; -var noNewSegs = false; +var loc = false, locip, locproto = "http:"; var isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false, syncTglRecv = true; var hasWhite = false, hasRGB = false, hasCCT = false; -var whites = [0,0,0]; -var colors = [[0,0,0],[0,0,0],[0,0,0]]; -var expanded = [false]; -var powered = [true]; var nlDur = 60, nlTar = 0; var nlMode = false; +var segLmax = 0; // size (in pixels) of largest selected segment var selectedFx = 0; +var selectedPal = 0; var csel = 0; // selected color slot (0-2) var currentPreset = -1; var lastUpdate = 0; var segCount = 0, ledCount = 0, lowestUnused = 0, maxSeg = 0, lSeg = 0; -var pcMode = false, pcModeA = false, lastw = 0; +var pcMode = false, pcModeA = false, lastw = 0, wW; var tr = 7; var d = document; -const ranges = RangeTouch.setup('input[type="range"]', {}); var palettesData; -var pJson = {}; +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 lastinfo = {}; -var ws; -var fxlist = d.getElementById('fxlist'), pallist = d.getElementById('pallist'); +var isM = false, mw = 0, mh=0; +var ws, cpick, ranges, wsRpt=0; var cfg = { theme:{base:"dark", bg:{url:""}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}}, comp :{colors:{picker: true, rgb: false, quick: true, hex: false}, - labels:true, pcmbot:false, pid:true, seglen:false, css:true, hdays:false} + labels:true, pcmbot:false, pid:true, seglen:false, segpwr:false, segexp:false, + css:true, hdays:false, fxdef:true, idsort: false} }; var hol = [ [0,11,24,4,"https://aircoookie.github.io/xmas.png"], // christmas [0,2,17,1,"https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day - [2022,3,17,2,"https://aircoookie.github.io/easter.png"], - [2023,3,9,2,"https://aircoookie.github.io/easter.png"], - [2024,2,31,2,"https://aircoookie.github.io/easter.png"] + [2025,3,20,2,"https://aircoookie.github.io/easter.png"], + [2024,2,31,2,"https://aircoookie.github.io/easter.png"], + [0,6,4,1,"https://images.alphacoders.com/516/516792.jpg"], // 4th of July + [0,0,1,1,"https://images.alphacoders.com/119/1198800.jpg"] // new year ]; -var cpick = new iro.ColorPicker("#picker", { - width: 260, - wheelLightness: false, - wheelAngle: 270, - wheelDirection: "clockwise", - layout: [ - { - component: iro.ui.Wheel, - options: {} - } - ] -}); - -function handleVisibilityChange() { - if (!d.hidden && new Date () - lastUpdate > 3000) { - requestJson(null); - } -} +function handleVisibilityChange() {if (!d.hidden && new Date () - lastUpdate > 3000) requestJson();} +function sCol(na, col) {d.documentElement.style.setProperty(na, col);} +function gId(c) {return d.getElementById(c);} +function gEBCN(c) {return d.getElementsByClassName(c);} +function isEmpty(o) {return Object.keys(o).length === 0;} +function isObj(i) {return (i && typeof i === 'object' && !Array.isArray(i));} +function isNumeric(n) {return !isNaN(parseFloat(n)) && isFinite(n);} -function sCol(na, col) { - d.documentElement.style.setProperty(na, col); -} - -function isRgbBlack(a, s) { - return (a[s][0] == 0 && a[s][1] == 0 && a[s][2] == 0); -} +// returns true if dataset R, G & B values are 0 +function isRgbBlack(a) {return (parseInt(a.r) == 0 && parseInt(a.g) == 0 && parseInt(a.b) == 0);} -// returns RGB color from a given slot s 0-2 from color array a -function rgbStr(a, s) { - return "rgb(" + a[s][0] + "," + a[s][1] + "," + a[s][2] + ")"; -} +// returns RGB color from a given dataset +function rgbStr(a) {return "rgb(" + a.r + "," + a.g + "," + a.b + ")";} // brightness approximation for selecting white as text color if background bri < 127, and black if higher -function rgbBri(a, s) { - var R = a[s][0], G = a[s][1], B = a[s][2]; - return 0.2126*R + 0.7152*G + 0.0722*B; -} +function rgbBri(a) {return 0.2126*parseInt(a.r) + 0.7152*parseInt(a.g) + 0.0722*parseInt(a.b);} // sets background of color slot selectors -function setCSL(s) { - var cd = d.getElementsByClassName('cl')[s]; - var w = whites[s]; - if (hasRGB && !isRgbBlack(colors, s)) { - cd.style.background = rgbStr(colors, s); - cd.style.color = rgbBri(colors, s) > 127 ? "#000":"#fff"; - if (hasWhite && w > 0) { - cd.style.background = `linear-gradient(180deg, ${rgbStr(colors, s)} 30%, ${rgbStr([[w,w,w]], 0)})`; - } +function setCSL(cs) +{ + let w = cs.dataset.w ? parseInt(cs.dataset.w) : 0; + let hasShadow = getComputedStyle(cs).textShadow !== "none"; + if (hasRGB && !isRgbBlack(cs.dataset)) { + if (!hasShadow) cs.style.color = rgbBri(cs.dataset) > 127 ? "#000":"#fff"; // if text has no CSS "shadow" + cs.style.background = (hasWhite && w > 0) ? `linear-gradient(180deg, ${rgbStr(cs.dataset)} 30%, rgb(${w},${w},${w}))` : rgbStr(cs.dataset); } else { - if (!hasWhite) w = 0; - cd.style.background = rgbStr([[w,w,w]], 0); - cd.style.color = w > 127 ? "#000":"#fff"; + if (hasRGB && !hasWhite) w = 0; + cs.style.background = `rgb(${w},${w},${w})`; + if (!hasShadow) cs.style.color = w > 127 ? "#000":"#fff"; } } @@ -98,15 +75,16 @@ function applyCfg() cTheme(cfg.theme.base === "light"); var bg = cfg.theme.color.bg; if (bg) sCol('--c-1', bg); - if (lastinfo.leds) updateUI(); // update component visibility var l = cfg.comp.labels; - sCol('--tbp',l ? "14px 14px 10px 14px":"10px 22px 4px 22px"); - sCol('--bbp',l ? "9px 0 7px 0":"10px 0 4px 0"); - sCol('--bhd',l ? "block":"none"); // hides/shows button labels - sCol('--bmt',l ? "0px":"5px"); + sCol('--tbp', l ? "14px 14px 10px 14px":"10px 22px 4px 22px"); + sCol('--bbp', l ? "9px 0 7px 0":"10px 0 4px 0"); + sCol('--bhd', l ? "block":"none"); // show/hide labels + sCol('--bmt', l ? "0px":"5px"); sCol('--t-b', cfg.theme.alpha.tab); + sCol('--sgp', !cfg.comp.segpwr ? "block":"none"); // show/hide segment power size(); localStorage.setItem('wledUiCfg', JSON.stringify(cfg)); + if (lastinfo.leds) updateUI(); // update component visibility } function tglHex() @@ -127,119 +105,151 @@ function tglLabels() applyCfg(); } +function tglRgb() +{ + cfg.comp.colors.rgb = !cfg.comp.colors.rgb; + applyCfg(); +} + function cTheme(light) { if (light) { - sCol('--c-1','#eee'); - sCol('--c-f','#000'); - sCol('--c-2','#ddd'); - sCol('--c-3','#bbb'); - sCol('--c-4','#aaa'); - sCol('--c-5','#999'); - sCol('--c-6','#999'); - sCol('--c-8','#888'); - sCol('--c-b','#444'); - sCol('--c-c','#333'); - sCol('--c-e','#111'); - sCol('--c-d','#222'); - sCol('--c-r','#c42'); - sCol('--c-o','rgba(204, 204, 204, 0.9)'); - sCol('--c-sb','#0003'); sCol('--c-sbh','#0006'); - sCol('--c-tb','rgba(204, 204, 204, var(--t-b))'); - sCol('--c-tba','rgba(170, 170, 170, var(--t-b))'); - sCol('--c-tbh','rgba(204, 204, 204, var(--t-b))'); - d.getElementById('imgw').style.filter = "invert(0.8)"; - } else { // default dark theme - sCol('--c-1','#111'); - sCol('--c-f','#fff'); - sCol('--c-2','#222'); - sCol('--c-3','#333'); - sCol('--c-4','#444'); - sCol('--c-5','#555'); - sCol('--c-6','#666'); - sCol('--c-8','#888'); - sCol('--c-b','#bbb'); - sCol('--c-c','#ccc'); - sCol('--c-e','#eee'); - sCol('--c-d','#ddd'); - sCol('--c-r','#831'); - sCol('--c-o','rgba(34, 34, 34, 0.9)'); - sCol('--c-sb','#fff3'); sCol('--c-sbh','#fff5'); - sCol('--c-tb','rgba(34, 34, 34, var(--t-b))'); - sCol('--c-tba','rgba(102, 102, 102, var(--t-b))'); - sCol('--c-tbh','rgba(51, 51, 51, var(--t-b))'); - d.getElementById('imgw').style.filter = "unset"; - } -} - -function loadBg(iUrl) { - let bg = d.getElementById('bg'); + sCol('--c-1','#eee'); + sCol('--c-f','#000'); + sCol('--c-2','#ddd'); + sCol('--c-3','#bbb'); + sCol('--c-4','#aaa'); + sCol('--c-5','#999'); + sCol('--c-6','#999'); + sCol('--c-8','#888'); + sCol('--c-b','#444'); + sCol('--c-c','#333'); + sCol('--c-e','#111'); + sCol('--c-d','#222'); + sCol('--c-r','#a21'); + sCol('--c-g','#2a1'); + sCol('--c-l','#26c'); + sCol('--c-o','rgba(204, 204, 204, 0.9)'); + sCol('--c-sb','#0003'); sCol('--c-sbh','#0006'); + sCol('--c-tb','rgba(204, 204, 204, var(--t-b))'); + sCol('--c-tba','rgba(170, 170, 170, var(--t-b))'); + sCol('--c-tbh','rgba(204, 204, 204, var(--t-b))'); + gId('imgw').style.filter = "invert(0.8)"; + } else { + sCol('--c-1','#111'); + sCol('--c-f','#fff'); + sCol('--c-2','#222'); + sCol('--c-3','#333'); + sCol('--c-4','#444'); + sCol('--c-5','#555'); + sCol('--c-6','#666'); + sCol('--c-8','#888'); + sCol('--c-b','#bbb'); + sCol('--c-c','#ccc'); + sCol('--c-e','#eee'); + sCol('--c-d','#ddd'); + sCol('--c-r','#e42'); + sCol('--c-g','#4e2'); + sCol('--c-l','#48a'); + sCol('--c-o','rgba(34, 34, 34, 0.9)'); + sCol('--c-sb','#fff3'); sCol('--c-sbh','#fff5'); + sCol('--c-tb','rgba(34, 34, 34, var(--t-b))'); + sCol('--c-tba','rgba(102, 102, 102, var(--t-b))'); + sCol('--c-tbh','rgba(51, 51, 51, var(--t-b))'); + gId('imgw').style.filter = "unset"; + } +} + +function loadBg(iUrl) +{ + let bg = gId('bg'); let img = d.createElement("img"); img.src = iUrl; - if (iUrl == "") { + if (iUrl == "" || iUrl==="https://picsum.photos/1920/1080") { var today = new Date(); - for (var i=0; i=hs && today<=he) img.src = hol[i][4]; + he.setDate(he.getDate() + h[3]); + if (today>=hs && today<=he) img.src = h[4]; } } - img.addEventListener('load', (event) => { + img.addEventListener('load', (e) => { var a = parseFloat(cfg.theme.alpha.bg); if (isNaN(a)) a = 0.6; bg.style.opacity = a; bg.style.backgroundImage = `url(${img.src})`; img = null; + gId('namelabel').style.color = "var(--c-c)"; // improve namelabel legibility on background image }); } function loadSkinCSS(cId) { - if (!d.getElementById(cId)) // check if element exists + if (!gId(cId)) // check if element exists { - var h = document.getElementsByTagName('head')[0]; - var l = document.createElement('link'); + var h = d.getElementsByTagName('head')[0]; + var l = d.createElement('link'); l.id = cId; l.rel = 'stylesheet'; l.type = 'text/css'; - l.href = (loc?`http://${locip}`:'.') + '/skin.css'; + l.href = getURL('/skin.css'); l.media = 'all'; h.appendChild(l); } } -function onLoad() { - if (window.location.protocol == "file:") { - loc = true; - locip = localStorage.getItem('locIp'); +function getURL(path) { + return (loc ? locproto + "//" + locip : "") + path; +} +function onLoad() +{ + let l = window.location; + if (l.protocol == "file:") { + loc = true; + locip = localStorage.getItem('locIp'); if (!locip) { locip = prompt("File Mode. Please enter WLED IP!"); localStorage.setItem('locIp', locip); } + } else { + // detect reverse proxy and/or HTTPS + let pathn = l.pathname; + let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/"); + //if (paths[0]==="sliders") paths.shift(); + //while (paths[0]==="") paths.shift(); + locproto = l.protocol; + locip = l.hostname + (l.port ? ":" + l.port : ""); + if (paths.length > 0 && paths[0]!=="") { + loc = true; + locip += "/" + paths[0]; + } else if (locproto==="https:") { + loc = true; + } } var sett = localStorage.getItem('wledUiCfg'); if (sett) cfg = mergeDeep(cfg, JSON.parse(sett)); resetPUtil(); + if (localStorage.getItem('pcm') == "true" || (!/Mobi/.test(navigator.userAgent) && localStorage.getItem('pcm') == null)) togglePcMode(true); applyCfg(); if (cfg.comp.hdays) { //load custom holiday list - fetch((loc?`http://${locip}`:'.') + "/holidays.json", { // may be loaded from external source + fetch(getURL("/holidays.json"), { // may be loaded from external source method: 'get' }) - .then(res => { + .then((res)=>{ //if (!res.ok) showErrorToast(); return res.json(); }) - .then(json => { + .then((json)=>{ if (Array.isArray(json)) hol = json; //TODO: do some parsing first }) - .catch(function (error) { + .catch((e)=>{ console.log("No array of holidays in holidays.json. Defaults loaded."); }) - .finally(function(){ + .finally(()=>{ loadBg(cfg.theme.bg.url); }); } else @@ -248,20 +258,29 @@ function onLoad() { selectSlot(0); updateTablinks(0); - resetUtil(); - cpick.on("input:end", function() { - setColor(1); - }); - cpick.on("color:change", updatePSliders); pmtLS = localStorage.getItem('wledPmt'); - setTimeout(function(){requestJson(null, false);}, 50); + + // Load initial data + loadPalettes(()=>{ + // fill effect extra data array + loadFXData(()=>{ + // load and populate effects + loadFX(()=>{ + setTimeout(()=>{ // ESP8266 can't handle quick requests + loadPalettesData(()=>{ + requestJson();// will load presets and create WS + }); + },100); + }); + }); + }); + resetUtil(); + d.addEventListener("visibilitychange", handleVisibilityChange, false); - size(); - d.getElementById("cv").style.opacity=0; - if (localStorage.getItem('pcm') == "true") togglePcMode(true); + //size(); + gId("cv").style.opacity=0; var sls = d.querySelectorAll('input[type="range"]'); for (var sl of sls) { - sl.addEventListener('input', updateBubble, true); sl.addEventListener('touchstart', toggleBubble); sl.addEventListener('touchend', toggleBubble); } @@ -269,15 +288,14 @@ function onLoad() { function updateTablinks(tabI) { - var tablinks = d.getElementsByClassName("tablinks"); - for (var i of tablinks) { - i.className = i.className.replace(" active", ""); - } + var tablinks = gEBCN("tablinks"); + for (var i of tablinks) i.classList.remove('active'); if (pcMode) return; - tablinks[tabI].className += " active"; + tablinks[tabI].classList.add('active'); } -function openTab(tabI, force = false) { +function openTab(tabI, force = false) +{ if (pcMode && !force) return; iSlide = tabI; _C.classList.toggle('smooth', false); @@ -286,24 +304,34 @@ function openTab(tabI, force = false) { } var timeout; -function showToast(text, error = false) { - if (error) d.getElementById('connind').style.backgroundColor = "#831"; - var x = d.getElementById("toast"); +function showToast(text, error = false) +{ + if (error) gId('connind').style.backgroundColor = "var(--c-r)"; + var x = gId('toast'); + //if (error) text += ''; x.innerHTML = text; - x.className = error ? "error":"show"; + x.classList.add(error ? 'error':'show'); clearTimeout(timeout); x.style.animation = 'none'; - x.style.animation = null; - timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900); + timeout = setTimeout(()=>{ x.classList.remove('show'); }, 2900); + if (error) console.log(text); } -function showErrorToast() { - // if we received a timeout force WS reconnect - reconnectWS(); +function showErrorToast() +{ showToast('Connection to light failed!', true); } -function clearErrorToast() { - d.getElementById("toast").className = d.getElementById("toast").className.replace("error", ""); + +function clearErrorToast(n=5000) +{ + var x = gId('toast'); + if (x.classList.contains('error')) { + clearTimeout(timeout); + timeout = setTimeout(()=>{ + x.classList.remove('show'); + x.classList.remove('error'); + }, n); + } } function getRuntimeStr(rt) @@ -329,53 +357,54 @@ function inforow(key, val, unit = "") function getLowestUnusedP() { var l = 1; - for (var key in pJson) { - if (key == l) l++; - } + for (var key in pJson) if (key == l) l++; if (l > 250) l = 250; return l; } -function checkUsed(i) { - var id = d.getElementById(`p${i}id`).value; - if (pJson[id] && (i == 0 || id != i)) { - d.getElementById(`p${i}warn`).innerHTML = `⚠ Overwriting ${pName(id)}!`; - } else { - d.getElementById(`p${i}warn`).innerHTML = ""; - } +function checkUsed(i) +{ + var id = gId(`p${i}id`).value; + if (pJson[id] && (i == 0 || id != i)) + gId(`p${i}warn`).innerHTML = `⚠ Overwriting ${pName(id)}!`; + else + gId(`p${i}warn`).innerHTML = id>250?"⚠ ID must be 250 or less.":""; } -function pName(i) { +function pName(i) +{ var n = "Preset " + i; - if (pJson[i].n) n = pJson[i].n; + if (pJson && pJson[i] && pJson[i].n) n = pJson[i].n; return n; } -function isPlaylist(i) { +function isPlaylist(i) +{ return pJson[i].playlist && pJson[i].playlist.ps; } -function papiVal(i) { - if (!pJson[i]) return ""; +function papiVal(i) +{ + if (!pJson || !pJson[i]) return ""; var o = Object.assign({},pJson[i]); if (o.win) return o.win; delete o.n; delete o.p; delete o.ql; return JSON.stringify(o); } -function qlName(i) { - if (!pJson[i]) return ""; - if (!pJson[i].ql) return ""; +function qlName(i) +{ + if (!pJson || !pJson[i] || !pJson[i].ql) return ""; return pJson[i].ql; } -function cpBck() { - var copyText = d.getElementById("bck"); +function cpBck() +{ + var copyText = gId("bck"); copyText.select(); copyText.setSelectionRange(0, 999999); d.execCommand("copy"); - showToast("Copied to clipboard!"); } @@ -385,10 +414,9 @@ function presetError(empty) try { bckstr = localStorage.getItem("wledP"); if (bckstr.length > 10) hasBackup = true; - } catch (e) { + } catch (e) {} - } - var cn = `
`; + var cn = `
`; if (empty) cn += `You have no presets yet!`; else @@ -402,85 +430,139 @@ function presetError(empty) else cn += `Here is a backup of the last known good state:`; cn += `
- `; + `; } cn += `
`; - d.getElementById('pcont').innerHTML = cn; - if (hasBackup) d.getElementById('bck').value = bckstr; + gId('pcont').innerHTML = cn; + if (hasBackup) gId('bck').value = bckstr; } function loadPresets(callback = null) { - //1st boot (because there is a callback) + // 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 + // 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 + // afterwards if (!callback && pmt == pmtLast) return; - pmtLast = pmt; - - var url = '/presets.json'; - if (loc) { - url = `http://${locip}/presets.json`; - } - - fetch - (url, { + fetch(getURL('/presets.json'), { method: 'get' }) .then(res => { - if (!res.ok) { - showErrorToast(); - } + if (res.status=="404") return {"0":{}}; + //if (!res.ok) showErrorToast(); return res.json(); }) .then(json => { pJson = json; + pmtLast = pmt; populatePresets(); }) - .catch(function (error) { - showToast(error, true); - console.log(error); + .catch((e)=>{ + //showToast(e, true); presetError(false); }) - .finally(() => { + .finally(()=>{ if (callback) setTimeout(callback,99); }); } -var pQL = []; +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(); + }) + .catch((e)=>{ + showToast(e, true); + }) + .finally(()=>{ + if (callback) callback(); + updateUI(); + }); +} + +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(); + }) + .catch((e)=>{ + //setTimeout(loadFX, 250); // retry + showToast(e, true); + }) + .finally(()=>{ + if (callback) callback(); + updateUI(); + }); +} +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(";!;"); + }) + .catch((e)=>{ + fxdata = []; + //setTimeout(loadFXData, 250); // retry + showToast(e, true); + }) + .finally(()=>{ + if (callback) callback(); + updateUI(); + }); +} + +var pQL = []; function populateQL() { var cn = ""; if (pQL.length > 0) { - cn += `

Quick load

`; - - var it = 0; + pQL.sort((a,b) => (a[0]>b[0])); + cn += `

Quick load

`; for (var key of (pQL||[])) { - cn += ``; - it++; - if (it > 4) { - it = 0; - cn += '
'; - } + cn += ``; } - if (it != 0) cn+= '
'; - - cn += `

All presets

`; - } - d.getElementById('pql').innerHTML = cn; + gId('pql').classList.add('expanded'); + } else gId('pql').classList.remove('expanded'); + gId('pql').innerHTML = cn; } function populatePresets(fromls) { if (fromls) pJson = JSON.parse(localStorage.getItem("wledP")); + if (!pJson) {setTimeout(loadPresets,250); return;} delete pJson["0"]; var cn = ""; var arr = Object.entries(pJson); @@ -488,25 +570,25 @@ function populatePresets(fromls) pQL = []; var is = []; pNum = 0; - for (var key of (arr||[])) { - if (!isObject(key[1])) continue; + if (!isObj(key[1])) continue; let i = parseInt(key[0]); var qll = key[1].ql; - if (qll) pQL.push([i, qll]); + if (qll) pQL.push([i, qll, pName(i)]); is.push(i); - cn += `
`; + cn += `
`; if (cfg.comp.pid) cn += `
${i}
`; - cn += `
${isPlaylist(i)?"":""}${pName(i)}
- -
-

`; + cn += `
${isPlaylist(i)?"":""}${pName(i)} +
+ +
+
`; pNum++; } - d.getElementById('pcont').innerHTML = cn; + gId('pcont').innerHTML = cn; if (pNum > 0) { if (pmtLS != pmt && pmt != 0) { localStorage.setItem("wledPmt", pmt); @@ -514,20 +596,66 @@ function populatePresets(fromls) localStorage.setItem("wledP", JSON.stringify(pJson)); } pmtLS = pmt; - for (var a = 0; a < is.length; a++) { - let i = is[a]; - if (expanded[i+100]) expand(i+100, true); - } - //makePlSel(arr); } else { presetError(true); } updatePA(); populateQL(); } +function parseInfo(i) { + lastinfo = i; + var name = i.name; + gId('namelabel').innerHTML = name; + if (!name.match(/[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f\u3131-\uD79D]/)) + gId('namelabel').style.transform = "rotate(180deg)"; // rotate if no CJK characters + if (name === "Dinnerbone") d.documentElement.style.transform = "rotate(180deg)"; // Minecraft easter egg + if (i.live) name = "(Live) " + name; + if (loc) name = "(L) " + name; + d.title = name; + ledCount = i.leds.count; + syncTglRecv = i.str; + maxSeg = i.leds.maxseg; + pmt = i.fs.pmt; + gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none"; + // do we have a matrix set-up + mw = i.leds.matrix ? i.leds.matrix.w : 0; + mh = i.leds.matrix ? i.leds.matrix.h : 0; + isM = mw>0 && mh>0; + if (!isM) { + gId("filter0D").classList.remove('hide'); + gId("filter1D").classList.add('hide'); + gId("filter2D").classList.add('hide'); + } else { + gId("filter0D").classList.add('hide'); + gId("filter1D").classList.remove('hide'); + gId("filter2D").classList.remove('hide'); + } +// if (i.noaudio) { +// gId("filterVol").classList.add("hide"); +// gId("filterFreq").classList.add("hide"); +// } +// if (!i.u || !i.u.AudioReactive) { +// gId("filterVol").classList.add("hide"); hideModes(" ♪"); // hide volume reactive effects +// gId("filterFreq").classList.add("hide"); hideModes(" ♫"); // hide frequency reactive effects +// } +} + +//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"); +// Array.from(oldScript.attributes) +// .forEach( attr => newScript.setAttribute(attr.name, attr.value) ); +// newScript.appendChild(document.createTextNode(oldScript.innerHTML)); +// oldScript.parentNode.replaceChild(newScript, oldScript); +// }); +//} +//setInnerHTML(obj, html); + function populateInfo(i) { var cn=""; - var heap = i.freeheap/1000; + var heap = i.freeheap/1024; heap = heap.toFixed(1); var pwr = i.leds.pwr; var pwru = "Not calculated"; @@ -536,215 +664,290 @@ function populateInfo(i) var urows=""; if (i.u) { for (const [k, val] of Object.entries(i.u)) { - if (val[1]) { + if (val[1]) urows += inforow(k,val[0],val[1]); - } else { + else urows += inforow(k,val); - } } } - var vcn = "Kuuhaku"; - if (i.ver.startsWith("0.13.")) vcn = "Toki"; + if (i.ver.startsWith("0.14.")) vcn = "Hoshi"; +// if (i.ver.includes("-bl")) vcn = "Supāku"; if (i.cn) vcn = i.cn; cn += `v${i.ver} "${vcn}" (HyperSerialWLED)

- ${urows} - ${inforow("Build",i.vid)} - ${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} - ${inforow("Uptime",getRuntimeStr(i.uptime))} - ${inforow("Free heap",heap," kB")} - ${inforow("Estimated current",pwru)} - ${inforow("Frames / second",i.leds.fps)} - ${inforow("MAC address",i.mac)} - ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")} - ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} -
`; - d.getElementById('kv').innerHTML = cn; +${urows} +${urows===""?'':'
'} +${i.opt&0x100?inforow("Debug",""):''} +${inforow("Build",i.vid)} +${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} +${inforow("Uptime",getRuntimeStr(i.uptime))} +${inforow("Time",i.time)} +${inforow("Free heap",heap," kB")} +${i.psram?inforow("Free PSRAM",(i.psram/1024).toFixed(1)," kB"):""} +${inforow("Estimated current",pwru)} +${inforow("Average FPS",i.leds.fps)} +${inforow("MAC address",i.mac)} +${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")} +${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} +`; + gId('kv').innerHTML = cn; + // update all sliders in Info + for (let sd of (gId('kv').getElementsByClassName('sliderdisplay')||[])) { + let s = sd.previousElementSibling; + if (s) updateTrail(s); + } } function populateSegments(s) { var cn = ""; + let li = lastinfo; segCount = 0; lowestUnused = 0; lSeg = 0; - for (var y = 0; y < (s.seg||[]).length; y++) - { + for (var inst of (s.seg||[])) { segCount++; - var inst=s.seg[y]; let i = parseInt(inst.id); - powered[i] = inst.on; if (i == lowestUnused) lowestUnused = i+1; if (i > lSeg) lSeg = i; - cn += `
- -
-
${inst.n ? inst.n : "Segment "+i}
- -
- -
- -
- -
- -
-
-
- - - - - - - - - - - -
Start LED${cfg.comp.seglen?"Length":"Stop LED"}Offset
- - - - - - - - - - - -
GroupingSpacingApply
-
- - -
- - -
-
-

`; - } - - d.getElementById('segcont').innerHTML = cn; - if (lowestUnused >= maxSeg) { - d.getElementById('segutil').innerHTML = 'Maximum number of segments reached.'; - noNewSegs = true; - } else if (noNewSegs) { - resetUtil(); - noNewSegs = false; - } + let sg = gId(`seg${i}`); + let exp = sg ? (sg.classList.contains('expanded') || (i===0 && cfg.comp.segexp)) : false; + + // segment set icon color + let cG = "var(--c-b)"; + switch (inst.set) { + case 1: cG = "var(--c-r)"; break; + case 2: cG = "var(--c-g)"; break; + case 3: cG = "var(--c-l)"; break; + } + + let segp = `
`+ + ``+ + `
`+ + ``+ + `
`+ + `
`+ + `
`; + let staX = inst.start; + let stoX = inst.stop; + let staY = inst.startY; + let stoY = inst.stopY; + let isMSeg = isM && staXReverse ${isM?'':'direction'}`; + let miXck = ``; + let rvYck = "", miYck =""; + if (isMSeg) { + rvYck = ``; + miYck = ``; + } + let map2D = `
Expand 1D FX
`+ + `
`+ + `
`; + let sndSim = `
Sound sim
`+ + `
`+ + `
`; + cn += `
`+ + ``+ + `
`+ + `&#x${inst.frz ? (li.live && li.liveseg==i?'e410':'e0e8') : 'e325'};`+ + (inst.n ? inst.n : "Segment "+i) + + `
`+ + `ɸ${String.fromCharCode(inst.set+"A".charCodeAt(0))};`+ + `
`+ + `
`+ + ``+ + `
`+ + ``+ + (cfg.comp.segpwr ? segp : '') + + `
`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + (isMSeg ? ''+ + ''+ + ''+ + ''+ + ''+ + '' : '') + + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `
${isMSeg?'Start X':'Start LED'}${isMSeg?(cfg.comp.seglen?"Width":"Stop X"):(cfg.comp.seglen?"LED count":"Stop LED")}${isMSeg?'':'Offset'}
${isMSeg?miXck+'
'+rvXck:''}
Start Y'+(cfg.comp.seglen?'Height':'Stop Y')+'
'+miYck+'
'+rvYck+'
GroupingSpacing
`+ + `
`+ + (!isMSeg ? rvXck : '') + + (isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') + + (s.AudioReactive && s.AudioReactive.on ? "" : sndSim) + + ``+ + `
`+ + ``+ + ``+ + `
`+ + `
`+ + (cfg.comp.segpwr ? '' : segp) + + `
`; + } + + gId('segcont').innerHTML = cn; + let noNewSegs = (lowestUnused >= maxSeg); + resetUtil(noNewSegs); + if (gId('selall')) gId('selall').checked = true; for (var i = 0; i <= lSeg; i++) { + if (!gId(`seg${i}`)) continue; updateLen(i); - updateTrail(d.getElementById(`seg${i}bri`)); - let segr = d.getElementById(`segr${i}`); - if (segr) segr.style.display = "none"; + updateTrail(gId(`seg${i}bri`)); + gId(`segr${i}`).classList.add("hide"); + if (!gId(`seg${i}sel`).checked && gId('selall')) gId('selall').checked = false; // uncheck if at least one is unselected. + } + if (segCount < 2) { + gId(`segd${lSeg}`).classList.add("hide"); + if (parseInt(gId("seg0bri").value)==255) gId(`segp0`).classList.add("hide"); + } + if (!isM && !noNewSegs && (cfg.comp.seglen?parseInt(gId(`seg${lSeg}s`).value):0)+parseInt(gId(`seg${lSeg}e`).value) 1) ? "block":"none"; // rsbtn parent + + if (Array.isArray(li.maps) && li.maps.length>1) { + let cont = `Ledmap: 
"; + gId("ledmap").innerHTML = cont; + gId("ledmap").classList.remove('hide'); + } else { + gId("ledmap").classList.add('hide'); } - if (segCount < 2) d.getElementById(`segd${lSeg}`).style.display = "none"; - if (!noNewSegs && (cfg.comp.seglen?parseInt(d.getElementById(`seg${lSeg}s`).value):0)+parseInt(d.getElementById(`seg${lSeg}e`).value) 1) ? "inline":"none"; } -function populateEffects(effects) +function populateEffects() { - var html = ``; + var effects = eJson; + var html = ""; - effects.shift(); //remove solid + effects.shift(); // temporary remove solid for (let i = 0; i < effects.length; i++) { - effects[i] = {id: parseInt(i)+1, name:effects[i]}; + effects[i] = { + id: effects[i][0], + name:effects[i][1] + }; } - effects.sort(compare); - + effects.sort((a,b) => (a.name).localeCompare(b.name)); effects.unshift({ "id": 0, - "name": "Solid", - "class": "sticky" + "name": "Solid" }); - for (let i = 0; i < effects.length; i++) { - html += generateListItemHtml( - 'fx', - effects[i].id, - effects[i].name, - 'setX', - '', - effects[i].class, - ); + for (let ef of effects) { + // add slider and color control to setFX (used by requestjson) + let id = ef.id; + let nm = ef.name+" "; + let fd = ""; + if (ef.name.indexOf("RSVD") < 0) { + if (Array.isArray(fxdata) && fxdata.length>id) { + if (fxdata[id].length==0) fd = ";;!;1" + else fd = fxdata[id]; + let eP = (fd == '')?[]:fd.split(";"); // effect parameters + let p = (eP.length<3 || eP[2]==='')?[]:eP[2].split(","); // palette data + if (p.length>0 && (p[0] !== "" && !isNumeric(p[0]))) nm += "🎨"; // effects using palette + let m = (eP.length<4 || eP[3]==='')?'1':eP[3]; // flags + if (id == 0) m = ''; // solid has no flags + if (m.length>0) { + if (m.includes('0')) nm += "•"; // 0D effects (PWM & On/Off) + if (m.includes('1')) nm += "⋮"; // 1D effects + if (m.includes('2')) nm += "▦"; // 2D effects + if (m.includes('v')) nm += "♪"; // volume effects + if (m.includes('f')) nm += "♫"; // frequency effects + } + } + html += generateListItemHtml('fx',id,nm,'setFX','',fd); + } } - fxlist.innerHTML=html; + gId('fxlist').innerHTML=html; } -function populatePalettes(palettes) +function populatePalettes() { - palettes.shift(); //remove default - for (let i = 0; i < palettes.length; i++) { - palettes[i] = { - "id": parseInt(i)+1, - "name": palettes[i] - }; - } - palettes.sort(compare); + lJson.shift(); // temporary remove default + lJson.sort((a,b) => (a[1]).localeCompare(b[1])); + lJson.unshift([0,"Default"]); - palettes.unshift({ - "id": 0, - "name": "Default", - "class": "sticky" - }); - - var html = ``; - for (let i = 0; i < palettes.length; i++) { + var html = ""; + for (let pa of lJson) { html += generateListItemHtml( 'palette', - palettes[i].id, - palettes[i].name, + pa[0], + pa[1], 'setPalette', - `
`, - palettes[i].class, + `
` ); } - pallist.innerHTML=html; + gId('pallist').innerHTML=html; + // append custom palettes (when loading for the 1st time) + if (!isEmpty(lastinfo) && lastinfo.cpalcount) { + for (let j = 0; j
` + ); + } + } } function redrawPalPrev() { let palettes = d.querySelectorAll('#pallist .lstI'); - for (let i = 0; i < palettes.length; i++) { - let id = palettes[i].dataset.id; - let lstPrev = palettes[i].querySelector('.lstIprev'); - if (lstPrev) { - lstPrev.style = genPalPrevCss(id); + for (var pal of (palettes||[])) { + let lP = pal.querySelector('.lstIprev'); + if (lP) { + lP.style = genPalPrevCss(pal.dataset.id); } } } function genPalPrevCss(id) { - if (!palettesData) { - return; - } + if (!palettesData) return; + var paletteData = palettesData[id]; - if (!paletteData) { - return 'display: none'; - } + if (!paletteData) return 'display: none'; // We need at least two colors for a gradient if (paletteData.length == 1) { @@ -756,60 +959,68 @@ function genPalPrevCss(id) var gradient = []; for (let j = 0; j < paletteData.length; j++) { - const element = paletteData[j]; - let r; - let g; - let b; + const e = paletteData[j]; + let r, g, b; let index = false; - if (Array.isArray(element)) { - index = element[0]/255*100; - r = element[1]; - g = element[2]; - b = element[3]; - } else if (element == 'r') { + if (Array.isArray(e)) { + index = Math.round(e[0]/255*100); + r = e[1]; + g = e[2]; + b = e[3]; + } else if (e == 'r') { r = Math.random() * 255; g = Math.random() * 255; b = Math.random() * 255; } else { - if (colors) { - let pos = element[1] - 1; - r = colors[pos][0]; - g = colors[pos][1]; - b = colors[pos][2]; - } + let i = e[1] - 1; + var cd = gId('csl').children; + r = parseInt(cd[i].dataset.r); + g = parseInt(cd[i].dataset.g); + b = parseInt(cd[i].dataset.b); } if (index === false) { - index = j / paletteData.length * 100; + index = Math.round(j / paletteData.length * 100); } - + gradient.push(`rgb(${r},${g},${b}) ${index}%`); } return `background: linear-gradient(to right,${gradient.join()});`; } -function generateListItemHtml(listName, id, name, clickAction, extraHtml = '', extraClass = '') +function generateListItemHtml(listName, id, name, clickAction, extraHtml = '', effectPar = '') +{ + return `
`+ + ``+ + extraHtml + + `
`; +} + +function btype(b) { - return `
- - - ${name} - -${extraHtml} -
`; -} - -function btype(b){ switch (b) { + case 2: case 32: return "ESP32"; + case 3: + case 33: return "ESP32-S2"; + case 4: + case 34: return "ESP32-S3"; + case 5: + case 35: return "ESP32-C3"; + case 1: case 82: return "ESP8266"; } return "?"; } -function bname(o){ + +function bname(o) +{ if (o.name=="WLED") return o.ip; return o.name; } @@ -821,84 +1032,119 @@ function populateNodes(i,n) var nnodes = 0; if (n.nodes) { n.nodes.sort((a,b) => (a.name).localeCompare(b.name)); - for (var x=0;x${bname(o)}`; - urows += inforow(url,`${btype(o.type)}
${o.vid==0?"N/A":o.vid}`); - nnodes++; + let onoff = ``; + var url = ``; + urows += inforow(url,`${btype(o.type&0x7F)}
${o.vid==0?"N/A":o.vid}`); + nnodes++; } } } if (i.ndc < 0) cn += `Instance List is disabled.`; else if (nnodes == 0) cn += `No other instances found.`; - cn += ` -${urows} -${inforow("Current instance:",i.name)} -
`; - d.getElementById('kn').innerHTML = cn; + cn += ` + ${inforow("Current instance:",i.name)} + ${urows} +
`; + gId('kn').innerHTML = cn; } function loadNodes() { - var url = '/json/nodes'; - if (loc) { - url = `http://${locip}/json/nodes`; - } - - fetch - (url, { + fetch(getURL('/json/nodes'), { method: 'get' }) - .then(res => { - if (!res.ok) { - showToast('Could not load Node list!', true); - } + .then((res)=>{ + if (!res.ok) showToast('Could not load Node list!', true); return res.json(); }) - .then(json => { + .then((json)=>{ + clearErrorToast(100); populateNodes(lastinfo, json); }) - .catch(function (error) { - showToast(error, true); - console.log(error); + .catch((e)=>{ + showToast(e, true); }); } -//update the 'sliderdisplay' background div of a slider for a visual indication of slider position +// update the 'sliderdisplay' background div of a slider for a visual indication of slider position function updateTrail(e) { if (e==null) return; - var max = e.hasAttribute('max') ? e.attributes.max.value : 255; - var perc = e.value * 100 / max; - perc = parseInt(perc); - if (perc < 50) perc += 2; - var val = `linear-gradient(90deg, var(--bg) ${perc}%, var(--c-4) ${perc}%)`; - e.parentNode.getElementsByClassName('sliderdisplay')[0].style.background = val; -} - -//rangetouch slider function -function updateBubble(e) -{ - var bubble = e.target.parentNode.getElementsByTagName('output')[0]; - if (bubble) { - bubble.innerHTML = e.target.value; + let sd = e.parentNode.getElementsByClassName('sliderdisplay')[0]; + if (sd && getComputedStyle(sd).getPropertyValue("--bg") !== "none") { + var max = e.hasAttribute('max') ? e.attributes.max.value : 255; + var perc = Math.round(e.value * 100 / max); + if (perc < 50) perc += 2; + var val = `linear-gradient(90deg, var(--bg) ${perc}%, var(--c-6) ${perc}%)`; + sd.style.backgroundImage = val; } + var b = e.parentNode.parentNode.getElementsByTagName('output')[0]; + if (b) b.innerHTML = e.value; } -//rangetouch slider function +// rangetouch slider function function toggleBubble(e) { - e.target.parentNode.querySelector('output').classList.toggle('hidden'); + var b = e.target.parentNode.parentNode.getElementsByTagName('output')[0]; + b.classList.toggle('sliderbubbleshow'); } -//updates segment length upon input of segment values +// updates segment length upon input of segment values function updateLen(s) { - if (!d.getElementById(`seg${s}s`)) return; - var start = parseInt(d.getElementById(`seg${s}s`).value); - var stop = parseInt(d.getElementById(`seg${s}e`).value); - var len = stop - (cfg.comp.seglen?0:start); + if (!gId(`seg${s}s`)) return; + var start = parseInt(gId(`seg${s}s`).value); + var stop = parseInt(gId(`seg${s}e`).value) + (cfg.comp.seglen?start:0); + var len = stop - start; + let sY = gId(`seg${s}sY`); + let eY = gId(`seg${s}eY`); + let sX = gId(`seg${s}s`); + let eX = gId(`seg${s}e`); + let of = gId(`seg${s}of`); + let mySH = gId("mkSYH"); + let mySD = gId("mkSYD"); + if (isM) { + // do we have 1D segment *after* the matrix? + if (start >= mw*mh) { + if (sY) { sY.value = 0; sY.max = 0; sY.min = 0; } + if (eY) { eY.value = 1; eY.max = 1; eY.min = 0; } + sX.min = mw*mh; sX.max = ledCount-1; + eX.min = mw*mh+1; eX.max = ledCount; + if (mySH) mySH.classList.add("hide"); + if (mySD) mySD.classList.add("hide"); + if (of) of.classList.remove("hide"); + } else { + // matrix setup + if (mySH) mySH.classList.remove("hide"); + if (mySD) mySD.classList.remove("hide"); + if (of) of.classList.add("hide"); + let startY = parseInt(sY.value); + let stopY = parseInt(eY.value) + (cfg.comp.seglen?startY:0); + len *= (stopY-startY); + let tPL = gId(`seg${s}lbtm`); + if (stop-start>1 && stopY-startY>1) { + // 2D segment + if (tPL) tPL.classList.remove('hide'); // unhide transpose checkbox + let sE = gId('fxlist').querySelector(`.lstI[data-id="${selectedFx}"]`); + if (sE) { + let sN = sE.querySelector(".lstIname").innerText; + let seg = gId(`seg${s}map2D`); + if (seg) { + if(sN.indexOf("\u25A6")<0) seg.classList.remove('hide'); // unhide mapping for 1D effects (| in name) + else seg.classList.add('hide'); // hide mapping otherwise + } + } + } else { + // 1D segment in 2D set-up + if (tPL) { + tPL.classList.add('hide'); // hide transpose checkbox + gId(`seg${s}tp`).checked = false; // and uncheck it + } + } + } + } var out = "(delete)"; if (len > 1) { out = `${len} LEDs`; @@ -906,77 +1152,156 @@ function updateLen(s) out = "1 LED"; } - if (d.getElementById(`seg${s}grp`) != null) + if (gId(`seg${s}grp`) != null) { - var grp = parseInt(d.getElementById(`seg${s}grp`).value); - var spc = parseInt(d.getElementById(`seg${s}spc`).value); + var grp = parseInt(gId(`seg${s}grp`).value); + var spc = parseInt(gId(`seg${s}spc`).value); if (grp == 0) grp = 1; var virt = Math.ceil(len/(grp + spc)); if (!isNaN(virt) && (grp > 1 || spc > 0)) out += ` (${virt} virtual)`; } + if (isM && start >= mw*mh) out += " [strip]"; - d.getElementById(`seg${s}len`).innerHTML = out; + gId(`seg${s}len`).innerHTML = out; } -//updates background color of currently selected preset +// updates background color of currently selected preset function updatePA() { - var ps = d.getElementsByClassName("pres"); //reset all preset buttons - for (var i of ps) { - i.classList.remove("selected"); - } - ps = d.getElementsByClassName("psts"); //reset all quick selectors - for (var i of ps) { - i.classList.remove("selected"); - } + let ps; + ps = gEBCN("pres"); for (let p of ps) p.classList.remove('selected'); + ps = gEBCN("psts"); for (let p of ps) p.classList.remove('selected'); if (currentPreset > 0) { - var acv = d.getElementById(`p${currentPreset}o`); - if (acv && !expanded[currentPreset+100]) - acv.classList.add("selected"); - acv = d.getElementById(`p${currentPreset}qlb`); - if (acv) - acv.classList.add("selected"); + var acv = gId(`p${currentPreset}o`); + if (acv /*&& !acv.classList.contains('expanded')*/) { + acv.classList.add('selected'); + /* + // scroll selected preset into view (on WS refresh) + acv.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + */ + } + acv = gId(`p${currentPreset}qlb`); + if (acv) acv.classList.add('selected'); } } function updateUI() { - d.getElementById('buttonPower').className = (isOn) ? "active":""; - d.getElementById('buttonNl').className = (nlA) ? "active":""; - d.getElementById('buttonSync').className = (syncSend) ? "active":""; + gId('buttonPower').className = (isOn) ? 'active':''; + gId('buttonNl').className = (nlA) ? 'active':''; + gId('buttonSync').className = (syncSend) ? 'active':''; + + updateSelectedFx(); + updateSelectedPalette(selectedPal); // must be after updateSelectedFx() to un-hide color slots for * palettes + + updateTrail(gId('sliderBri')); + updateTrail(gId('sliderSpeed')); + updateTrail(gId('sliderIntensity')); + + updateTrail(gId('sliderC1')); + updateTrail(gId('sliderC2')); + updateTrail(gId('sliderC3')); + + if (hasRGB) { + updateTrail(gId('sliderR')); + updateTrail(gId('sliderG')); + updateTrail(gId('sliderB')); + } + if (hasWhite) updateTrail(gId('sliderW')); - updateTrail(d.getElementById('sliderBri')); - updateTrail(d.getElementById('sliderSpeed')); - updateTrail(d.getElementById('sliderIntensity')); - d.getElementById('wwrap').style.display = (hasWhite) ? "block":"none"; - d.getElementById('wbal').style.display = (hasCCT) ? "block":"none"; var ccfg = cfg.comp.colors; - d.getElementById('hexw').style.display = ccfg.hex ? "block":"none"; - d.getElementById('pwrap').style.display = (hasRGB && ccfg.picker) ? "block":"none"; - d.getElementById('kwrap').style.display = (hasRGB && !hasCCT && ccfg.picker) ? "block":"none"; - d.getElementById('rgbwrap').style.display = (hasRGB && ccfg.rgb) ? "block":"none"; - d.getElementById('qcs-w').style.display = (hasRGB && ccfg.quick) ? "block":"none"; - d.getElementById('palwrap').style.display = hasRGB ? "block":"none"; + gId('wwrap').style.display = (hasWhite) ? "block":"none"; // white channel + gId('wbal').style.display = (hasCCT) ? "block":"none"; // white balance + gId('hexw').style.display = (ccfg.hex) ? "block":"none"; // HEX input + gId('picker').style.display = (hasRGB && ccfg.picker) ? "block":"none"; // color picker wheel + gId('hwrap').style.display = (hasRGB && !ccfg.picker) ? "block":"none"; // hue slider + gId('swrap').style.display = (hasRGB && !ccfg.picker) ? "block":"none"; // saturation slider + gId('vwrap').style.display = (hasRGB) ? "block":"none"; // brightness (value) slider + gId('kwrap').style.display = (hasRGB && !hasCCT) ? "block":"none"; // Kelvin slider + gId('rgbwrap').style.display = (hasRGB && ccfg.rgb) ? "block":"none"; // RGB sliders + gId('qcs-w').style.display = (hasRGB && ccfg.quick) ? "block":"none"; // quick selection + //gId('csl').style.display = (hasRGB || hasWhite) ? "block":"none"; // color selectors (hide for On/Off bus) + //gId('palw').style.display = (hasRGB) ? "inline-block":"none"; // palettes are shown/hidden in setEffectParameters() updatePA(); updatePSliders(); } +function updateSelectedPalette(s) +{ + var parent = gId('pallist'); + var selPaletteInput = parent.querySelector(`input[name="palette"][value="${s}"]`); + if (selPaletteInput) selPaletteInput.checked = true; + + var selElement = parent.querySelector('.selected'); + if (selElement) selElement.classList.remove('selected'); + + var selectedPalette = parent.querySelector(`.lstI[data-id="${s}"]`); + if (selectedPalette) parent.querySelector(`.lstI[data-id="${s}"]`).classList.add('selected'); + + // in case of special palettes (* Colors...), force show color selectors (if hidden by effect data) + let cd = gId('csl').children; // color selectors + if (s > 1 && s < 6) { + cd[0].classList.remove('hide'); // * Color 1 + if (s > 2) cd[1].classList.remove('hide'); // * Color 1 & 2 + if (s > 3) cd[2].classList.remove('hide'); // all colors + } else { + for (let i of cd) if (i.dataset.hide == '1') i.classList.add('hide'); + } +} + +function updateSelectedFx() +{ + var parent = gId('fxlist'); + var selEffectInput = parent.querySelector(`input[name="fx"][value="${selectedFx}"]`); + if (selEffectInput) selEffectInput.checked = true; + + var selElement = parent.querySelector('.selected'); + if (selElement) { + selElement.classList.remove('selected'); + selElement.style.bottom = null; // remove element style added in slider handling + } + + var selectedEffect = parent.querySelector(`.lstI[data-id="${selectedFx}"]`); + if (selectedEffect) { + selectedEffect.classList.add('selected'); + setEffectParameters(selectedFx); + // hide non-0D effects if segment only has 1 pixel (0D) + var fxs = parent.querySelectorAll('.lstI'); + for (const fx of fxs) { + if (!fx.dataset.opt) continue; + let opts = fx.dataset.opt.split(";"); + if (fx.dataset.id>0) { + if (segLmax==0) fx.classList.add('hide'); // none of the segments selected (hide all effects) + else { + if ((segLmax==1 && (!opts[3] || opts[3].indexOf("0")<0)) || (!isM && opts[3] && ((opts[3].indexOf("2")>=0 && opts[3].indexOf("1")<0)))) fx.classList.add('hide'); + else fx.classList.remove('hide'); + } + } + } + // hide 2D mapping and/or sound simulation options + var selectedName = selectedEffect.querySelector(".lstIname").innerText; + var segs = gId("segcont").querySelectorAll(`div[data-map="map2D"]`); + for (const seg of segs) if (selectedName.indexOf("\u25A6")<0) seg.classList.remove('hide'); else seg.classList.add('hide'); + var segs = gId("segcont").querySelectorAll(`div[data-snd="si"]`); + for (const seg of segs) if (selectedName.indexOf("\u266A")<0 && selectedName.indexOf("\u266B")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also "♫ "? + } +} + function displayRover(i,s) { - d.getElementById('rover').style.transform = (i.live && s.lor == 0) ? "translateY(0px)":"translateY(100%)"; + gId('rover').style.transform = (i.live && s.lor == 0 && i.liveseg<0) ? "translateY(0px)":"translateY(100%)"; var sour = i.lip ? i.lip:""; if (sour.length > 2) sour = " from " + sour; - d.getElementById('lv').innerHTML = `WLED is receiving live ${i.lm} data${sour}`; - d.getElementById('roverstar').style.display = (i.live && s.lor) ? "block":"none"; + gId('lv').innerHTML = `WLED is receiving live ${i.lm} data${sour}`; + gId('roverstar').style.display = (i.live && s.lor) ? "block":"none"; } -function compare(a, b) { - if (a.name < b.name) return -1; - return 1; -} -function cmpP(a, b) { - if (!a[1].n) return (a[0] > b[0]); - //return a[1].n.localeCompare(b[1].n,undefined, {numeric: true}); +function cmpP(a, b) +{ + if (cfg.comp.idsort || !a[1].n) return (parseInt(a[0]) > parseInt(b[0])); // sort playlists first, followed by presets with characters and last presets with special 1st character const c = a[1].n.charCodeAt(0); const d = b[1].n.charCodeAt(0); @@ -986,62 +1311,66 @@ function cmpP(a, b) { return n.localeCompare((b[1].playlist ? '<' : y) + b[1].n, undefined, {numeric: true}); } -//forces a WebSockets reconnect if timeout (error toast), or successful HTTP response to JSON request -function reconnectWS() { - if (ws) ws.close(); - ws = null; - if (lastinfo && lastinfo.ws > -1) setTimeout(makeWS,500); -} - function makeWS() { - if (ws) return; - ws = new WebSocket((window.location.protocol == 'https:'?'wss':'ws')+'://'+(loc?locip:window.location.hostname)+'/ws'); + if (ws || lastinfo.ws < 0) return; + let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws"; + ws = new WebSocket(url); ws.binaryType = "arraybuffer"; - ws.onmessage = function(event) { - if (event.data instanceof ArrayBuffer) return; //liveview packet - var json = JSON.parse(event.data); + ws.onmessage = (e)=>{ + if (e.data instanceof ArrayBuffer) return; // liveview packet + var json = JSON.parse(e.data); + if (json.leds) return; // JSON liveview packet clearTimeout(jsonTimeout); jsonTimeout = null; + lastUpdate = new Date(); clearErrorToast(); - d.getElementById('connind').style.backgroundColor = "#079"; - var info = json.info; - d.getElementById('buttonNodes').style.display = (info.ndc > 0 && window.innerWidth > 770) ? "block":"none"; - lastinfo = info; - if (isInfo) { - populateInfo(info); - } - s = json.state; - displayRover(info, s); - readState(json.state); + gId('connind').style.backgroundColor = "var(--c-l)"; + // json object should contain json.info AND json.state (but may not) + var i = json.info; + if (i) { + parseInfo(i); + if (isInfo) populateInfo(i); + } else + i = lastinfo; + var s = json.state ? json.state : json; + displayRover(i, s); + readState(s); }; ws.onclose = (e)=>{ - //if there is already a new web socket open, do not null ws - if (ws && ws.readyState === WebSocket.OPEN) return; - - d.getElementById('connind').style.backgroundColor = "#831"; + gId('connind').style.backgroundColor = "var(--c-r)"; + if (wsRpt++ < 5) setTimeout(makeWS,1500); // retry WS connection ws = null; } ws.onopen = (e)=>{ + //ws.send("{'v':true}"); // unnecessary (https://github.com/Aircoookie/WLED/blob/master/wled00/ws.cpp#L18) + wsRpt = 0; reqsLegal = true; } } -function readState(s,command=false) { +function readState(s,command=false) +{ + if (!s) return false; + if (s.success) return true; // no data to process + isOn = s.on; - d.getElementById('sliderBri').value= s.bri; + gId('sliderBri').value = s.bri; nlA = s.nl.on; nlDur = s.nl.dur; nlTar = s.nl.tbri; - nlMode = s.nl.mode; + nlFade = s.nl.fade; syncSend = s.udpn.send; - currentPreset = s.ps; + if (s.pl<0) currentPreset = s.ps; + else currentPreset = s.pl; + tr = s.transition; - d.getElementById('tt').value = tr/10; + gId('tt').value = tr/10; populateSegments(s); var selc=0; var sellvl=0; // 0: selc is invalid, 1: selc is mainseg, 2: selc is first selected hasRGB = hasWhite = hasCCT = false; + segLmax = 0; for (let i = 0; i < (s.seg||[]).length; i++) { if (sellvl == 0 && s.seg[i].id == s.mainseg) { @@ -1052,123 +1381,243 @@ function readState(s,command=false) { if (sellvl < 2) selc = i; // get first selected segment sellvl = 2; var lc = lastinfo.leds.seglc[s.seg[i].id]; - hasRGB |= lc & 0x01; - hasWhite |= lc & 0x02; - hasCCT |= lc & 0x04; + hasRGB |= !!(lc & 0x01); + hasWhite |= !!(lc & 0x02); + hasCCT |= !!(lc & 0x04); + let sLen = (s.seg[i].stop - s.seg[i].start)*(s.seg[i].stopY?(s.seg[i].stopY - s.seg[i].startY):1); + segLmax = segLmax < sLen ? sLen : segLmax; } } var i=s.seg[selc]; if (sellvl == 1) { var lc = lastinfo.leds.seglc[i.id]; - hasRGB = lc & 0x01; - hasWhite = lc & 0x02; - hasCCT = lc & 0x04; + hasRGB = !!(lc & 0x01); + hasWhite = !!(lc & 0x02); + hasCCT = !!(lc & 0x04); } if (!i) { showToast('No Segments!', true); updateUI(); - return; - } - - colors = i.col; - for (let e = 0; e < 3; e++) - { - if (i.col[e].length > 3) whites[e] = parseInt(i.col[e][3]); - setCSL(e); + return true; } - selectSlot(csel); - if (i.cct != null && i.cct>=0) d.getElementById("sliderA").value = i.cct; - - d.getElementById('sliderSpeed').value = i.sx; - d.getElementById('sliderIntensity').value = i.ix; - // Effects - var selFx = fxlist.querySelector(`input[name="fx"][value="${i.fx}"]`); - if (selFx) selFx.checked = true; - else location.reload(); //effect list is gone (e.g. if restoring tab). Reload. + if (s.seg.length>2) d.querySelectorAll(".pop").forEach((e)=>{e.classList.remove("hide");}); - var selElement = fxlist.querySelector('.selected'); - if (selElement) { - selElement.classList.remove('selected') + var cd = gId('csl').children; + for (let e = cd.length-1; e >= 0; e--) { + cd[e].dataset.r = i.col[e][0]; + cd[e].dataset.g = i.col[e][1]; + cd[e].dataset.b = i.col[e][2]; + if (hasWhite || (!hasRGB && !hasWhite)) { cd[e].dataset.w = i.col[e][3]; } + setCSL(cd[e]); } - var selectedEffect = fxlist.querySelector(`.lstI[data-id="${i.fx}"]`); - selectedEffect.classList.add('selected'); - selectedFx = i.fx; + selectSlot(csel); + if (i.cct != null && i.cct>=0) gId("sliderA").value = i.cct; - // Palettes - pallist.querySelector(`input[name="palette"][value="${i.pal}"]`).checked = true; - selElement = pallist.querySelector('.selected'); - if (selElement) { - selElement.classList.remove('selected') - } - pallist.querySelector(`.lstI[data-id="${i.pal}"]`).classList.add('selected'); + gId('sliderSpeed').value = i.sx; + gId('sliderIntensity').value = i.ix; + gId('sliderC1').value = i.c1 ? i.c1 : 0; + gId('sliderC2').value = i.c2 ? i.c2 : 0; + gId('sliderC3').value = i.c3 ? i.c3 : 0; + gId('checkO1').checked = !(!i.o1); + gId('checkO2').checked = !(!i.o2); + gId('checkO3').checked = !(!i.o3); - if (!command) { - selectedEffect.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }); + if (s.error && s.error != 0) { + var errstr = ""; + switch (s.error) { + case 10: + errstr = "Could not mount filesystem!"; + break; + case 11: + errstr = "Not enough space to save preset!"; + break; + case 12: + errstr = "Preset not found."; + break; + case 13: + errstr = "Missing ir.json."; + break; + case 19: + errstr = "A filesystem error has occured."; + break; + } + showToast('Error ' + s.error + ": " + errstr, true); } - if (s.error && s.error != 0) { - var errstr = ""; - switch (s.error) { - case 10: - errstr = "Could not mount filesystem!"; - break; - case 11: - errstr = "Not enough space to save preset!"; - break; - case 12: - errstr = "Preset not found."; - break; - case 19: - errstr = "A filesystem error has occured."; - break; + selectedPal = i.pal; + selectedFx = i.fx; + redrawPalPrev(); // if any color changed (random palette did at least) + updateUI(); + return true; +} + +// control HTML elements for Slider and Color Control (original ported form WLED-SR) +// Technical notes +// =============== +// If an effect name is followed by an @, slider and color control is effective. +// If not effective then: +// - For AC effects (id<128) 2 sliders and 3 colors and the palette will be shown +// - For SR effects (id>128) 5 sliders and 3 colors and the palette will be shown +// If effective (@) +// - a ; separates slider controls (left) from color controls (middle) and palette control (right) +// - if left, middle or right is empty no controls are shown +// - a , separates slider controls (max 5) or color controls (max 3). Palette has only one value +// - a ! means that the default is used. +// - For sliders: Effect speeds, Effect intensity, Custom 1, Custom 2, Custom 3 +// - For colors: Fx color, Background color, Custom +// - For palette: prompt for color palette OR palette ID if numeric (will hide palette selection) +// +// Note: If palette is on and no colors are specified 1,2 and 3 is shown in each color circle. +// If a color is specified, the 1,2 or 3 is replaced by that specification. +// Note: Effects can override default pattern behaviour +// - FadeToBlack can override the background setting +// - Defining SEGCOL() can override a specific palette using these values (e.g. Color Gradient) +function setEffectParameters(idx) +{ + if (!(Array.isArray(fxdata) && fxdata.length>idx)) return; + var controlDefined = fxdata[idx].length; + var effectPar = fxdata[idx]; + var effectPars = (effectPar == '')?[]:effectPar.split(";"); + var slOnOff = (effectPars.length==0 || effectPars[0]=='')?[]:effectPars[0].split(","); + var coOnOff = (effectPars.length<2 || effectPars[1]=='')?[]:effectPars[1].split(","); + var paOnOff = (effectPars.length<3 || effectPars[2]=='')?[]:effectPars[2].split(","); + + // set html slider items on/off + let nSliders = 5; + for (let i=0; ii && slOnOff[i] != "")) { + if (slOnOff.length>i && slOnOff[i]!="!") label.innerHTML = slOnOff[i]; + else if (i==0) label.innerHTML = "Effect speed"; + else if (i==1) label.innerHTML = "Effect intensity"; + else label.innerHTML = "Custom" + (i-1); + slider.classList.remove('hide'); + } else { + slider.classList.add('hide'); + } + } + if (slOnOff.length>5) { // up to 3 checkboxes + gId('fxopt').classList.remove('fade'); + for (let i = 0; i<3; i++) { + if (5+ii && coOnOff[i] != "") { + btn.classList.remove('hide'); + btn.dataset.hide = 0; + if (coOnOff[i] != "!") { + var abbreviation = coOnOff[i].substr(0,2); + btn.innerHTML = abbreviation; + if (abbreviation != coOnOff[i]) { + cslLabel += sep + abbreviation + '=' + coOnOff[i]; + sep = ', '; + } } - showToast('Error ' + s.error + ": " + errstr, true); + else if (i==0) btn.innerHTML = "Fx"; + else if (i==1) btn.innerHTML = "Bg"; + else btn.innerHTML = "Cs"; + if (!cslCnt || oCsel==i) selectSlot(i); // select 1st displayed slot or old one + cslCnt++; + } else if (!controlDefined) { // if no controls then all buttons should be shown for color 1..3 + btn.classList.remove('hide'); + btn.dataset.hide = 0; + btn.innerHTML = `${i+1}`; + if (!cslCnt || oCsel==i) selectSlot(i); // select 1st displayed slot or old one + cslCnt++; + } else { + btn.classList.add('hide'); + btn.dataset.hide = 1; + btn.innerHTML = `${i+1}`; // name hidden buttons 1..3 for * palettes + } } - updateUI(); + gId("cslLabel").innerHTML = cslLabel; + + // set palette on/off + var palw = gId("palw"); // wrapper + var pall = gId("pall"); // label + // if not controlDefined or palette has a value + if (hasRGB && ((!controlDefined) || (paOnOff.length>0 && paOnOff[0]!="" && isNaN(paOnOff[0])))) { + palw.style.display = "inline-block"; + if (paOnOff.length>0 && paOnOff[0].indexOf("=")>0) { + // embeded default values + var dPos = paOnOff[0].indexOf("="); + var v = Math.max(0,Math.min(255,parseInt(paOnOff[0].substr(dPos+1)))); + paOnOff[0] = paOnOff[0].substring(0,dPos); + } + if (paOnOff.length>0 && paOnOff[0] != "!") pall.innerHTML = paOnOff[0]; + else pall.innerHTML = ' Color palette'; + } else { + // disable palette list + pall.innerHTML = ' Color palette not used'; + palw.style.display = "none"; + } + // not all color selectors shown, hide palettes created from color selectors + // NOTE: this will disallow user to select "* Color ..." palettes which may be undesirable in some cases or for some users + //for (let e of (gId('pallist').querySelectorAll('.lstI')||[])) { + // let fltr = "* C"; + // if (cslCnt==1 && csel==0) fltr = "* Colors"; + // else if (cslCnt==2) fltr = "* Colors Only"; + // if (cslCnt < 3 && e.querySelector('.lstIname').innerText.indexOf(fltr)>=0) e.classList.add('hide'); else e.classList.remove('hide'); + //} } var jsonTimeout; var reqsLegal = false; -function requestJson(command, rinfo = true) { - d.getElementById('connind').style.backgroundColor = "#a90"; - if (command && !reqsLegal) return; //stop post requests from chrome onchange event on page restore - lastUpdate = new Date(); - if (!jsonTimeout) jsonTimeout = setTimeout(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 url = rinfo ? '/json/si': (command ? '/json/state':'/json'); - if (loc) { - url = `http://${locip}${url}`; - } - - var useWs = ((command || rinfo) && ws && ws.readyState === WebSocket.OPEN); - + var useWs = (ws && ws.readyState === WebSocket.OPEN); var type = command ? 'post':'get'; - if (command) - { - command.v = true; //get complete API response + if (command) { + command.v = true; // force complete /json/si API response command.time = Math.floor(Date.now() / 1000); - var t = d.getElementById('tt'); - if (t.validity.valid && command.transition===undefined) { + var t = gId('tt'); + if (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 > 1000) useWs = false; //do not send very long requests over websocket - } + 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 - (url, { + fetch(getURL('/json/si'), { method: type, headers: { "Content-type": "application/json; charset=UTF-8" @@ -1176,86 +1625,54 @@ function requestJson(command, rinfo = true) { body: req }) .then(res => { - if (!res.ok) { - showErrorToast(); - } + clearTimeout(jsonTimeout); + jsonTimeout = null; + if (!res.ok) showErrorToast(); return res.json(); }) .then(json => { - clearTimeout(jsonTimeout); - jsonTimeout = null; - clearErrorToast(); - d.getElementById('connind').style.backgroundColor = "#070"; - if (!json) { - showToast('Empty response', true); - } - if (json.success) { - return; - } - var s = json; - if (reqsLegal && !ws) reconnectWS(); - - if (!command || rinfo) { //we have info object - if (!rinfo) { //entire JSON (on load) - populateEffects(json.effects); - populatePalettes(json.palettes); - - //load palette previews, presets, and open websocket sequentially - setTimeout(function(){ - loadPresets(function(){ - loadPalettesData(function(){ - if (!ws && json.info.ws > -1) makeWS(); - }); - }); - },25); - - reqsLegal = true; - } - - var info = json.info; - var name = info.name; - d.getElementById('namelabel').innerHTML = name; - if (name === "Dinnerbone") { - d.documentElement.style.transform = "rotate(180deg)"; - } - if (info.live) { - name = "(Live) " + name; - } - if (loc) { - name = "(L) " + name; - } - d.title = name; - ledCount = info.leds.count; - syncTglRecv = info.str; - maxSeg = info.leds.maxseg; - pmt = info.fs.pmt; - - if (!command && rinfo) setTimeout(loadPresets, 99); - - d.getElementById('buttonNodes').style.display = (info.ndc > 0 && window.innerWidth > 770) ? "block":"none"; - lastinfo = info; - if (isInfo) { - populateInfo(info); - } - s = json.state; - displayRover(info, s); + 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); } - - readState(s,command); + 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; }) - .catch(function (error) { - showToast(error, true); - console.log(error); + .catch((e)=>{ + showToast(e, true); }); } -function togglePower() { +function togglePower() +{ isOn = !isOn; var obj = {"on": isOn}; + if (isOn && lastinfo && lastinfo.live && lastinfo.liveseg>=0) { + obj.live = false; + obj.seg = []; + obj.seg[0] = {"id": lastinfo.liveseg, "frz": false}; + } requestJson(obj); } -function toggleNl() { +function toggleNl() +{ nlA = !nlA; if (nlA) { @@ -1267,115 +1684,141 @@ function toggleNl() { requestJson(obj); } -function toggleSync() { +function toggleSync() +{ syncSend = !syncSend; - if (syncSend) - { - showToast('Other lights in the network will now sync to this one.'); - } else { - showToast('This light and other lights in the network will no longer sync.'); - } + if (syncSend) showToast('Other lights in the network will now sync to this one.'); + else showToast('This light and other lights in the network will no longer sync.'); var obj = {"udpn": {"send": syncSend}}; if (syncTglRecv) obj.udpn.recv = syncSend; requestJson(obj); } -function toggleLiveview() { +function toggleLiveview() +{ + if (isInfo && isM) toggleInfo(); + if (isNodes && isM) toggleNodes(); isLv = !isLv; - d.getElementById('liveview').style.display = (isLv) ? "block":"none"; - var url = loc ? `http://${locip}/liveview`:"/liveview"; - d.getElementById('liveview').src = (isLv) ? url:"about:blank"; - d.getElementById('buttonSr').className = (isLv) ? "active":""; - if (!isLv && ws && ws.readyState === WebSocket.OPEN) ws.send('{"lv":false}'); + let wsOn = ws && ws.readyState === WebSocket.OPEN; + + var lvID = "liveview"; + if (isM && wsOn) { + lvID += "2D"; + if (isLv) gId('klv2D').innerHTML = ``; + gId('mlv2D').style.transform = (isLv) ? "translateY(0px)":"translateY(100%)"; + } + + gId(lvID).style.display = (isLv) ? "block":"none"; + gId(lvID).src = (isLv) ? getURL("/" + lvID + ((wsOn) ? "?ws":"")):"about:blank"; + gId('buttonSr').classList.toggle("active"); + if (!isLv && wsOn) ws.send('{"lv":false}'); size(); } -function toggleInfo() { +function toggleInfo() +{ if (isNodes) toggleNodes(); + if (isLv && isM) toggleLiveview(); isInfo = !isInfo; - if (isInfo) populateInfo(lastinfo); - d.getElementById('info').style.transform = (isInfo) ? "translateY(0px)":"translateY(100%)"; - d.getElementById('buttonI').className = (isInfo) ? "active":""; + if (isInfo) requestJson(); + gId('info').style.transform = (isInfo) ? "translateY(0px)":"translateY(100%)"; + gId('buttonI').className = (isInfo) ? "active":""; } -function toggleNodes() { +function toggleNodes() +{ if (isInfo) toggleInfo(); + if (isLv && isM) toggleLiveview(); isNodes = !isNodes; - d.getElementById('nodes').style.transform = (isNodes) ? "translateY(0px)":"translateY(100%)"; - d.getElementById('buttonNodes').className = (isNodes) ? "active":""; if (isNodes) loadNodes(); + gId('nodes').style.transform = (isNodes) ? "translateY(0px)":"translateY(100%)"; + gId('buttonNodes').className = (isNodes) ? "active":""; } -function makeSeg() { - var ns = 0; - if (lowestUnused > 0) { - var pend = parseInt(d.getElementById(`seg${lowestUnused -1}e`).value,10) + (cfg.comp.seglen?parseInt(d.getElementById(`seg${lowestUnused -1}s`).value,10):0); - if (pend < ledCount) ns = pend; - } - var cn = `
-
- New segment ${lowestUnused} - -
-
-
- - - - - - - - - - - - -
Start LED${cfg.comp.seglen?"Length":"Stop LED"}Apply
-
${ledCount - ns} LED${ledCount - ns >1 ? "s":""}
-
-
`; - d.getElementById('segutil').innerHTML = cn; -} - -function resetUtil() { - var cn = `
`; - d.getElementById('segutil').innerHTML = cn; +function makeSeg() +{ + var ns = 0, ct = 0; + var lu = lowestUnused; + let li = lastinfo; + if (lu > 0) { + let xend = parseInt(gId(`seg${lu -1}e`).value,10) + (cfg.comp.seglen?parseInt(gId(`seg${lu -1}s`).value,10):0); + if (isM) { + ns = 0; + ct = mw; + } else { + if (xend < ledCount) ns = xend; + ct = ledCount-(cfg.comp.seglen?ns:0) + } + } + gId('segutil').scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + var cn = `
`+ + `
`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `
${isM?'Start X':'Start LED'}${isM?(cfg.comp.seglen?"Width":"Stop X"):(cfg.comp.seglen?"LED count":"Stop LED")}
Start Y${cfg.comp.seglen?'Height':'Stop Y'}
`+ + `
${ledCount - ns} LEDs
`+ + `
`+ + `
`+ + `
`; + gId('segutil').innerHTML = cn; +} + +function resetUtil(off=false) +{ + gId('segutil').innerHTML = `
` + + '' + + `
Add segment
` + + '
' + + `` + + '
' + + '
'; } -var plJson = {"0":{ - "ps": [0], - "dur": [100], - "transition": [-1], //to be inited to default transition dur - "repeat": 0, - "r": false, - "end": 0 -}}; - -function makePlSel(incPl=false) { +function makePlSel(el, incPl=false) +{ var plSelContent = ""; delete pJson["0"]; // remove filler preset var arr = Object.entries(pJson); - for (var i = 0; i < arr.length; i++) { - var n = arr[i][1].n ? arr[i][1].n : "Preset " + arr[i][0]; - if (!incPl && arr[i][1].playlist && arr[i][1].playlist.ps) continue; //remove playlists, sub-playlists not yet supported - plSelContent += `` + for (var a of arr) { + var n = a[1].n ? a[1].n : "Preset " + a[0]; + if (cfg.comp.idsort) n = a[0] + ' ' + n; + if (!incPl && a[1].playlist && a[1].playlist.ps) continue; // remove playlists, sub-playlists not yet supported + plSelContent += `` } return plSelContent; } -function refreshPlE(p) { - var plEDiv = d.getElementById(`ple${p}`); +function refreshPlE(p) +{ + var plEDiv = gId(`ple${p}`); if (!plEDiv) return; - var content = ""; + var content = "
Playlist entries
"; for (var i = 0; i < plJson[p].ps.length; i++) { content += makePlEntry(p,i); } + content += `
`; plEDiv.innerHTML = content; var dels = plEDiv.getElementsByClassName("btn-pl-del"); if (dels.length < 2) dels[0].style.display = "none"; - var sels = d.getElementById(`seg${p+100}`).getElementsByClassName("sel"); + var sels = gId(`seg${p+100}`).getElementsByClassName("sel"); for (var i of sels) { if (i.dataset.val) { if (parseInt(i.dataset.val) > 0) i.value = i.dataset.val; @@ -1384,15 +1827,17 @@ function refreshPlE(p) { } } -//p: preset ID, i: ps index -function addPl(p,i) { +// p: preset ID, i: ps index +function addPl(p,i) +{ plJson[p].ps.splice(i+1,0,0); plJson[p].dur.splice(i+1,0,plJson[p].dur[i]); plJson[p].transition.splice(i+1,0,plJson[p].transition[i]); refreshPlE(p); } -function delPl(p,i) { +function delPl(p,i) +{ if (plJson[p].ps.length < 2) return; plJson[p].ps.splice(i,1); plJson[p].dur.splice(i,1); @@ -1400,186 +1845,266 @@ function delPl(p,i) { refreshPlE(p); } -function plePs(p,i,field) { +function plePs(p,i,field) +{ plJson[p].ps[i] = parseInt(field.value); } -function pleDur(p,i,field) { +function pleDur(p,i,field) +{ if (field.validity.valid) plJson[p].dur[i] = Math.floor(field.value*10); } -function pleTr(p,i,field) { +function pleTr(p,i,field) +{ if (field.validity.valid) plJson[p].transition[i] = Math.floor(field.value*10); } -function plR(p) { +function plR(p) +{ var pl = plJson[p]; - pl.r = d.getElementById(`pl${p}rtgl`).checked; - if (d.getElementById(`pl${p}rptgl`).checked) { //infinite + pl.r = gId(`pl${p}rtgl`).checked; + if (gId(`pl${p}rptgl`).checked) { // infinite pl.repeat = 0; delete pl.end; - d.getElementById(`pl${p}o1`).style.display = "none"; + gId(`pl${p}o1`).style.display = "none"; } else { - pl.repeat = parseInt(d.getElementById(`pl${p}rp`).value); - pl.end = parseInt(d.getElementById(`pl${p}selEnd`).value); - d.getElementById(`pl${p}o1`).style.display = "block"; + pl.repeat = parseInt(gId(`pl${p}rp`).value); + pl.end = parseInt(gId(`pl${p}selEnd`).value); + gId(`pl${p}o1`).style.display = "block"; } } -function makeP(i,pl) { +function makeP(i,pl) +{ var content = ""; if (pl) { + if (i===0) plJson[0] = { + ps: [1], + dur: [100], + transition: [tr], + repeat: 0, + r: false, + end: 0 + }; var rep = plJson[i].repeat ? plJson[i].repeat : 0; - content = `
Playlist Entries
-
-
+ + +
\ No newline at end of file diff --git a/wled00/data/liveviewws.htm b/wled00/data/liveviewws.htm deleted file mode 100644 index 9234d317ce..0000000000 --- a/wled00/data/liveviewws.htm +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - WLED Live Preview - - - -
- - - \ No newline at end of file diff --git a/wled00/data/liveviewws2D.htm b/wled00/data/liveviewws2D.htm new file mode 100644 index 0000000000..c50f40fbc6 --- /dev/null +++ b/wled00/data/liveviewws2D.htm @@ -0,0 +1,84 @@ + + + + + + + WLED Live Preview + + + + + + + \ No newline at end of file diff --git a/wled00/data/msg.htm b/wled00/data/msg.htm index e25aeda00c..2bb7e8825b 100644 --- a/wled00/data/msg.htm +++ b/wled00/data/msg.htm @@ -4,27 +4,13 @@ WLED Message - + diff --git a/wled00/data/pixart/boxdraw.js b/wled00/data/pixart/boxdraw.js new file mode 100644 index 0000000000..c000c2e612 --- /dev/null +++ b/wled00/data/pixart/boxdraw.js @@ -0,0 +1,62 @@ +function drawBoxes(inputPixelArray, widthPixels, heightPixels) { + + var w = window; + + // Get the canvas context + var ctx = canvas.getContext('2d', { willReadFrequently: true }); + + // Set the width and height of the canvas + if (w.innerHeight < w.innerWidth) { + canvas.width = Math.floor(w.innerHeight * 0.98); + } + else{ + canvas.width = Math.floor(w.innerWidth * 0.98); + } + //canvas.height = w.innerWidth; + + let pixelSize = Math.floor(canvas.width/widthPixels); + + let xOffset = (w.innerWidth - (widthPixels * pixelSize))/2 + + //Set the canvas height to fit the right number of pixelrows + canvas.height = (pixelSize * heightPixels) + 10 + + //Iterate through the matrix + for (let y = 0; y < heightPixels; y++) { + for (let x = 0; x < widthPixels; x++) { + + // Calculate the index of the current pixel + let i = (y*widthPixels) + x; + + //Gets the RGB of the current pixel + let pixel = inputPixelArray[i]; + + let pixelColor = 'rgb(' + pixel[0] + ', ' + pixel[1] + ', ' + pixel[2] + ')'; + + let textColor = 'rgb(128,128,128)'; + + // Set the fill style to the pixel color + ctx.fillStyle = pixelColor; + + //Draw the rectangle + ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); + + // Draw a border on the box + ctx.strokeStyle = '#888888'; + ctx.lineWidth = 1; + ctx.strokeRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); + + //Write text to box + ctx.font = "10px Arial"; + ctx.fillStyle = textColor; + ctx.textAlign = "center"; + ctx.textBaseline = 'middle'; + ctx.fillText((pixel[4] + 1), (x * pixelSize) + (pixelSize /2), (y * pixelSize) + (pixelSize /2)); + } + } + var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.width, canvas.height); + canvas.width = w.innerWidth; + ctx.putImageData(imageData, xOffset, 0); +} + diff --git a/wled00/data/pixart/favicon-16x16.png b/wled00/data/pixart/favicon-16x16.png new file mode 100644 index 0000000000..feb51ca09f Binary files /dev/null and b/wled00/data/pixart/favicon-16x16.png differ diff --git a/wled00/data/pixart/favicon-32x32.png b/wled00/data/pixart/favicon-32x32.png new file mode 100644 index 0000000000..a3b5cceb11 Binary files /dev/null and b/wled00/data/pixart/favicon-32x32.png differ diff --git a/wled00/data/pixart/favicon.ico b/wled00/data/pixart/favicon.ico new file mode 100644 index 0000000000..bde8945e58 Binary files /dev/null and b/wled00/data/pixart/favicon.ico differ diff --git a/wled00/data/pixart/getPixelValues.js b/wled00/data/pixart/getPixelValues.js new file mode 100644 index 0000000000..7f4265fd8f --- /dev/null +++ b/wled00/data/pixart/getPixelValues.js @@ -0,0 +1,320 @@ +function getPixelRGBValues(base64Image) { + httpArray = []; + fileJSON = `{"on":true,"bri":${brgh.value},"seg":{"id":${tSg.value},"i":[`; + + //Which object holds the secret to the segment ID + + let segID = 0; + if(tSg.style.display == "flex"){ + segID = tSg.value + } else { + segID = sID.value; + } + + + //const copyJSONledbutton = gId('copyJSONledbutton'); + const maxNoOfColorsInCommandSting = parseInt(cLN.value); + + let hybridAddressing = false; + let selectedIndex = -1; + + selectedIndex = frm.selectedIndex; + const formatSelection = frm.options[selectedIndex].value; + + + selectedIndex = lSS.selectedIndex; + const ledSetupSelection = lSS.options[selectedIndex].value; + + selectedIndex = cFS.selectedIndex; + let hexValueCheck = true; + if (cFS.options[selectedIndex].value == 'dec'){ + hexValueCheck = false + } + + selectedIndex = aS.selectedIndex; + let segmentValueCheck = true; //If Range or Hybrid + if (aS.options[selectedIndex].value == 'single'){ + segmentValueCheck = false + } else if (aS.options[selectedIndex].value == 'hybrid'){ + hybridAddressing = true; + } + + let curlString = '' + let haString = '' + + let colorSeparatorStart = '"'; + let colorSeparatorEnd = '"'; + if (!hexValueCheck){ + colorSeparatorStart = '['; + colorSeparatorEnd = ']'; + } + // Warnings + let hasTransparency = false; //If alpha < 255 is detected on any pixel, this is set to true in code below + let imageInfo = ''; + + // Create an off-screen canvas + var canvas = cE('canvas'); + var context = canvas.getContext('2d', { willReadFrequently: true }); + + // Create an image element and set its src to the base64 image + var image = new Image(); + image.src = base64Image; + + // Wait for the image to load before drawing it onto the canvas + image.onload = function() { + + let scalePath = scDiv.children[0].children[0]; + let color = scalePath.getAttribute("fill"); + let sizeX = szX.value; + let sizeY = szY.value; + + if (color != accentColor || sizeX < 1 || sizeY < 1){ + //image will not be resized Set desired size to original size + sizeX = image.width; + sizeY = image.height; + //failsafe for not generating huge images automatically + if (image.width > 512 || image.height > 512) + { + sizeX = 16; + sizeY = 16; + } + } + + // Set the canvas size to the same as the desired image size + canvas.width = sizeX; + canvas.height = sizeY; + + imageInfo = '

Width: ' + sizeX + ', Height: ' + sizeY + ' (make sure this matches your led matrix setup)

' + + // Draw the image onto the canvas + context.drawImage(image, 0, 0, sizeX, sizeY); + + // Get the pixel data from the canvas + var pixelData = context.getImageData(0, 0, sizeX, sizeY).data; + + // Create an array to hold the RGB values of each pixel + var pixelRGBValues = []; + + // If the first row of the led matrix is right -> left + let right2leftAdjust = 1; + + if (ledSetupSelection == 'l2r'){ + right2leftAdjust = 0; + } + + // Loop through the pixel data and get the RGB values of each pixel + for (var i = 0; i < pixelData.length; i += 4) { + var r = pixelData[i]; + var g = pixelData[i + 1]; + var b = pixelData[i + 2]; + var a = pixelData[i + 3]; + + let pixel = i/4 + let row = Math.floor(pixel/sizeX); + let led = pixel; + if (ledSetupSelection == 'matrix'){ + //Do nothing, the matrix is set upp like the index in the image + //Every row starts from the left, i.e. no zigzagging + } + else if ((row + right2leftAdjust) % 2 === 0) { + //Setup is traditional zigzag + //right2leftAdjust basically flips the row order if = 1 + //Row is left to right + //Leave led index as pixel index + + } else { + //Setup is traditional zigzag + //Row is right to left + //Invert index of row for led + let indexOnRow = led - (row * sizeX); + let maxIndexOnRow = sizeX - 1; + let reversedIndexOnRow = maxIndexOnRow - indexOnRow; + led = (row * sizeX) + reversedIndexOnRow; + } + + // Add the RGB values to the pixel RGB values array + pixelRGBValues.push([r, g, b, a, led, pixel, row]); + } + + pixelRGBValues.sort((a, b) => a[5] - b[5]); + + //Copy the values to a new array for resorting + let ledRGBValues = [... pixelRGBValues]; + + //Sort the array based on led index + ledRGBValues.sort((a, b) => a[4] - b[4]); + + //Generate JSON in WLED format + let JSONledString = ''; + + //Set starting values for the segment check to something that is no color + let segmentStart = -1; + let maxi = ledRGBValues.length; + let curentColorIndex = 0 + let commandArray = []; + + //For every pixel in the LED array + for (let i = 0; i < maxi; i++) { + let pixel = ledRGBValues[i]; + let r = pixel[0]; + let g = pixel[1]; + let b = pixel[2]; + let a = pixel[3]; + let segmentString = ''; + let segmentEnd = -1; + + if(segmentValueCheck){ + if (segmentStart < 0){ + //This is the first led of a new segment + segmentStart = i; + } //Else we allready have a start index + + if (i < maxi - 1){ + + let iNext = i + 1; + let nextPixel = ledRGBValues[iNext]; + + if (nextPixel[0] != r || nextPixel[1] != g || nextPixel[2] != b ){ + //Next pixel has new color + //The current segment ends with this pixel + segmentEnd = i + 1 //WLED wants the NEXT LED as the stop led... + if (segmentStart == i && hybridAddressing){ + //If only one led/pixel, no segment info needed + if (JSONledString == ''){ + //If addressing is single, we need to start every command with a starting possition + segmentString = '' + i + ','; + //Fixed to b2 + } else{ + segmentString = '' + } + } + else { + segmentString = segmentStart + ',' + segmentEnd + ','; + } + } + + } else { + //This is the last pixel, so the segment must end + segmentEnd = i + 1; + + if (segmentStart + 1 == segmentEnd && hybridAddressing){ + //If only one led/pixel, no segment info needed + if (JSONledString == ''){ + //If addressing is single, we need to start every command with a starting possition + segmentString = '' + i + ','; + //Fixed to b2 + } else{ + segmentString = '' + } + } + else { + segmentString = segmentStart + ',' + segmentEnd + ','; + } + } + } else{ + //Write every pixel + if (JSONledString == ''){ + //If addressing is single, we need to start every command with a starting possition + JSONledString = i + //Fixed to b2 + } + + segmentStart = i + segmentEnd = i + //Segment string should be empty for when addressing single. So no need to set it again. + } + + if (a < 255){ + hasTransparency = true; //If ANY pixel has alpha < 255 then this is set to true to warn the user + } + + if (segmentEnd > -1){ + //This is the last pixel in the segment, write to the JSONledString + //Return color value in selected format + let colorValueString = r + ',' + g + ',' + b ; + + if (hexValueCheck){ + const [red, green, blue] = [r, g, b]; + colorValueString = `${[red, green, blue].map(x => x.toString(16).padStart(2, '0')).join('')}`; + } else{ + //do nothing, allready set + } + + // Check if start and end is the same, in which case remove + + JSONledString += segmentString + colorSeparatorStart + colorValueString + colorSeparatorEnd; + fileJSON = JSONledString + segmentString + colorSeparatorStart + colorValueString + colorSeparatorEnd; + + curentColorIndex = curentColorIndex + 1; // We've just added a new color to the string so up the count with one + + if (curentColorIndex % maxNoOfColorsInCommandSting === 0 || i == maxi - 1) { + + //If we have accumulated the max number of colors to send in a single command or if this is the last pixel, we should write the current colorstring to the array + commandArray.push(JSONledString); + JSONledString = ''; //Start on an new command string + } else + { + //Add a comma to continue the command string + JSONledString = JSONledString + ',' + } + //Reset segment values + segmentStart = - 1; + } + } + + JSONledString = '' + + //For every commandString in the array + for (let i = 0; i < commandArray.length; i++) { + let thisJSONledString = `{"on":true,"bri":${brgh.value},"seg":{"id":${segID},"i":[${commandArray[i]}]}}`; + httpArray.push(thisJSONledString); + + let thiscurlString = `curl -X POST "http://${gurl.value}/json/state" -d \'${thisJSONledString}\' -H "Content-Type: application/json"`; + + //Aggregated Strings That should be returned to the user + if (i > 0){ + JSONledString = JSONledString + '\n\n'; + curlString = curlString + ' && '; + } + JSONledString += thisJSONledString; + curlString += thiscurlString; + } + + + haString = `#Uncomment if you don\'t allready have these defined in your switch section of your configuration.yaml +#- platform: command_line + #switches: + ${haIDe.value} + friendly_name: ${haNe.value} + unique_id: ${haUe.value} + command_on: > + ${curlString} + command_off: > + curl -X POST "http://${gurl.value}/json/state" -d \'{"on":false}\' -H "Content-Type: application/json"`; + + if (formatSelection == 'wled'){ + JLD.value = JSONledString; + } else if (formatSelection == 'curl'){ + JLD.value = curlString; + } else if (formatSelection == 'ha'){ + JLD.value = haString; + } else { + JLD.value = 'ERROR!/n' + formatSelection + ' is an unknown format.' + } + + fileJSON += ']}}'; + + let infoDiv = imin; + let canvasDiv = imin; + if (hasTransparency){ + imageInfo = imageInfo + '

WARNING! Transparency info detected in image. Transparency (alpha) has been ignored. To ensure you get the result you desire, use only solid colors in your image.

' + } + + infoDiv.innerHTML = imageInfo; + canvasDiv.style.display = "block" + + + //Drawing the image + drawBoxes(pixelRGBValues, sizeX, sizeY); + } +} \ No newline at end of file diff --git a/wled00/data/pixart/pixart.css b/wled00/data/pixart/pixart.css new file mode 100644 index 0000000000..39ba1f2836 --- /dev/null +++ b/wled00/data/pixart/pixart.css @@ -0,0 +1,324 @@ + +.box { + border: 2px solid #fff; +} +body { + font-family: Arial, sans-serif; + background-color: #111; +} + +.top-part { + width: 600px; + margin: 0 auto; +} +.container { + max-width: 100% -40px; + border-radius: 0px; + padding: 20px; + text-align: center; +} +h1 { + font-size: 2.3em; + color: #ddd; + margin: 1px 0; + font-family: Arial, sans-serif; + line-height: 0.5; + /*text-align: center;*/ +} +h2 { + font-size: 1.1em; + color: rgba(221, 221, 221, 0.61); + margin: 1px 0; + font-family: Arial, sans-serif; + line-height: 0.5; + text-align: center; +} +h3 { + font-size: 0.7em; + color: rgba(221, 221, 221, 0.61); + margin: 1px 0; + font-family: Arial, sans-serif; + line-height: 1.4; + text-align: center; + align-items: center; + justify-content: center; + display: flex; +} + +p { + font-size: 1em; + color: #777; + line-height: 1.5; + font-family: Arial, sans-serif; +} + +#fieldTable { + font-size: 1 em; + color: #777; + line-height: 1; + font-family: Arial, sans-serif; +} + +#scaleTable { + font-size: 1 em; + color: #777; + line-height: 1; + font-family: Arial, sans-serif; +} + +#drop-zone { + display: block; + width: 100%-40px; + border: 3px dashed #ddd; + border-radius: 0px; + text-align: center; + padding: 20px; + margin: 0px; + cursor: pointer; + font-family: Arial, sans-serif; + font-size: 15px; + color: #777; +} + +#file-picker { + display: none; +} +.adaptiveTD{ + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + +} + +.mainSelector { + background-color: #222; + color: #ddd; + border: 1px solid #333; + margin-top: 4px; + margin-bottom: 4px; + padding: 0 8px; + height: 28px; + font-size: 15px; + border-radius: 7px; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.adaptiveSelector { + background-color: #222; + color: #ddd; + border: 1px solid #333; + margin-top: 4px; + margin-bottom: 4px; + padding: 0 8px; + height: 28px; + font-size: 15px; + border-radius: 7px; + flex-grow: 1; + display: none; +} + +.segmentsDiv{ + width: 36px; + padding-left: 5px; +} + +* input[type=range] { + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + flex-grow: 1; + padding: 0; + margin: 4px 8px 4px 0; + background-color: transparent; + cursor: pointer; + background: linear-gradient(to right, #bbb 50%, #333 50%); + border-radius: 7px; +} + +input[type=range]:focus { + outline: none; +} +input[type=range]::-webkit-slider-runnable-track { + height: 28px; + cursor: pointer; + background: transparent; + border-radius: 7px; +} +input[type=range]::-webkit-slider-thumb { + height: 16px; + width: 16px; + border-radius: 50%; + background: #fff; + cursor: pointer; + -webkit-appearance: none; + margin-top: 4px; + border-radius: 7px; +} +input[type=range]::-moz-range-track { + height: 28px; + background-color: rgba(0, 0, 0, 0); + border-radius: 7px; +} +input[type=range]::-moz-range-thumb { + border: 0px solid rgba(0, 0, 0, 0); + height: 16px; + width: 16px; + border-radius: 7px; + background: #fff; +} + +.rangeNumber{ + width: 20px; + vertical-align: middle; +} + +.fullTextField[type=text] { + background-color: #222; + border: 1px solid #333; + padding-inline-start: 5px; + margin-top: 4px; + margin-bottom: 4px; + height: 24px; + border-radius: 0px; + font-family: Arial, sans-serif; + font-size: 15px; + color: #ddd; + border-radius: 7px; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; +} +.flxTFld{ + background-color: #222; + border: 1px solid #333; + padding-inline-start: 5px; + height: 24px; + border-radius: 0px; + font-family: Arial, sans-serif; + font-size: 15px; + color: #ddd; + border-radius: 7px; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; +} + +* input[type=submit] { + background-color: #222; + border: 1px solid #333; + padding: 0.5em; + width: 100%; + border-radius: 24px; + font-family: Arial, sans-serif; + font-size: 1.3em; + color: #ddd; +} + +* button { + background-color: #222; + border: 1px solid #333; + padding-inline: 5px; + width: 100%; + border-radius: 24px; + font-family: Arial, sans-serif; + font-size: 1em; + color: #ddd; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +#scaleDiv { + display: flex; + align-items: center; + vertical-align: middle; +} + +textarea { + grid-row: 1 / 2; + width: 100%; + height: 200px; + background-color: #222; + border: 1px solid #333; + color: #ddd; +} +.hide { + display: none; +} + +.svg-icon { + vertical-align: middle; +} +#image-container { + display: grid; + grid-template-rows: 1fr 1fr; +} +#button-container { + display: flex; + padding-bottom: 10px; + padding-top: 10px; +} + +.buttonclass { + flex: 1; + padding-top: 5px; + padding-bottom: 5px; +} + +.gap { + width: 10px; +} + +#submitConvert::before { + content: ""; + display: inline-block; + background-image: url('data:image/svg+xml;utf8, '); + width: 36px; + height: 36px; +} + +#sizeDiv * { + display: inline-block; +} +.sizeInputFields{ + width: 50px; + background-color: #222; + border: 1px solid #333; + padding-inline-start: 5px; + margin-top: -5px; + height: 24px; + border-radius: 7px; + font-family: Arial, sans-serif; + font-size: 15px; + color: #ddd; +} +a:link { + color: rgba(221, 221, 221, 0.61); + background-color: transparent; + text-decoration: none; +} + +a:visited { + color: rgba(221, 221, 221, 0.61); + background-color: transparent; + text-decoration: none; +} + +a:hover { + color: #ddd; + background-color: transparent; + text-decoration: none; +} + +a:active { + color: rgba(221, 221, 221, 0.61); + background-color: transparent; + text-decoration: none; +} \ No newline at end of file diff --git a/wled00/data/pixart/pixart.htm b/wled00/data/pixart/pixart.htm new file mode 100644 index 0000000000..c67ac46a55 --- /dev/null +++ b/wled00/data/pixart/pixart.htm @@ -0,0 +1,210 @@ + + + + + + + WLED Pixel Art Converter + + + + + + +
+
+

+ + + + + + + WLED Pixel Art Converter +

+
+

Convert image to WLED JSON (pixel art on WLED matrix)

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + + 128 +
+ + + + 256 +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + + +
+
+ + + + + +
+
+ + + +  Scale image +
+
+ +
+

+ +

+

+ Drop image here
or
+ Click to select a file +
+ +

+ +

+ +

+ +
+ + + +

+ + + + +
+

Version 1.0.8
 -  Help/About

+
+
+ + +
+ + + + + + \ No newline at end of file diff --git a/wled00/data/pixart/pixart.js b/wled00/data/pixart/pixart.js new file mode 100644 index 0000000000..7c347f19a7 --- /dev/null +++ b/wled00/data/pixart/pixart.js @@ -0,0 +1,364 @@ +//Start up code +//if (window.location.protocol == "file:") { +// let locip = prompt("File Mode. Please enter WLED IP!"); +// gId('curlUrl').value = locip; +//} else +// +//Start up code +let devMode = false; //Remove +gurl.value = location.host; + +const urlParams = new URLSearchParams(window.location.search); +if (gurl.value.length < 1){ + gurl.value = "Missing_Host"; +} + +function gen(){ + //Generate image if enough info is in place + //Is host non empty + //Is image loaded + //is scale > 0 + if (((szX.value > 0 && szY.value > 0) || szDiv.style.display == 'none') && gurl.value.length > 0 && prw.style.display != 'none'){ + //regenerate + let base64Image = prw.src; + if (isValidBase64Gif(base64Image)) { + im.src = base64Image; + getPixelRGBValues(base64Image); + imcn.style.display = "block"; + bcn.style.display = ""; + } else { + let imageInfo = '

WARNING! File does not appear to be a valid image

'; + imin.innerHTML = imageInfo; + imin.style.display = "block"; + imcn.style.display = "none"; + JLD.value = ''; + if (devMode) console.log("The string '" + base64Image + "' is not a valid base64 image."); + } + } + + if(gurl.value.length > 0){ + gId("sSg").setAttribute("fill", accentColor); + } else{ + gId("sSg").setAttribute("fill", accentTextColor); + let ts = tSg; + ts.style.display = "none"; + ts.innerHTML = ""; + sID.style.display = "flex"; + } +} + + +// Code for copying the generated string to clipboard + +cjb.addEventListener('click', async () => { + let JSONled = JLD; + JSONled.select(); + try { + await navigator.clipboard.writeText(JSONled.value); + } catch (err) { + try { + await d.execCommand("copy"); + } catch (err) { + console.error('Failed to copy text: ', err); + } + } +}); + +// Event listeners ======================= + +lSS.addEventListener("change", gen); +szY.addEventListener("change", gen); +szX.addEventListener("change", gen); +cFS.addEventListener("change", gen); +aS.addEventListener("change", gen); +brgh.addEventListener("change", gen); +cLN.addEventListener("change", gen); +haIDe.addEventListener("change", gen); +haUe.addEventListener("change", gen); +haNe.addEventListener("change", gen); +gurl.addEventListener("change", gen); +sID.addEventListener("change", gen); +prw.addEventListener("load", gen); +//gId("convertbutton").addEventListener("click", gen); + +tSg.addEventListener("change", () => { + sop = tSg.options[tSg.selectedIndex]; + szX.value = sop.dataset.x; + szY.value = sop.dataset.y; + gen(); +}); + +gId("sendJSONledbutton").addEventListener('click', async () => { + if (window.location.protocol === "https:") { + alert('Will only be available when served over http (or WLED is run over https)'); + } else { + postPixels(); + } +}); + +brgh.oninput = () => { + brgV.textContent = brgh.value; + let perc = parseInt(brgh.value)*100/255; + var val = `linear-gradient(90deg, #bbb ${perc}%, #333 ${perc}%)`; + brgh.style.backgroundImage = val; +} + +cLN.oninput = () => { + let cln = cLN; + cLV.textContent = cln.value; + let perc = parseInt(cln.value)*100/512; + var val = `linear-gradient(90deg, #bbb ${perc}%, #333 ${perc}%)`; + cln.style.backgroundImage = val; +} + +frm.addEventListener("change", () => { + for (var i = 0; i < hideableRows.length; i++) { + hideableRows[i].classList.toggle("hide", frm.value !== "ha"); + gen(); + } +}); + +async function postPixels() { + let ss = gId("sendSvgP"); + ss.setAttribute("fill", prsCol); + let er = false; + for (let i of httpArray) { + try { + if (devMode) console.log(i); + if (devMode) console.log(i.length); + const response = await fetch('http://'+gId('curlUrl').value+'/json/state', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + //'Content-Type': 'text/html; charset=UTF-8' + }, + body: i + }); + const data = await response.json(); + if (devMode) console.log(data); + } catch (error) { + console.error(error); + er = true; + } + } + if(er){ + //Something went wrong + ss.setAttribute("fill", redColor); + setTimeout(function(){ + ss.setAttribute("fill", accentTextColor); + }, 1000); + } else { + // A, OK + ss.setAttribute("fill", greenColor); + setTimeout(function(){ + ss.setAttribute("fill", accentColor); + }, 1000); + } +} + +//File uploader code +const dropZone = gId('drop-zone'); +const filePicker = gId('file-picker'); +const preview = prw; + +// Listen for dragenter, dragover, and drop events +dropZone.addEventListener('dragenter', dragEnter); +dropZone.addEventListener('dragover', dragOver); +dropZone.addEventListener('drop', dropped); +dropZone.addEventListener('click', zoneClicked); + +// Listen for change event on file picker +filePicker.addEventListener('change', filePicked); + +// Handle zone click +function zoneClicked(e) { + e.preventDefault(); + //this.classList.add('drag-over'); + //alert('Hej'); + filePicker.click(); +} + +// Handle dragenter +function dragEnter(e) { + e.preventDefault(); + this.classList.add('drag-over'); +} + +// Handle dragover +function dragOver(e) { + e.preventDefault(); +} + +// Handle drop +function dropped(e) { + e.preventDefault(); + this.classList.remove('drag-over'); + + // Get the dropped file + const file = e.dataTransfer.files[0]; + updatePreview(file) +} + +// Handle file picked +function filePicked(e) { + // Get the picked file + const file = e.target.files[0]; + updatePreview(file) +} + +// Update the preview image +function updatePreview(file) { + // Use FileReader to read the file + const reader = new FileReader(); + reader.onload = () => { + // Update the preview image + preview.src = reader.result; + //gId("submitConvertDiv").style.display = ""; + prw.style.display = ""; + }; + reader.readAsDataURL(file); +} + +function isValidBase64Gif(string) { + // Use a regular expression to check that the string is a valid base64 string + /* + const base64gifPattern = /^data:image\/gif;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/; + const base64pngPattern = /^data:image\/png;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/; + const base64jpgPattern = /^data:image\/jpg;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/; + const base64webpPattern = /^data:image\/webp;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/; + */ + //REMOVED, Any image appear to work as long as it can be drawn to the canvas. Leaving code in for future use, possibly + if (1==1 || base64gifPattern.test(string) || base64pngPattern.test(string) || base64jpgPattern.test(string) || base64webpPattern.test(string)) { + return true; + } else { + //Not OK + return false; + } +} + +var hideableRows = d.querySelectorAll(".ha-hide"); +for (var i = 0; i < hideableRows.length; i++) { + hideableRows[i].classList.add("hide"); +} +frm.addEventListener("change", () => { + for (var i = 0; i < hideableRows.length; i++) { + hideableRows[i].classList.toggle("hide", frm.value !== "ha"); + } +}); + +function switchScale() { + //let scalePath = gId("scaleDiv").children[1].children[0] + let scaleTogglePath = scDiv.children[0].children[0] + let color = scaleTogglePath.getAttribute("fill"); + let d = ''; + if (color === accentColor) { + color = accentTextColor; + d = scaleToggleOffd; + szDiv.style.display = "none"; + // Set values to actual XY of image, if possible + } else { + color = accentColor; + d = scaleToggleOnd; + szDiv.style.display = ""; + } + //scalePath.setAttribute("fill", color); + scaleTogglePath.setAttribute("fill", color); + scaleTogglePath.setAttribute("d", d); + gen(); +} + +function generateSegmentOptions(array) { + //This function is prepared for a name property on each segment for easier selection + //Currently the name is generated generically based on index + tSg.innerHTML = ""; + for (var i = 0; i < array.length; i++) { + var option = cE("option"); + option.value = array[i].value; + option.text = array[i].text; + option.dataset.x = array[i].x; + option.dataset.y = array[i].y; + tSg.appendChild(option); + if(i === 0) { + option.selected = true; + szX.value = option.dataset.x; + szY.value = option.dataset.y; + } + } +} + +// Get segments from device +async function getSegments() { + cv = gurl.value; + if (cv.length > 0 ){ + try { + var arr = []; + const response = await fetch('http://'+cv+'/json/state'); + const json = await response.json(); + let ids = json.seg.map(sg => ({id: sg.id, n: sg.n, xs: sg.start, xe: sg.stop, ys: sg.startY, ye: sg.stopY})); + for (var i = 0; i < ids.length; i++) { + arr.push({ + value: ids[i]["id"], + text: ids[i]["n"] + ' (index: ' + ids[i]["id"] + ')', + x: ids[i]["xe"] - ids[i]["xs"], + y: ids[i]["ye"] - ids[i]["ys"] + }); + } + generateSegmentOptions(arr); + tSg.style.display = "flex"; + sID.style.display = "none"; + gId("sSg").setAttribute("fill", greenColor); + setTimeout(function(){ + gId("sSg").setAttribute("fill", accentColor); + }, 1000); + + } catch (error) { + console.error(error); + gId("sSg").setAttribute("fill", redColor); + setTimeout(function(){ + gId("sSg").setAttribute("fill", accentColor); + }, 1000); + tSg.style.display = "none"; + sID.style.display = "flex"; + } + } else{ + gId("sSg").setAttribute("fill", redColor); + setTimeout(function(){ + gId("sSg").setAttribute("fill", accentTextColor); + }, 1000); + tSg.style.display = "none"; + sID.style.display = "flex"; + } +} + +//Initial population of segment selection +function generateSegmentArray(noOfSegments) { + var arr = []; + for (var i = 0; i < noOfSegments; i++) { + arr.push({ + value: i, + text: "Segment index " + i + }); + } + return arr; +} + +var segmentData = generateSegmentArray(10); + +generateSegmentOptions(segmentData); + +seDiv.innerHTML = +'' +/*gId("convertbutton").innerHTML = +'   Convert to WLED JSON '; +*/ +cjb.innerHTML = +'   Copy to clipboard'; +gId("sendJSONledbutton").innerHTML = +'   Send to device'; + +//After everything is loaded, check if we have a possible IP/host + +if(gurl.value.length > 0){ + // Needs to be addressed directly here so the object actually exists + gId("sSg").setAttribute("fill", accentColor); +} diff --git a/wled00/data/pixart/site.webmanifest b/wled00/data/pixart/site.webmanifest new file mode 100644 index 0000000000..82452af2e0 --- /dev/null +++ b/wled00/data/pixart/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "WLED Pixel Art Convertor", + "short_name": "ledconv", + "icons": [ + { + "src": "/favicon-32x32.png", + "sizes": "32x322", + "type": "image/png" + }, + { + "src": "/favicon-32x32.png", + "sizes": "32x32", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" + } \ No newline at end of file diff --git a/wled00/data/pixart/statics.js b/wled00/data/pixart/statics.js new file mode 100644 index 0000000000..b4f3c40718 --- /dev/null +++ b/wled00/data/pixart/statics.js @@ -0,0 +1,51 @@ +//elements +var gurl = gId('curlUrl'); +var szX = gId("sizeX"); +var szY = gId("sizeY"); +var szDiv = gId("sizeDiv"); +var prw = gId("preview"); +var sID = gId('segID'); +var JLD = gId('JSONled'); +var tSg = gId('targetSegment'); +var brgh = gId("brightnessNumber"); + +var seDiv = gId("getSegmentsDiv") +var cjb = gId("copyJSONledbutton"); +var frm = gId("formatSelector"); +var cLN = gId("colorLimitNumber"); +var haIDe = gId("haID"); +var haUe = gId("haUID"); +var haNe = gId("haName"); +var aS = gId("addressingSelector"); +var cFS = gId("colorFormatSelector"); +var lSS = gId("ledSetupSelector"); +var imin = gId('image-info'); +var imcn = gId('image-container'); +var bcn = gId("button-container"); +var im = gId('image'); +//var ss = gId("sendSvgP"); +var scDiv = gId("scaleDiv"); +var w = window; +var canvas = gId('pixelCanvas'); +var brgV = gId("brightnessValue"); +var cLV = gId("colorLimitValue") + +//vars +var httpArray = []; +var fileJSON = ''; + +var hideableRows = d.querySelectorAll(".ha-hide"); +for (var i = 0; i < hideableRows.length; i++) { + hideableRows[i].classList.add("hide"); +} + +var accentColor = '#eee'; +var accentTextColor = '#777'; +var prsCol = '#ccc'; +var greenColor = '#056b0a'; +var redColor = '#6b050c'; + +var scaleToggleOffd = "M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z"; +var scaleToggleOnd = "M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"; + +var sSg = gId("getSegmentsSVGpath"); \ No newline at end of file diff --git a/wled00/data/pxmagic/pxmagic.htm b/wled00/data/pxmagic/pxmagic.htm new file mode 100644 index 0000000000..47092c3600 --- /dev/null +++ b/wled00/data/pxmagic/pxmagic.htm @@ -0,0 +1,1986 @@ + + + + + + + + Pixel Magic Tool + + + + +
+
+
+
+ Pixel Magic Tool +
+
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ +
+ +
+ +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+

+ Drag and drop a file here or click to select a local file +

+ +
+
+
+ +
+
+
+ +
+ +
+
+
+ + + diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm index c8e2c983d8..0e16010813 100644 --- a/wled00/data/settings.htm +++ b/wled00/data/settings.htm @@ -1,7 +1,56 @@ - + + + WLED Settings + - -
-
-
-
-
-
-
-
+ + + + + + + + + + + \ No newline at end of file diff --git a/wled00/data/settings_2D.htm b/wled00/data/settings_2D.htm new file mode 100644 index 0000000000..7d817181df --- /dev/null +++ b/wled00/data/settings_2D.htm @@ -0,0 +1,361 @@ + + + + + + 2D Set-up + + + + +
+
+
+
+
+

2D setup

+ Strip or panel: +
+ +
+ +
+
+ + diff --git a/wled00/data/settings_dmx.htm b/wled00/data/settings_dmx.htm index 2053fa15e5..3c8658fac8 100644 --- a/wled00/data/settings_dmx.htm +++ b/wled00/data/settings_dmx.htm @@ -1,44 +1,91 @@ -DMX Settings - - + + + + + DMX Settings + +
+

+

Imma firin ma lazer (if it has DMX support)

Proxy Universe from E1.31 to DMX (0=disabled)
diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 6b8d347f9e..5bddafe5e1 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -1,310 +1,368 @@ - - - + + LED Settings - + +

-

LED & Hardware setup

- Total LEDs: ?
- Recommended power supply for brightest white:
- ?
-
-
- Enable automatic brightness limiter:
-
- Maximum Current: mA
- - Automatically limits brightness to stay close to the limit.
- Keep at <1A if powering LEDs directly from the ESP 5V pin!
- If you are using an external power supply, enter its rating.
- (Current estimated usage: unknown)


- LED voltage (Max. current for a single LED):
-
- - Keep at default if you are unsure about your type of LEDs.
-
-

Hardware setup

-
LED outputs:
-
- -
- LED Memory Usage: 0 / ? B
-

- -
- Make a segment for each output:
- Custom bus start indices:
-
-
- Color Order Override: -
-
- -
-
-
-
- Touch threshold:
- IR GPIO:  ×
- Apply IR change to main segment only:
- - IR info
- Relay GPIO: Invert  ×
-
+
+

LED & Hardware setup

+ Total LEDs: ?
+ Recommended power supply for brightest white:
+ ?
+
+
+ Enable automatic brightness limiter:
+
+ Maximum Current: mA
+ + Automatically limits brightness to stay close to the limit.
+ Keep at <1A if powering LEDs directly from the ESP 5V pin!
+ If you are using an external power supply, enter its rating.
+ (Current estimated usage: unknown)


+ LED voltage (Max. current for a single LED):
+
+ + Keep at default if you are unsure about your type of LEDs.
+
+

Hardware setup

+
LED outputs:
+
+ +
+ LED Memory Usage: 0 / ? B
+

+ +
+ Make a segment for each output:
+ Custom bus start indices:
+ Use global LED buffer:
+
+
+ Color Order Override: +
+
+ +
+
+
+
+ Disable internal pull-up/down:
+ Touch threshold:
+ IR GPIO:  ✕
+ Apply IR change to main segment only:
+ + IR info
+ Relay GPIO: Invert  ✕
+

Defaults

Turn LEDs on after power up/reset:
- Default brightness: (0-255)

- Apply preset at boot (0 uses defaults) -

+ Default brightness: (0-255)

+ Apply preset at boot (0 uses defaults) +

Use Gamma correction for color: (strongly recommended)
- Use Gamma correction for brightness: (not recommended)

- Brightness factor: %% + Use Gamma correction for brightness: (not recommended)
+ Use Gamma value:

+ Brightness factor: %

Transitions

Crossfade:
- Transition Time: ms
- Enable Palette transitions: + Effect blending:
+ Transition Time: ms
+ Enable Palette transitions:
+ Random Cycle Palette Time: s

Timed light

- Default Duration: min
- Default Target brightness:
+ Default Duration: min
+ Default Target brightness:
Mode: -

White management

- White Balance correction:
- - Auto-calculate white channel from RGB:
- -
- Calculate CCT from RGB:
- CCT additive blending: %%
-

Advanced

+ White Balance correction:
+
+ Global override for Auto-calculate white:
+ +
+ Calculate CCT from RGB:
+ CCT additive blending: % +
+

Advanced

Palette blending:
Target refresh rate: FPS -
-
Config template:
-
- +
+
Config template:
+
+
-
+
diff --git a/wled00/data/settings_pin.htm b/wled00/data/settings_pin.htm new file mode 100644 index 0000000000..22f94e3790 --- /dev/null +++ b/wled00/data/settings_pin.htm @@ -0,0 +1,23 @@ + + + + + + PIN required + + + + +
+

Please enter settings PIN code

+ +
+ +
+ + \ No newline at end of file diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index 6fd8b55c8b..2cb4a264a8 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -1,65 +1,111 @@ - + Misc Settings - +
+
-
+
+

Security & Update setup

+ Settings PIN:
+
⚠ Unencrypted transmission. Be prudent when selecting PIN, do NOT use your banking, door, SIM, etc. pin!

Lock wireless (OTA) software update:
Passphrase:
To enable OTA, for security reasons you need to also enter the correct password!
@@ -69,28 +115,30 @@

Security & Update setup

Deny access to WiFi settings if locked:

Factory reset:
All settings and presets will be erased.

- HTTP traffic is unencrypted. An attacker in the same network can intercept form data! +
⚠ Unencrypted transmission. An attacker on the same network can intercept form data!
+

Software Update


- Enable ArduinoOTA:
-

Backup & Restore

- Backup presets
-
Restore presets


- Backup configuration
-
Restore configuration

-
⚠ Restoring presets/configuration will OVERWRITE your current presets/configuration.
+ Enable ArduinoOTA: +
+

Backup & Restore

+ Backup presets
+
Restore presets


+ Backup configuration
+
Restore configuration

+
⚠ Restoring presets/configuration will OVERWRITE your current presets/configuration.
Incorrect configuration may require a factory reset or re-flashing of your ESP.
- For security reasons, passwords are not backed up. + For security reasons, passwords are not backed up. +

About

WLED version ##VERSION##

Contributors, dependencies and special thanks
A huge thank you to everyone who helped me create WLED!

- (c) 2016-2022 Christian Schwinne
+ (c) 2016-2024 Christian Schwinne
Licensed under the MIT license

Server message: Response error!
- + - \ No newline at end of file diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index 1fe65b9c0f..a7ff45cfe1 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -1,101 +1,151 @@ -Sync Settings - - + var m=1; + for(j=0;j<8;j++) + { + a+=gId("G"+(j+1)).checked*m; + b+=gId("R"+(j+1)).checked*m; + m*=2; + } + gId("GS").value=a; + gId("GR").value=b; + } + function SP(){var p = d.Sf.DI.value; gId("xp").style.display = (p > 0)?"none":"block"; if (p > 0) d.Sf.EP.value = p;} + function SetVal(){switch(parseInt(d.Sf.EP.value)){case 5568: d.Sf.DI.value = 5568; break; case 6454: d.Sf.DI.value = 6454; break; case 4048: d.Sf.DI.value = 4048; break; }; SP();FC();} + function S(){ + let l = window.location; + if (l.protocol == "file:") { + loc = true; + locip = localStorage.getItem('locIp'); + if (!locip) { + locip = prompt("File Mode. Please enter WLED IP!"); + localStorage.setItem('locIp', locip); + } + } else { + // detect reverse proxy + let paths = l.pathname.slice(1,l.pathname.endsWith('/')?-1:undefined).split("/"); + if (paths.length > 2) { + locproto = l.protocol; + loc = true; + locip = l.hostname + (l.port ? ":" + l.port : "") + "/" + paths[0]; + } + } + loadJS(getURL('/settings/s.js?p=4'), false); // If we set async false, file is loaded and executed, then next statement is processed + if (loc) d.Sf.action = getURL('/settings/sync'); + } + function getURL(path) { + return (loc ? locproto + "//" + locip : "") + path; + } + + +
+

+

Sync setup

WLED Broadcast

UDP Port:
-2nd Port:

- +2nd Port:
+

Sync groups

+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sync groups12345678
Send:
Receive:
12345678
Send:
Receive:

-Receive: Brightness, Color, and Effects
+Receive: Brightness, Color, and Effects
Segment options, bounds
Send notifications on direct change:
Send notifications on button press or IR:
Send Alexa notifications:
Send Philips Hue change notifications:
Send Macro notifications:
-Send notifications twice:
+UDP packet retransmissions:

Reboot required to apply changes. +

Instance List

Enable instance list:
Make this instance discoverable: +

Realtime

-Receive UDP realtime:

+Receive UDP realtime:
+Use main segment only:

Network DMX input
Type:
Reboot required. Check out LedFx!
Skip out-of-sequence packets:
-DMX start address:
+DMX start address:
+DMX segment spacing:
+E1.31 port priority:
DMX mode:
E1.31 info
Timeout: ms
Force max brightness:
Disable realtime gamma correction:
Realtime LED offset: +

Alexa Voice Assistant

+
+ This firmware build does not include Alexa support.

+
+
Emulate Alexa device:
-Alexa invocation name: -

Blynk

-Blynk, MQTT and Hue sync all connect to external hosts!
-This may impact the responsiveness of the ESP8266.

+Alexa invocation name:
+Also emulate devices to call the first presets

+
+
+
MQTT and Hue sync all connect to external hosts!
+This may impact the responsiveness of WLED.

+
For best results, only use one of these services at a time.
-(alternatively, connect a second ESP to them and use the UDP sync)

-Host: -Port:
-Device Auth token:
-Clear the token field to disable. Setup info +(alternatively, connect a second ESP to them and use the UDP sync) +

MQTT

+
+ This firmware build does not include MQTT support.
+
+
Enable MQTT:
Broker: Port:
@@ -148,8 +213,14 @@

MQTT

Device Topic:
Group Topic:
Publish on button press:
+Retain brightness & color messages:
Reboot required to apply changes. MQTT info +

Philips Hue

+
+ This firmware build does not include Philips Hue support.
+
+
You can find the bridge IP and the light number in the 'About' section of the hue app.
Poll Hue light every ms:
Then, receive On/Off, Brightness, and Color
@@ -161,6 +232,7 @@

Philips Hue

Press the pushlink button on the bridge, after that save this page!
(when first connecting)
Hue status: Disabled in this build +

Serial

Baud rate:
-
+
Use 24h format:
Time zone:
UTC offset: seconds (max. 18 hours)
Current local time is unknown.
@@ -194,11 +214,11 @@

Clock

Countdown Mode:
Countdown Goal:
Date: 20--
- Time: ::
+ Time: ::

Macro presets

- Macros have moved!
- Presets now also can be used as macros to save both JSON and HTTP API commands.
- Just enter the preset ID below!
+ Macros have moved!
+ Presets now also can be used as macros to save both JSON and HTTP API commands.
+ Just enter the preset ID below!
Use 0 for the default action instead of a preset
Alexa On/Off Preset:
Countdown-Over Preset:
diff --git a/wled00/data/settings_ui.htm b/wled00/data/settings_ui.htm index 24083b4545..28aa589e49 100644 --- a/wled00/data/settings_ui.htm +++ b/wled00/data/settings_ui.htm @@ -1,222 +1,270 @@ - - + + - + UI Settings -
+


Web Setup

- Server description:
- Sync button toggles both send and receive:
+ Server description:
+ Sync button toggles both send and receive:
+
+ This firmware build does not include simplified UI support.
+
+
Enable simplified UI:
The following UI customization settings are unique both to the WLED device and this browser.
You will need to set them again if using a different browser, device or WLED IP address.
Refresh the main UI to apply changes.

@@ -225,22 +273,27 @@

Web Setup

UI Appearance

:
- :
- :
- :
+ :
+ :
+ :
+ :
+ :
+ :
+ :
I hate dark mode:
:
:
- :
- BG image URL:
+ :
+ BG image URL:
Random BG image:
- :
+ :
Custom CSS:
:
-
Holidays:
+
Holidays:
+

diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index b037c7a741..abcfef20d2 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -1,141 +1,318 @@ - - - - - Usermod Settings - - + + + + + Usermod Settings + + @@ -147,7 +324,19 @@

Usermod Setup

-
Loading settings...
+ Global I2C GPIOs (HW)
+ (change requires reboot!)
+ SDA: + SCL: +
+ Global SPI GPIOs (HW)
+ (only changable on ESP32, change requires reboot!)
+ MOSI: + MISO: + SCLK: +
+ Reboot after save?
+
Loading settings...

diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm index f9642d5186..bfc530352e 100644 --- a/wled00/data/settings_wifi.htm +++ b/wled00/data/settings_wifi.htm @@ -2,33 +2,161 @@ - + WiFi Settings - + - +
+

+

WiFi setup

Connect to existing network

- Network name (SSID, empty to not connect):

+
+ Network name (SSID, empty to not connect):
+
Network password:

Static IP (leave at 0.0.0.0 for DHCP):
. @@ -45,38 +173,55 @@

Connect to existing network

. .
- mDNS address (leave empty for no mDNS):
- http:// .local
+ mDNS address (leave empty for no mDNS):
+ http:// .local
Client IP: Not connected

Configure Access Point

- AP SSID (leave empty for no AP):

+ AP SSID (leave empty for no AP):

Hide AP name:
AP password (leave empty for open):

Access Point WiFi channel:
- AP opens: -
+ AP opens: +
AP IP: Not active

Experimental

+ Force 802.11g mode (ESP8266 only):
Disable WiFi sleep:
Can help with connectivity issues.
- Do not enable if WiFi is working correctly, increases power consumption.
-
-

Ethernet Type

-

+ Do not enable if WiFi is working correctly, increases power consumption.
+ +
+

Wireless Remote

+ Listen for events over ESP-NOW
+ Keep disabled if not using a remote, increases power consumption.
+ + Enable Remote:
+ Hardware MAC:
+ Last Seen: None
+
+ +
+

Ethernet Type

+

+

- \ No newline at end of file + diff --git a/wled00/data/simple.css b/wled00/data/simple.css new file mode 100644 index 0000000000..87eecee736 --- /dev/null +++ b/wled00/data/simple.css @@ -0,0 +1,933 @@ +@font-face { + font-family: "WIcons"; + src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAnUAAsAAAAAE1AAAAmFAAGZmgAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgXwRCAqcYJZIATYCJANwCzoABCAFgwYHIBs7D8iOwzgm3MXMnzZCktnjcbN+QlJLaJ3ulULplpW6UqWioeS91Jye0jUlJwZr5nTdE3LntdPvAg+ft/fbsLsGlNLuhlmQjKi7NPDEIgwTmP//a6mdl+SHUBhEIdHFxak7s4E/yzhJSjC7BQQLfDwopF/i6aqSElEFDXx8ZVWjy3rym4N6FlZQ4hu+nXsGIDMQF3gAxa14AgArtVMhfkgjfEAbiChwuSIwEUCmudPhiQdT6rvIjLSRZEwDhF9BIsooI53TIRIoIUD8kyNZI7UjAyMrR/aM/DwaOpozah9LGCsY2zN2YOzs2L3xqeNp4zXjq8bXT/hMBLj/53YDAIS+7u668n3H+HRPdZd1u3TzdRZdVMTfIl5HfKgd1b7Svqd9W9uprdP8QTOmeaz5TPORJlDDjHVjG0ANMQYsmRrKlmpyqV7kubIQC2GSIkFS+MneCJ48JJFVChQfuwKMp2yU9pmq1VKUR6ret0Gp0SjVYRRF+Xj7+OiUSk/GIzu1miHZWx+g8Y1RUktPmqIitRTXVNzzCtuFPKcH0zRBG+Y9/CnhBa20v5oHfsEUMgXMPEfO5ZcJx0FIPiVywgjb6MIuV+oZ4v2kk6/znIxDKrguM22y+bW8wUGqi7aL8fQJzwnCj8tIppdI9bYDSVJVCQInipW0HbtclcT7vCyLmXaSVrQSNMybaJJBh2PiXrXbgd6AbqecdDTO9EQEIeW0VPWQcdQ8ltPOEu+76q2IxUToJeWpfjQiHHH5AsADLj1bHgQxXsUoHfKYbg+CxCxC69eHcOvWheJ1l6b0nD7jG+bSA1dCZVxmw8ZJ/IYtxPtbJxlpQ/LGjSq00TmdNIZxrGel+y+rZJro+nUh3PrNIGwK6WrXNMV2xTeRWHSjScktLJfe1rc7spyvk3b6V4k48Sr3Am1Pv/QifhsI2uMvc863OiQQRNoedpPfHnSwcete+aDEE67cKzTgBlQgjpjgTDnJtGnX2qbmXJ6FOBLZ7wsr+JZzYnbjdbkCuEfU0HvlwqbtUgJ7zRXFNJsvSxlwz2WYta4xjri/fsulnnFVPyonpP0RL5oVNKkkfElG4csTDNAsgzC38G7gSKVgSZ7m/cEvKALmxKz//u7h6egHF7MrH4jJp/Zx4q32a8T71xnHVRCGlfFZNttd2FcUaay6e9PkhucyR0oPu1z1z/DB+8wixAFdMU1gnmB4xAw68pwHcWjlFrBnXxLjj63UGgvNGVGAJFzxFw+Womn7MAibVbu6leHRB5sc10fLtbrdr/JqV6Yr+ovwFtRHE7M4zG90qNB6YREoo51kFJabq3NeHVKdef/hsMFFSpt5m8XmJqDDAnR0c418mxmxrQzQuyPnspRwfAYkpthzr7gST1xNSf4WtBMM9DQT19uL+gb47gFLP3cT08F8I4dZxJl41Gsx9WHzLBOHzWjRS9NLCOUBCFQ+uGhB/V7ZzUwKESTmDriJ+UecdD/bFXFMLLsjgiAt4pp7ulpxb2tzE8I8xhyHODBK3SGg6QP12BiP3YMw2rDFtWUDXL+esnv3H9QxqfmbDnbMLjGUFpqqZbnWSg0lhWv9wU35qTHqP9zqUrL7kqKj8YjZzg01pb9+yQ8sXZpYxKGiFJTNsIwwpyR44gEOnV/+ennFdHD/2lQ3uS5y1qzIztXUNPE6odYJ0PqUiWJtgKGKMILY60dxeYynbb+sFKKqNn0Wz2rLtMbBQWPnYtmJa4WqFRob/9mmuycQVv7ifCNvXrlhzgDLDvAGA+8H5xjK948cDet+FaXfS+Lko/Wt+vScqarq6kZTbk4NaKqpObkEEpsac9L1rRNXJgPbrWyDdYje6tBQAztkbYC0wDe4UnNipmnZtInu/ujf6Kf7ve112Huf92Ev/7enB/+nP7pbrPiQJZbi0jCSpoN9UNPTkj7JMwpbWgopAbhtbOWkytAF3K+/qo0SASNW2G2bLfnshpB4a9dmz7/Hx//dc3OXNZ46YRyXUV2dYRsD97qKL79qazu+vSI1vPXT7375bWSGocBofD2eIRzJ0cMC0tenwQ0gfvuSdvd14f1uEooLPE3JJHL6uCd/n5n8d35UOKPn6nhr8kyrV3ad3nz2iTiNL414EnefL/JGLlWZtZWaqoEh4xSjvsGb/6m9raFlsLm4uHkQWlv7T/weZzjHHe7xZiUzpJ5WAWBLDNwRKxwRYnFoXGxcaKxN6DR8BNn2o9Nqmmutvra5TnIjXMBlmIFZ3yPYX3Mt9v5mmHuwYvvxPverL9eSvszXNjUXrkbqcGOVW2bEbDGKi3MLVTWzzWHF54Bu/2rA1qko6l9fFgVbBurfVBWFFlVW1ugxOwcs+8W//FcUZJieLl9WXA8eGL5crB7fhOMyxl8bjQWGjB1bW/ok6Ucqensr7F8H7utsmdqoHmz99rvyeE/Pz7u64mvVXLjyY8v8j5XhZeH3aPX75dpiO5eN/OzwcG7zkflt/sd5e7YcqbOowfRg22R5585at2vXX87W1Y0gQ079497eYT1EkyoEqMYABmHd8QvKGrRG6bJYTDCCZYGEWcm5G1jXM2i54Y9WtiBuklP57YtBZMAWlu2fYzDM7Q+5FmxKS3Oz5jwK6IactbWPowuQgNyHluKlaw9wnbOmtuajo/VSw9FrBSRwMcuUV2ZwFhh6s7hsqriWCsgA2s3nFcri4I7O+asxwxZbtLL03E9bhcR6Yz9mIbF0U96K0xGA7bx9y+l2//73j+H2i0EGd27uAVNI/WhCYuWqIDaYxads0lcVFV+dOlHmBx/qO7c6/uZX0tReUtJQv64y3adAvX6xDezAX/8Wm8Cgh/95O9OxsNCYnsXWQ+7pCz8/NMZ57ZAIGEdTw+ap8V+I3NUVe375wiv+lccqj172X7Yw5gJAUQGYPQ6QyxRfgeC+Qc5WnAMCAHFv6TJtet3pn/83b4YCAIBv35ofpTRyt5PjZEwT8KYAEQK8nFgBcE/yUwn2oqHSBKoEG7KZQLMpjo5uha/PI2yuBWOCTSDZajpqQ68+Za18jgGgYMT8nBhjKcFrKCYF6yKSZRLF5tR5YKhUzzNWM52mBvuPMiL7xPx4UaRgFiJZAVFscZ2HUIhUPcEaH5WWDvvmvdPfl5KaCvO8o1+fFCBb6hvuLz8lMROwfjPN8iar90RCCiRCJr3ugqHf6LqgUYYs5hzvu9tMIOUr/xpvRsNVvdZ/p+mB8n7V2Spo0T+aRhPpNhsNFOqxoE2u0suqTipgx58IJA0AAAA=) format('woff'); +} + +:root { + --c-1: #111; + --c-f: #fff; + --c-2: #222; + --c-3: #333; + --c-4: #444; + --c-5: #555; + --c-6: #666; + --c-8: #888; + --c-b: #bbb; + --c-c: #ccc; + --c-e: #eee; + --c-d: #ddd; + --c-r: #e42; + --c-g: #4e2; + --c-l: #48a; + --t-b: 0.5; + --c-o: rgba(34, 34, 34, 0.9); + --c-tb : rgba(34, 34, 34, var(--t-b)); + --c-tba: rgba(102, 102, 102, var(--t-b)); + --c-tbh: rgba(51, 51, 51, var(--t-b)); + /*following are internal*/ + --th: 70px; + --tp: 70px; + --bh: 63px; + --tbp: 14px 8px 10px; + --bbp: 9px 0 7px 0; + --bhd: none; + --bmt: 0px; +} + +html { + touch-action: manipulation; +} + +body { + margin: 0; + background-color: var(--c-1); + font-family: Helvetica, Verdana, sans-serif; + font-size: 17px; + color: var(--c-f); + text-align: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; + scrollbar-width: 6px; + scrollbar-color: var(--c-sb) transparent; +} + +html, +body { + height: 100%; + width: 100%; + position: fixed; + overscroll-behavior: none; +} + +#bg { + height: 100vh; + width: 100vw; + position: fixed; + z-index: -10; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + opacity: 0; + transition: opacity 2s; +} + +p { + margin: 10px 0 2px 0; +} +a, p, a:visited { + color: var(--c-d); +} +a, a:visited { + text-decoration: none; +} + +button { + outline: none; + cursor: pointer; + background-color: transparent; + border: none; + transition: color 0.3s, background-color 0.3s; + font-size: 19px; + color: var(--c-c); + min-width: 40px; + min-height: 40px; +} +button:hover { + background: var(--c-4); +} + +.label { + margin: 0; + padding: 6px 0 0; +} + +#namelabel { + position: fixed; + bottom: calc(var(--bh) + 6px); + right: 4px; + color: var(--c-6); + cursor: pointer; + writing-mode: vertical-rl; +} + +.wrapper { + position: fixed; + top: 0; + left: 0; + right: 0; + background: var(--c-tb); + z-index: 1; +} + +.center { + margin: 0 auto; + width: 320px; +} + +.icons { + font-family: 'WIcons'; + font-style: normal; + font-size: 24px; + line-height: 1; + display: inline-block; + margin: -2px 0 4px 0; + text-shadow: -1px -1px 0 var(--c-3), 1px -1px 0 var(--c-3), -1px 1px 0 var(--c-3), 1px 1px 0 var(--c-3); +} + +.huge { + font-size: 42px; +} + +.infot { + table-layout: fixed; + width: 100%; +} + +.keytd { + text-align: left; + padding-bottom: 8px; +} + +.valtd { + text-align: right; + padding-bottom: 8px; +} + +.valtd i { + font-size: small; +} + +.slider-icon +{ + transform: translate(4px,3px); + color: var(--c-d); +} + +.il { + display: inline-block; + vertical-align: middle; +} + +.tab { + background-color: transparent; + color: var(--c-d); +} + +.tab button { + background-color: transparent; + float: left; + border: none; + transition: color 0.3s, background-color 0.3s; + font-size: 17px; + color: var(--c-c); + min-width: 44px; +} + +.top button { + padding: var(--tbp); + margin: 0; +} + +.tab button:hover { + background-color: var(--c-tbh); + color: var(--c-e); +} + +.tab button.active { + background-color: var(--c-tba) !important; + color: var(--c-f); +} + +.active { + background-color: var(--c-6) !important; + color: var(--c-f); +} + +.container { + width: 100%; + height: calc(100% - var(--tp) - var(--bh)); + margin-top: var(--tp); + overscroll-behavior: none; +} + +.tabcontent { + position: relative; + width: 100%; + box-sizing: border-box; + border: 0px; + overflow: auto; + height: 100%; + overscroll-behavior: none; +} + +.smooth { transition: transform calc(var(--f, 1)*.5s) ease-out } + +.tab-label { + margin: 0 0 -5px 0; + padding-bottom: 4px; +} + +.overlay { + position: fixed; + height: 100%; + width: 100%; + top: 0; + left: 0; + background-color: var(--c-3); + font-size: 24px; + display: flex; + align-items: center; + justify-content: center; + z-index: 11; + opacity: 0.95; + transition: 0.7s; + pointer-events: none; +} + +#toast { + opacity: 0; + background-color: var(--c-5); + max-width: 90%; + color: var(--c-f); + text-align: center; + border-radius: 5px; + padding: 16px; + position: fixed; + z-index: 5; + left: 50%; + transform: translateX(-50%); + bottom: calc(var(--bh) + 22px); + font-size: 17px; + pointer-events: none; +} + +#toast.show { + opacity: 1; + animation: fadein 0.5s, fadein 0.5s 2.5s reverse; +} + +#toast.error { + opacity: 1; + background-color: #b21; + animation: fadein 0.5s; +} + +.modal { + position:fixed; + left: 0px; + bottom: 0px; + right: 0px; + top: calc(var(--th) - 1px); + background-color: var(--c-o); + transform: translateY(100%); + transition: transform 0.4s; + padding: 8px; + font-size: 20px; + overflow: auto; +} + +#info, #nodes { + z-index: 3; +} + +#rover { + z-index: 2; +} + +#ndlt { + margin: 12px 0; +} + +#roverstar { + position: fixed; + top: calc(var(--th) + 5px); + left: 1px; + display: none; + cursor: pointer; +} + +#connind { + position: fixed; + bottom: calc(var(--bh) + 5px); + left: 4px; + padding: 5px; + border-radius: 5px; + background-color: #a90; + z-index: -2; +} + +#imgw { + display: inline-block; + margin: 8px; +} + +#kv, #kn { + /*max-width: 490px;*/ + display: inline-block; +} + +#info table, #nodes table { + table-layout: fixed; + width: 100%; +} + +#info td, #nodes td { + padding-bottom: 8px; +} + +#info .btn { + margin: 5px; +} +#info table .btn, #nodes table .btn { + margin: 0; + width: 180px; +} +#info div, #nodes div { + width: 490px; + margin: 0 auto; +} + +#kn td { + padding-bottom: 12px; +} + +#heart { + transition: color 0.9s; + font-size: 16px; + color: #f00; +} + +img { + max-width: 100%; + max-height: 100%; +} + +.wi { + image-rendering: pixelated; + image-rendering: crisp-edges; + width: 210px; +} + +@keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: calc(var(--bh) + 22px); opacity: 1;} +} + +.sliderwrap { + height: 30px; + width: 250px; + position: relative; + margin: 4px 0; +} +#Colors .sliderwrap { + width: 260px; + margin: 10px 0 0; +} + +.sliderdisplay { + content:''; + position: absolute; + top: 10px; left: 8px; right: 8px; + height: 8px; + background: var(--c-4); + border-radius: 16px; + pointer-events: none; + z-index: -1; +} +#Colors .sliderdisplay { + height: 28px; + top: 0; bottom: 0; + left: 0; right: 0; + /*border: 1px solid var(--c-b);*/ +} +#rwrap .sliderdisplay { background: linear-gradient(90deg, #000 0%, #f00); } +#gwrap .sliderdisplay { background: linear-gradient(90deg, #000 0%, #0f0); } +#bwrap .sliderdisplay { background: linear-gradient(90deg, #000 0%, #00f); } +#wwrap .sliderdisplay { background: linear-gradient(90deg, #000 0%, #fff); } +#kwrap .sliderdisplay { background: linear-gradient(90deg, #ff8f1f 0%, #fff 50%, #cbdbff); } +#wbal .sliderdisplay { background: linear-gradient(90deg, #ff8f1f 0%, #fff 50%, #d4e0ff); } + +.sliderbubble { + width: 24px; + position: relative; + display: inline-block; + border-radius: 10px; + background: var(--c-3); + color: var(--c-f); + padding: 4px 4px 2px; + font-size: 14px; + right: 3px; + transition: visibility 0.25s ease, opacity 0.25s ease; + opacity: 0; + visibility: hidden; +} + +output.sliderbubbleshow { + visibility: visible; + opacity: 1; +} + +.hidden { + display: none; +} + +input[type=range] { + -webkit-appearance: none; + width: 100%; + padding: 0; + margin: 0; + background-color: transparent; + cursor: pointer; +} +#Colors input[type=range] { + width: 252px; + margin: 0; +} +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 30px; + cursor: pointer; + background: transparent; +} +input[type=range]::-webkit-slider-thumb { + border: 2px solid #000; + height: 20px; + width: 20px; + border-radius: 50%; + background: var(--c-f); + cursor: pointer; + -webkit-appearance: none; + margin-top: 4px; +} +input[type=range]::-moz-range-track { + width: 100%; + height: 30px; + background-color: var(--c-0); +} +input[type=range]::-moz-range-thumb { + border: 2px solid var(--c-3); + height: 20px; + width: 20px; + border-radius: 50%; + background: var(--c-f); + transform: translateY(5px); +} +#Colors input[type=range]::-webkit-slider-thumb { + border: 2px solid #000; +} +#Colors input[type=range]::-moz-range-thumb { + border: 2px solid var(--c-1); +} + +#Presets .list { + max-height: 215px; + overflow-y: scroll; + overflow-x: hidden; + width: 280px; + margin: 0 0 0 20px; + -ms-overflow-style: none; + scrollbar-width: none; /* Firefox */ +} +/* Hide scrollbar for Chrome, Safari and Opera */ +#Presets .list::-webkit-scrollbar { + display: none; +} + +#Segments .sliderwrap{ + width: 225px; +} + +#picker, #rgbwrap, #kwrap, #vwrap, #wwrap, #wbal { + display: none; +} + +.hd { + display: var(--bhd); +} + +#briwrap { + float: right; + margin-top: var(--bmt); +} + +#picker { + width: 260px; +} + +#picker, #csl, #segcont { + margin: 10px auto 0; +} + +.btn { + margin: 10px auto 0; + width: 280px; + font-size: 19px; + background-color: var(--c-3); + color: var(--c-d); + cursor: pointer; + border: 1px solid var(--c-3); + border-radius: 25px; + transition-duration: 0.3s; + -webkit-backface-visibility: hidden; + -webkit-transform:translate3d(0,0,0); + overflow: clip; + text-overflow: clip; + min-height: 40px; + line-height: 40px; +} +.btn:hover { + background-color: var(--c-4); + border: 1px solid var(--c-4); +} + +.btn-xs { + width: 42px; + height: 42px; + margin: 4px; + padding: 0; +} + +#fxBtn, #palBtn { + background-color: var(--c-2); + border: 1px solid var(--c-2); +} +#fxBtn:hover, #palBtn:hover { + background-color: var(--c-3); + border: 1px solid var(--c-3); +} + +.btn-icon { + margin-right: 8px; + vertical-align: middle; + display: inline-block; +} + +.qcs { + margin: 2px; + border-radius: 14px; + display: inline-block; + width: 28px; + height: 28px; + line-height: 28px;} +.qcsb { + width: 26px; + height: 26px; + line-height: 26px; + border: 1px solid var(--c-f); +} +option { + background-color: var(--c-3); + color: var(--c-f); +} +input[type=number], input[type=text] { + background: var(--c-3); + color: var(--c-f); + border: 0px solid var(--c-f); + border-radius: 5px; + padding: 8px; + margin: 6px 6px 6px 0; + font-size: 19px; + transition: background-color 0.2s; + outline: none; + width: 50px; + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; +} + +::selection { + background: var(--c-b); +} + +input[type=number]:focus, input[type=text]:focus { + background: var(--c-6); +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; +} + +.pid { + position: absolute; + top: 0px; + left: 0px; + padding: 12px 0px 0px 12px; + font-size: 16px; + width: 20px; + text-align: center; + color: var(--c-b); +} + +.xxs { + border: 2px solid var(--c-e) !important; + width: 44px; + height: 44px; + margin: 5px; + padding: 0; +} + +.xxs-w { + border-width: 4px !important; + margin: 2px; + width: 50px; + height: 50px; + padding: 0; +} + +.qcs, .xxs { + text-shadow: -1px -1px 0 var(--c-6), 1px -1px 0 var(--c-6), -1px 1px 0 var(--c-6), 1px 1px 0 var(--c-6); +} + +.psts { + color: var(--c-f); + margin: 6px; +} + +.pwr { + color: var(--c-6); + cursor: pointer; +} + +.act { + color: var(--c-f); +} + +.check, .radio { + display: inline-block; + position: relative; + cursor: pointer; + text-align: center; +} + +.schkl { + width: 24px; + top: -2px; +} + +.check input, .radio input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +.checkmark, .radiomark { + position: absolute; + top: 0; + bottom: 0; + left: 0; + background-color: var(--c-3); + border: 1px solid var(--c-2); +} + +.radiomark { + height: 24px; + width: 24px; + border-radius: 50%; +} + +.checkmark { + height: 25px; + width: 25px; + border-radius: 10px; +} + +.check:hover input ~ .checkmark { + background-color: var(--c-4); +} + +.check input:checked ~ .checkmark { + background-color: var(--c-6); +} + +.checkmark:after, .radiomark:after { + content: ""; + position: absolute; + display: none; +} + +.check input:checked ~ .checkmark:after, .radio input:checked ~ .radiomark:after { + display: block; +} + +.check .checkmark:after { + left: 9px; + top: 5px; + width: 5px; + height: 10px; + border: solid var(--c-f); + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +.radio .radiomark:after { + width: 12px; + height: 12px; + top: 50%; + left: 50%; + margin: -6px; + border-radius: 50%; + background: var(--c-f); +} + +.h { + font-size: 13px; + color: var(--c-b); +} + +.list { + position: relative; + width: 280px; + transition: background-color 0.5s; + margin: auto auto 20px; + font-size: 19px; + line-height: 24px; +} + +.lstI { + cursor: pointer; + background-color: var(--c-2); + overflow: hidden; + border-radius: 20px; + display: block; + position: relative; + border: 1px solid var(--c-2); + padding: 8px 10px; + margin: 10px 0; + min-height: 24px; +} + +.selected { /* has to be after .lstI */ + background: var(--c-5); +} + +.lstI:hover { + background: var(--c-4); +} +/* +.lstI:last-child { + border: none; + border-radius: 0 0 20px 20px; + padding-bottom: 10px; +} +*/ +.lstIcontent { + width: 100%; + vertical-align: middle; + padding: 0 20px 0 5px; + text-align: left; +} + +.lstIname { + white-space: nowrap; + cursor: pointer; +} + +.lstIprev { + width: 100%; + height: 8px; + position: absolute; + bottom: 0; + left: 0; + } + +/* Dropdown Content (Hidden by Default) */ +.dd-content { + display: none; + position: absolute; + width: 284px; + z-index: 1; + height: 260px; + overflow-y: scroll; + overflow-x: hidden; + padding: 0 18px; + margin-top: 10px; + -ms-overflow-style: none; + scrollbar-width: none; /* Firefox */ +} +/* Hide scrollbar for Chrome, Safari and Opera */ +.dd-content::-webkit-scrollbar { + display: none; +} + +.fnd { + position: sticky; + top: 0; + z-index: 1; + width: 280px; + margin: 0 auto; +} + +.search-icon { + position: absolute; + top: 10px; + left: 13px; + pointer-events: none; + width: 24px; + height: 24px; + margin-top: -1px; + z-index: 1; +} + +.clear-icon { + position: absolute; + display: none; + top: 10px; + right: 13px; + cursor: pointer; + margin-top: -1px; + z-index: 1; +} + +input[type=text].fnd { + display: block; + width: 100%; + box-sizing: border-box; + padding: 8px 48px 8px 48px; + margin: 5px auto 0; + text-align: left; + border-radius: 25px; + background-color: var(--c-2); + border: 1px solid var(--c-4); +} + +input[type=text].fnd:focus { + background-color: var(--c-4); +} + +input[type=text].fnd:not(:placeholder-shown), input[type=text].fnd:hover { + background-color: var(--c-3); +} + +.h, .c { + text-align: center; +} + +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--c-sb); + opacity: 0.2; + border-radius: 5px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--c-sbh); +} + +@media not all and (hover: none) { + .sliderwrap:hover + output.sliderbubble { + visibility: visible; + opacity: 1; + } +} + +@media all and (max-width: 335px) { + .sliderbubble { + display: none; + } +} + +@media all and (max-width: 550px) and (min-width: 374px) { + #info .btn, #nodes .btn { + width: 150px; + } + #info div, #nodes div { + width: 320px; + } +} + +@media all and (max-width: 540px) { + .top button { + width: 16.6%; + padding: 8px 0 4px 0; + } +} + +@media all and (min-width: 541px) and (max-width: 719px) { + .top button { + width: 14.2%; + padding: 8px 0 4px 0; + } +} + +@media all and (max-width: 719px) { + .hd { + display: none !important; + } + #briwrap { + margin-top: 0px !important; + float: none; + } +} diff --git a/wled00/data/simple.htm b/wled00/data/simple.htm new file mode 100644 index 0000000000..955bd65d81 --- /dev/null +++ b/wled00/data/simple.htm @@ -0,0 +1,263 @@ + + + + + + + + + WLED + + + + + +
Loading WLED UI...
+ +
+ +
+
+
+ + +
+ + + +
+
+

Global brightness

+
+ +
+ +
+
+ +
+
+
+
+ +
+
+
+

Quick Load

+
+
+ +
+

Solid color

+
+
+
+
+
+
+

+
+
+
+
+
R
+
+
+ +
+ +
+
+ +
+ +
+

+
+
+ +
+ +
+
+
+
+

RGB channels

+
+
+ +
+
+

+
+
+ +
+
+

+
+
+ +
+
+

+
+
+

White channel

+
+ +
+
+
+
+

White balance

+
+ +
+
+
+
+ +
+

Color slots

+ +
+ +
+
+
+ +
+

Presets

+
+ + + +
+
+
+ +
+

Effect

+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+
Solid
+
Default
+
+
+ + + +
+
+ +
+
+
+
+ + + +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ + + + + +
+ + + diff --git a/wled00/data/simple.js b/wled00/data/simple.js new file mode 100644 index 0000000000..8a65deb7c1 --- /dev/null +++ b/wled00/data/simple.js @@ -0,0 +1,1459 @@ +//page js +var loc = false, locip, locproto = "http:"; +var noNewSegs = false; +var isOn = false, isInfo = false, isNodes = false, isRgbw = false, cct = false; +var whites = [0,0,0]; +var selColors; +var powered = [true]; +var selectedFx = 0; +var selectedPal = 0; +var csel = 0; +var currentPreset = -1; +var lastUpdate = 0; +var segCount = 0, ledCount = 0, lowestUnused = 0, maxSeg = 0, lSeg = 0; +var tr = 7; +var d = document; +var palettesData; +var fxdata = []; +var pJson = {}, eJson = {}, lJson = {}; +var pN = "", pI = 0, pNum = 0; +var pmt = 1, pmtLS = 0, pmtLast = 0; +var lastinfo = {}; +var ws, cpick, ranges; +var cfg = { + theme:{base:"dark", bg:{url:""}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}}, + comp :{colors:{picker: true, rgb: false, quick: true, hex: false}, labels:true, pcmbot:false, pid:true, seglen:false} +}; +var hol = [ + [0,11,24,4,"https://aircoookie.github.io/xmas.png"], // christmas + [0,2,17,1,"https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day + [2022,3,17,2,"https://aircoookie.github.io/easter.png"], + [2023,3,9,2,"https://aircoookie.github.io/easter.png"], + [2024,2,31,2,"https://aircoookie.github.io/easter.png"] +]; + +function handleVisibilityChange() {if (!d.hidden && new Date () - lastUpdate > 3000) requestJson();} +function sCol(na, col) {d.documentElement.style.setProperty(na, col);} +function gId(c) {return d.getElementById(c);} +function gEBCN(c) {return d.getElementsByClassName(c);} +function isEmpty(o) {return Object.keys(o).length === 0;} +function isObj(i) { return (i && typeof i === 'object' && !Array.isArray(i)); } + +function applyCfg() +{ + cTheme(cfg.theme.base === "light"); + var bg = cfg.theme.color.bg; + if (bg) sCol('--c-1', bg); + var ccfg = cfg.comp.colors; + //gId('picker').style.display = "none"; // ccfg.picker ? "block":"none"; + //gId('vwrap').style.display = "none"; // ccfg.picker ? "block":"none"; + //gId('rgbwrap').style.display = ccfg.rgb ? "block":"none"; + gId('qcs-w').style.display = ccfg.quick ? "block":"none"; + var l = cfg.comp.labels; //l = false; + var e = d.querySelectorAll('.tab-label'); + for (var i=0; i { + var a = parseFloat(cfg.theme.alpha.bg); + if (isNaN(a)) a = 0.6; + bg.style.opacity = a; + bg.style.backgroundImage = `url(${img.src})`; + img = null; + }); +} + +function loadSkinCSS(cId) +{ + if (!gId(cId)) // check if element exists + { + var h = document.getElementsByTagName('head')[0]; + var l = document.createElement('link'); + l.id = cId; + l.rel = 'stylesheet'; + l.type = 'text/css'; + l.href = getURL('/skin.css'); + l.media = 'all'; + h.appendChild(l); + } +} + +function getURL(path) { + return (loc ? locproto + "//" + locip : "") + path; +} +async function onLoad() +{ + let l = window.location; + if (l.protocol == "file:") { + loc = true; + locip = localStorage.getItem('locIp'); + if (!locip) { + locip = prompt("File Mode. Please enter WLED IP!"); + localStorage.setItem('locIp', locip); + } + } else { + // detect reverse proxy and/or HTTPS + let pathn = l.pathname; + let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/"); + if (paths[0]==="sliders") paths.shift(); + //while (paths[0]==="") paths.shift(); + locproto = l.protocol; + locip = l.hostname + (l.port ? ":" + l.port : ""); + if (paths.length > 0 && paths[0]!=="") { + loc = true; + locip += "/" + paths[0]; + } else if (locproto==="https:") { + loc = true; + } + } + var sett = localStorage.getItem('wledUiCfg'); + if (sett) cfg = mergeDeep(cfg, JSON.parse(sett)); + + makeWS(); + + applyCfg(); + if (cfg.theme.bg.url=="" || cfg.theme.bg.url === "https://picsum.photos/1920/1080") { + var iUrl = cfg.theme.bg.url; + fetch(getURL("/holidays.json"), { + method: 'get' + }) + .then((res)=>{ + return res.json(); + }) + .then((json)=>{ + if (Array.isArray(json)) hol = json; + //TODO: do some parsing first + }) + .catch((e)=>{ + console.log("holidays.json does not contain array of holidays. Defaults loaded."); + }) + .finally(()=>{ + var today = new Date(); + for (var i=0; i=hs && today{ + setColor(1); + }); + pmtLS = localStorage.getItem('wledPmt'); + + // Load initial data + loadPalettes(()=>{ + loadPalettesData(redrawPalPrev); + loadFX(()=>{ + loadFXData(); + loadPresets(()=>{ + requestJson(); + }); + }); + }); + + d.addEventListener("visibilitychange", handleVisibilityChange, false); + size(); + gId("cv").style.opacity=0; + var sls = d.querySelectorAll('input[type="range"]'); + for (var sl of sls) { + sl.addEventListener('touchstart', toggleBubble); + sl.addEventListener('touchend', toggleBubble); + } +} + +var timeout; +function showToast(text, error = false) +{ + if (error) gId('connind').style.backgroundColor = "var(--c-r)"; + var x = gId("toast"); + x.innerHTML = text; + x.className = error ? "error":"show"; + clearTimeout(timeout); + x.style.animation = 'none'; + timeout = setTimeout(()=>{ x.classList.remove("show"); }, 2900); + if (error) console.log(text); +} + +function showErrorToast() +{ + if (ws && ws.readyState === WebSocket.OPEN) { + // if we received a timeout force WS reconnect + ws.close(); + ws = null; + if (lastinfo.ws > -1) setTimeout(makeWS,500); + } + showToast('Connection to light failed!', true); +} + +function clearErrorToast() {gId("toast").className = gId("toast").className.replace("error", "");} + +function getRuntimeStr(rt) +{ + var t = parseInt(rt); + var days = Math.floor(t/86400); + var hrs = Math.floor((t - days*86400)/3600); + var mins = Math.floor((t - days*86400 - hrs*3600)/60); + var str = days ? (days + " " + (days == 1 ? "day" : "days") + ", ") : ""; + str += (hrs || days) ? (hrs + " " + (hrs == 1 ? "hour" : "hours")) : ""; + if (!days && hrs) str += ", "; + if (t > 59 && !days) str += mins + " min"; + if (t < 3600 && t > 59) str += ", "; + if (t < 3600) str += (t - mins*60) + " sec"; + return str; +} + +function inforow(key, val, unit = "") +{ + return `${key}${val}${unit}`; +} + +function pName(i) +{ + var n = "Preset " + i; + if (pJson && pJson[i] && pJson[i].n) n = pJson[i].n; + return n; +} + +function isPlaylist(i) +{ + return pJson[i].playlist && pJson[i].playlist.ps; +} + +function papiVal(i) +{ + if (!pJson || !pJson[i]) return ""; + var o = Object.assign({},pJson[i]); + if (o.win) return o.win; + delete o.n; delete o.p; delete o.ql; + return JSON.stringify(o); +} + +function qlName(i) +{ + if (!pJson || !pJson[i] || !pJson[i].ql) return ""; + return pJson[i].ql; +} + +function cpBck() +{ + var copyText = gId("bck"); + + copyText.select(); + copyText.setSelectionRange(0, 999999); + d.execCommand("copy"); + showToast("Copied to clipboard!"); +} + +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 + pJson = JSON.parse(localStorage.getItem("wledP")); + populatePresets(); + pmtLast = pmt; + callback(); + return; + } + + //afterwards + if (!callback && pmt == pmtLast) return; + + pmtLast = pmt; + + fetch(getURL('/presets.json'), { + method: 'get' + }) + .then(res => { + if (!res.ok) showErrorToast(); + return res.json(); + }) + .then(json => { + clearErrorToast(); + pJson = json; + populatePresets(); + }) + .catch(function (error) { + showToast(error, true); + console.log(error); + }) + .finally(()=>{ + if (callback) setTimeout(callback,99); + }); +} + +function loadPalettes(callback = null) +{ + fetch(getURL('/json/palettes'), { + method: 'get' + }) + .then(res => { + if (!res.ok) showErrorToast(); + return res.json(); + }) + .then(json => { + clearErrorToast(); + lJson = Object.entries(json); + populatePalettes(); + }) + .catch(function (error) { + showToast(error, true); + }) + .finally(()=>{ + if (callback) callback(); + }); +} + +function loadFX(callback = null) +{ + fetch(getURL('/json/effects'), { + method: 'get' + }) + .then(res => { + if (!res.ok) showErrorToast(); + return res.json(); + }) + .then(json => { + clearErrorToast(); + eJson = Object.entries(json); + populateEffects(); + }) + .catch(function (error) { + showToast(error, true); + }) + .finally(()=>{ + if (callback) callback(); + }); +} + +function loadFXData(callback = null) +{ + fetch(getURL('/json/fxdata'), { + method: 'get' + }) + .then(res => { + if (!res.ok) showErrorToast(); + return res.json(); + }) + .then(json => { + clearErrorToast(); + fxdata = json||[]; + // add default value for Solid + fxdata.shift() + fxdata.unshift("@;!;"); + }) + .catch(function (error) { + fxdata = []; + showToast(error, true); + }) + .finally(()=>{ + if (callback) callback(); + updateUI(); + }); +} + +var pQL = []; +function populateQL() +{ + var cn = ""; + if (pQL.length > 0) { + pQL.sort((a,b) => (a[0]>b[0])); + for (var key of (pQL||[])) { + cn += ``; + } + } + gId('pql').innerHTML = cn; +} + +function populatePresets() +{ + if (!pJson) {pJson={};return}; + delete pJson["0"]; + var cn = ""; //`

All presets

`; + var arr = Object.entries(pJson); + arr.sort(cmpP); + pQL = []; + var is = []; + pNum = 0; + for (var key of (arr||[])) + { + if (!isObj(key[1])) continue; + let i = parseInt(key[0]); + var qll = key[1].ql; + if (qll) pQL.push([i, qll, pName(i)]); + is.push(i); + + cn += `
`; + //if (cfg.comp.pid) cn += `
${i}
`; + cn += `${isPlaylist(i)?"":""}${pName(i)}
`; + pNum++; + } + gId('pcont').innerHTML = cn; + updatePA(); + populateQL(); +} + +function parseInfo() { + var li = lastinfo; + var name = li.name; + gId('namelabel').innerHTML = name; +// if (name === "Dinnerbone") d.documentElement.style.transform = "rotate(180deg)"; + if (li.live) name = "(Live) " + name; + if (loc) name = "(L) " + name; + d.title = name; + isRgbw = li.leds.wv; + ledCount = li.leds.count; + syncTglRecv = li.str; + maxSeg = li.leds.maxseg; + pmt = li.fs.pmt; + cct = li.leds.cct; +} + +function populateInfo(i) +{ + var cn=""; + var heap = i.freeheap/1000; + heap = heap.toFixed(1); + var pwr = i.leds.pwr; + var pwru = "Not calculated"; + if (pwr > 1000) {pwr /= 1000; pwr = pwr.toFixed((pwr > 10) ? 0 : 1); pwru = pwr + " A";} + else if (pwr > 0) {pwr = 50 * Math.round(pwr/50); pwru = pwr + " mA";} + var urows=""; + if (i.u) { + for (const [k, val] of Object.entries(i.u)) { + if (val[1]) + urows += inforow(k,val[0],val[1]); + else + urows += inforow(k,val); + } + } + var vcn = "Kuuhaku"; + if (i.ver.startsWith("0.14.")) vcn = "Hoshi"; + if (i.ver.includes("-bl")) vcn = "Supāku"; + if (i.cn) vcn = i.cn; + + cn += `v${i.ver} "${vcn}"

+${urows} +${inforow("Build",i.vid)} +${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} +${inforow("Uptime",getRuntimeStr(i.uptime))} +${inforow("Time",i.time)} +${inforow("Free heap",heap," kB")} +${i.psram?inforow("Free PSRAM",(i.psram/1024).toFixed(1)," kB"):""} +${inforow("Estimated current",pwru)} +${inforow("Average FPS",i.leds.fps)} +${inforow("MAC address",i.mac)} +${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")} +${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} +
`; + gId('kv').innerHTML = cn; +} + +function populateSegments(s) +{ + var cn = ""; + segCount = (s.seg||[]).length; + lowestUnused = 0; lSeg = 0; + + if (segCount > 1) { + for (var y = 0; y < segCount && y<4; y++) + { + var inst=s.seg[y]; + let i = parseInt(inst.id); + powered[i] = inst.on; + if (i == lowestUnused) lowestUnused = i+1; + if (i > lSeg) lSeg = i; + + cn += +`
${(inst.n&&inst.n!=='')?inst.n:('Segment '+y)}
+
+ +
+ +
+ +
+
+ +
+
`; + } + //if (gId('buttonBri').className !== 'active') tglBri(true); + } else { + //tglBri(false); + } + //gId('buttonBri').style.display = (segCount > 1) ? "block" : "none"; + gId('segcont').innerHTML = cn; + for (var i = 0; i < segCount && i<4; i++) updateTrail(gId(`seg${i}bri`)); +} + +function btype(b) +{ + switch (b) { + case 2: + case 32: return "ESP32"; + case 1: + case 82: return "ESP8266"; + } + return "?"; +} + +function bname(o) +{ + if (o.name=="WLED") return o.ip; + return o.name; +} + +function populateNodes(i,n) +{ + var cn=""; + var urows=""; + var nnodes = 0; + if (n.nodes) { + n.nodes.sort((a,b) => (a.name).localeCompare(b.name)); + for (var x=0;x${bname(o)}`; + urows += inforow(url,`${btype(o.type)}
${o.vid==0?"N/A":o.vid}`); + nnodes++; + } + } + } + if (i.ndc < 0) cn += `Instance List is disabled.`; + else if (nnodes == 0) cn += `No other instances found.`; + cn += ` + ${urows} + ${inforow("Current instance:",i.name)} +
`; + gId('kn').innerHTML = cn; +} + +function loadNodes() +{ + fetch(getURL('/json/nodes'), { + method: 'get' + }) + .then(res => { + if (!res.ok) showToast('Could not load Node list!', true); + return res.json(); + }) + .then(json => { + clearErrorToast(); + populateNodes(lastinfo, json); + }) + .catch(function (error) { + showToast(error, true); + console.log(error); + }); +} + +function populateEffects() +{ + var effects = eJson; + var html = ""; + + effects.shift(); //remove solid + for (let i = 0; i < effects.length; i++) effects[i] = {id: effects[i][0], name:effects[i][1]}; + effects.sort((a,b) => (a.name).localeCompare(b.name)); + effects.unshift({ + "id": 0, + "name": "Solid@;!;0" + }); + + for (let i = 0; i < effects.length; i++) { + // WLEDSR: add slider and color control to setEffect (used by requestjson) + if (effects[i].name.indexOf("RSVD") < 0) { + var posAt = effects[i].name.indexOf("@"); + var extra = ''; + if (posAt > 0) + extra = effects[i].name.substr(posAt); + else + posAt = 999; + html += generateListItemHtml( + 'fx', + effects[i].id, + effects[i].name.substr(0,posAt), + 'setEffect', + '','', + extra + ); + } + } + gId('fxlist').innerHTML=html; +} + +function populatePalettes() +{ + var palettes = lJson; + palettes.shift(); //remove default + for (let i = 0; i < palettes.length; i++) { + palettes[i] = { + "id": palettes[i][0], + "name": palettes[i][1] + }; + } + palettes.sort((a,b) => (a.name).localeCompare(b.name)); + palettes.unshift({ + "id": 0, + "name": "Default", + }); + var html = ""; + for (let i = 0; i < palettes.length; i++) { + html += generateListItemHtml( + 'palette', + palettes[i].id, + palettes[i].name, + 'setPalette', + `
` + ); + } + gId('pallist').innerHTML=html; +} + +function redrawPalPrev() +{ + let palettes = d.querySelectorAll('#pallist .lstI'); + for (let i = 0; i < palettes.length; i++) { + let id = palettes[i].dataset.id; + let lstPrev = palettes[i].querySelector('.lstIprev'); + if (lstPrev) { + lstPrev.style = genPalPrevCss(id); + } + } +} + +function genPalPrevCss(id) +{ + if (!palettesData) return; + + var paletteData = palettesData[id]; + var previewCss = ""; + + if (!paletteData) return 'display: none'; + + // We need at least two colors for a gradient + if (paletteData.length == 1) { + paletteData[1] = paletteData[0]; + if (Array.isArray(paletteData[1])) { + paletteData[1][0] = 255; + } + } + + var gradient = []; + for (let j = 0; j < paletteData.length; j++) { + const element = paletteData[j]; + let r; + let g; + let b; + let index = false; + if (Array.isArray(element)) { + index = element[0]/255*100; + r = element[1]; + g = element[2]; + b = element[3]; + } else if (element == 'r') { + r = Math.random() * 255; + g = Math.random() * 255; + b = Math.random() * 255; + } else { + if (selColors) { + let e = element[1] - 1; + r = selColors[e][0]; + g = selColors[e][1]; + b = selColors[e][2]; + } + } + if (index === false) { + index = j / paletteData.length * 100; + } + + gradient.push(`rgb(${r},${g},${b}) ${index}%`); + } + + return `background: linear-gradient(to right,${gradient.join()});`; +} + +function generateOptionItemHtml(id, name) +{ + return ``; +} + +function generateListItemHtml(listName, id, name, clickAction, extraHtml = '', extraClass = '', extraPar = '') +{ + return `
+
+ + ${name} + +
+ ${extraHtml} +
`; +} + +//update the 'sliderdisplay' background div of a slider for a visual indication of slider position +function updateTrail(e) +{ + if (e==null) return; + var max = e.hasAttribute('max') ? e.attributes.max.value : 255; + var perc = e.value * 100 / max; + perc = parseInt(perc); + if (perc < 50) perc += 2; + var val = `linear-gradient(90deg, var(--c-f) ${perc}%, var(--c-4) ${perc}%)`; + e.parentNode.getElementsByClassName('sliderdisplay')[0].style.background = val; + var b = e.parentNode.parentNode.getElementsByTagName('output')[0]; + if (b) b.innerHTML = e.value; +} + +//rangetouch slider function +function toggleBubble(e) +{ + var b = e.target.parentNode.parentNode.getElementsByTagName('output')[0]; + b.classList.toggle('sliderbubbleshow'); +} + +function updatePA() +{ + var ps = gEBCN("pres"); + for (let i = 0; i < ps.length; i++) { + ps[i].classList.remove('selected');; + } + ps = gEBCN("psts"); + for (let i = 0; i < ps.length; i++) { + ps[i].classList.remove('selected');; + } + if (currentPreset > 0) { + var acv = gId(`p${currentPreset}o`); + if (acv) acv.classList.add('selected'); + acv = gId(`p${currentPreset}qlb`); + if (acv) acv.classList.add('selected'); + } +} + +function updateUI() +{ + gId('buttonPower').className = (isOn) ? "active":""; + + var sel = 0; + if (lJson && lJson.length) { + for (var i=0; i b[0]); + // playlists follow presets + var name = (a[1].playlist ? '~' : ' ') + a[1].n; + return name.localeCompare((b[1].playlist ? '~' : ' ') + b[1].n, undefined, {numeric: true}); +} + +function makeWS() { + if (ws) return; + let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws"; + ws = new WebSocket(url); + ws.onmessage = (e)=>{ + var json = JSON.parse(e.data); + if (json.leds) return; //liveview packet + clearTimeout(jsonTimeout); + jsonTimeout = null; + lastUpdate = new Date(); + clearErrorToast(); + gId('connind').style.backgroundColor = "var(--c-l)"; + // json object should contain json.info AND json.state (but may not) + var i = json.info; + if (i) { + lastinfo = i; + parseInfo(); + if (isInfo) populateInfo(i); + } else + i = lastinfo; + var s = json.state ? json.state : json; + readState(s); + }; + ws.onclose = (e)=>{ + gId('connind').style.backgroundColor = "var(--c-r)"; + ws = null; + if (lastinfo.ws > -1) setTimeout(makeWS,500); + } + ws.onopen = (e)=>{ + ws.send("{'v':true}"); + reqsLegal = true; + clearErrorToast(); + } +} + +function readState(s,command=false) +{ + if (!s) return false; + + isOn = s.on; + gId('sliderBri').value= s.bri; + nlA = s.nl.on; + nlDur = s.nl.dur; + nlTar = s.nl.tbri; + nlFade = s.nl.fade; + syncSend = s.udpn.send; + if (s.pl<0) currentPreset = s.ps; + else currentPreset = s.pl; + tr = s.transition/10; + + var selc=0; var ind=0; + populateSegments(s); + for (let i = 0; i < (s.seg||[]).length; i++) + { + if(s.seg[i].sel) {selc = ind; break;} ind++; + } + var i=s.seg[selc]; + if (!i) { + showToast('No Segments!', true); + updateUI(); + return; + } + + selColors = i.col; + var cd = gId('csl').children; + for (let e = cd.length-1; e >= 0; e--) + { + var r,g,b,w; + r = i.col[e][0]; + g = i.col[e][1]; + b = i.col[e][2]; + if (isRgbw) w = i.col[e][3]; + cd[e].style.backgroundColor = "rgb(" + r + "," + g + "," + b + ")"; + if (isRgbw) whites[e] = parseInt(w); + selectSlot(csel); + } + gId('sliderW').value = whites[csel]; + if (i.cct && i.cct>=0) gId("sliderA").value = i.cct; + + gId('sliderSpeed').value = i.sx; + gId('sliderIntensity').value = i.ix; +/* + gId('sliderC1').value = i.f1x ? i.f1x : 0; + gId('sliderC2').value = i.f2x ? i.f2x : 0; + gId('sliderC3').value = i.f3x ? i.f3x : 0; +*/ + if (s.error && s.error != 0) { + var errstr = ""; + switch (s.error) { + case 10: + errstr = "Could not mount filesystem!"; + break; + case 11: + errstr = "Not enough space to save preset!"; + break; + case 12: + errstr = "Preset not found."; + break; + case 13: + errstr = "Missing IR.json."; + break; + case 19: + errstr = "A filesystem error has occurred."; + break; + } + showToast('Error ' + s.error + ": " + errstr, true); + } + + selectedPal = i.pal; + selectedFx = i.fx; + updateUI(); +} + +var jsonTimeout; +var reqsLegal = false; + +function requestJson(command=null) +{ + gId('connind').style.backgroundColor = "var(--c-r)"; + if (command && !reqsLegal) return; //stop post requests from chrome onchange event on page restore + if (!jsonTimeout) jsonTimeout = setTimeout(showErrorToast, 3000); + var req = null; + var useWs = (ws && ws.readyState === WebSocket.OPEN); + var type = command ? 'post':'get'; + if (command) { + if (useWs || !command.ps) command.v = true; // force complete /json/si API response + command.time = Math.floor(Date.now() / 1000); + req = JSON.stringify(command); + if (req.length > 1000) useWs = false; //do not send very long requests over websocket + }; + + if (useWs) { + ws.send(req?req:'{"v":true}'); + return; + } else if (command && command.ps) { //refresh UI if we don't use WS (async loading of presets) + setTimeout(requestJson,200); + } + + fetch(getURL('/json/si'), { + method: type, + headers: { + "Content-type": "application/json; charset=UTF-8" + }, + body: req + }) + .then(res => { + if (!res.ok) showErrorToast(); + return res.json(); + }) + .then(json => { + clearTimeout(jsonTimeout); + jsonTimeout = null; + lastUpdate = new Date(); + clearErrorToast(); + gId('connind').style.backgroundColor = "var(--c-g)"; + if (!json) { showToast('Empty response', true); return; } + if (json.success) return; + if (json.info) { + lastinfo = json.info; + parseInfo(); + if (isInfo) populateInfo(lastinfo); + } + var s = json.state ? json.state : json; + readState(s); + reqsLegal = true; + }) + .catch(function (error) { + showToast(error, true); + console.log(error); + }); +} + +function togglePower() +{ + isOn = !isOn; + var obj = {"on": isOn}; + requestJson(obj); +} + +function toggleInfo() +{ + if (isNodes) toggleNodes(); + isInfo = !isInfo; + if (isInfo) requestJson(); + gId('info').style.transform = (isInfo) ? "translateY(0px)":"translateY(100%)"; + gId('buttonI').className = (isInfo) ? "active":""; +} + +function toggleNodes() +{ + if (isInfo) toggleInfo(); + isNodes = !isNodes; + if (isNodes) loadNodes(); + gId('nodes').style.transform = (isNodes) ? "translateY(0px)":"translateY(100%)"; + gId('buttonNodes').className = (isNodes) ? "active":""; +} +/* +function tglBri(b=null) +{ + if (b===null) b = gId(`briwrap`).style.display === "block"; + gId('briwrap').style.display = !b ? "block":"none"; + gId('buttonBri').className = !b ? "active":""; + size(); +} +*/ +function tglCP() +{ + var p = gId('buttonCP').className === "active"; + gId('buttonCP').className = !p ? "active":""; + gId('picker').style.display = !p ? "block":"none"; + gId('vwrap').style.display = !p ? "block":"none"; + gId('rgbwrap').style.display = !p ? "block":"none"; + var csl = gId('Slots').style.display === "block"; + gId('Slots').style.display = !csl ? "block":"none"; + //var ps = gId(`Presets`).style.display === "block"; + //gId('Presets').style.display = !ps ? "block":"none"; +} + +function tglCs(i) +{ + var pss = gId(`p${i}cstgl`).checked; + gId(`p${i}o1`).style.display = pss? "block" : "none"; + gId(`p${i}o2`).style.display = !pss? "block" : "none"; +} + +function selSeg(s) +{ + var sel = gId(`seg${s}sel`).checked; + var obj = {"seg": {"id": s, "sel": sel}}; + requestJson(obj); +} + +function tglPalDropdown() +{ + var p = gId('palDropdown').style; + p.display = (p.display==='block'?'none':'block'); + gId('fxDropdown').style.display = 'none'; + if (p.display==='block') + gId('palDropdown').scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); +} + +function tglFxDropdown() +{ + var p = gId('fxDropdown').style; + p.display = (p.display==='block'?'none':'block'); + gId('palDropdown').style.display = 'none'; + if (p.display==='block') + gId('fxDropdown').scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); +} + +function setSegPwr(s) +{ + var obj = {"seg": {"id": s, "on": !powered[s]}}; + requestJson(obj); +} + +function setSegBri(s) +{ + var obj = {"seg": {"id": s, "bri": parseInt(gId(`seg${s}bri`).value)}}; + requestJson(obj); +} + +function setEffect(ind = 0) +{ + tglFxDropdown(); + var obj = {"seg": {"fx": parseInt(ind), "fxdef":true}}; // fxdef sets effect parameters to default values, TODO add client setting + requestJson(obj); +} + +function setPalette(paletteId = null) +{ + tglPalDropdown(); + var obj = {"seg": {"pal": paletteId}}; + requestJson(obj); +} + +function setBri() +{ + var obj = {"bri": parseInt(gId('sliderBri').value)}; + requestJson(obj); +} + +function setSpeed() +{ + var obj = {"seg": {"sx": parseInt(gId('sliderSpeed').value)}}; + requestJson(obj); +} + +function setIntensity() +{ + var obj = {"seg": {"ix": parseInt(gId('sliderIntensity').value)}}; + requestJson(obj); +} + +function setLor(i) +{ + var obj = {"lor": i}; + requestJson(obj); +} + +function setPreset(i) +{ + var obj = {"ps": i}; + if (isPlaylist(i)) obj.on = true; + showToast("Loading preset " + pName(i) +" (" + i + ")"); + requestJson(obj); +} + +function selectSlot(b) +{ + csel = b; + var cd = gId('csl').children; + for (let i = 0; i < cd.length; i++) cd[i].classList.remove('xxs-w'); + cd[b].classList.add('xxs-w'); + setPicker(cd[b].style.backgroundColor); + gId('sliderW').value = whites[b]; + redrawPalPrev(); + updatePSliders(); +} + +var lasth = 0; +function pC(col) +{ + if (col == "rnd") { + col = {h: 0, s: 0, v: 100}; + col.s = Math.floor((Math.random() * 50) + 50); + do { + col.h = Math.floor(Math.random() * 360); + } while (Math.abs(col.h - lasth) < 50); + lasth = col.h; + } + setPicker(col); + setColor(0); +} + +function updatePSliders() { + //update RGB sliders + var col = cpick.color.rgb; + gId('sliderR').value = col.r; + gId('sliderG').value = col.g; + gId('sliderB').value = col.b; + + //update hex field + var str = cpick.color.hexString.substring(1); + var w = whites[csel]; + if (w > 0) str += w.toString(16); + + //update value slider + var v = gId('sliderV'); + v.value = cpick.color.value; + //background color as if color had full value + var hsv = {"h":cpick.color.hue,"s":cpick.color.saturation,"v":100}; + var c = iro.Color.hsvToRgb(hsv); + var cs = 'rgb('+c.r+','+c.g+','+c.b+')'; + v.nextElementSibling.style.backgroundImage = `linear-gradient(90deg, #000 0%, ${cs})`; + + //update Kelvin slider + gId('sliderK').value = cpick.color.kelvin; +} + +function setPicker(rgb) { + var c = new iro.Color(rgb); + if (c.value > 0) cpick.color.set(c); + else cpick.color.setChannel('hsv', 'v', 0); +} + +function fromV() +{ + cpick.color.setChannel('hsv', 'v', d.getElementById('sliderV').value); +} + +function fromK() +{ + cpick.color.set({ kelvin: d.getElementById('sliderK').value }); +} + +function fromRgb() +{ + var r = gId('sliderR').value; + var g = gId('sliderG').value; + var b = gId('sliderB').value; + setPicker(`rgb(${r},${g},${b})`); + setColor(0); +} + +// sets color from picker: 0=all, 1=leaving picker/HSV, 2=ignore white channel +function setColor(sr) +{ + var cd = gId('csl').children; // color slots + if (sr == 1 && cd[csel].style.backgroundColor == 'rgb(0, 0, 0)') cpick.color.setChannel('hsv', 'v', 100); + cd[csel].style.backgroundColor = cpick.color.rgbString; + if (sr != 2) whites[csel] = parseInt(gId('sliderW').value); + var col = cpick.color.rgb; + var obj = {"seg": {"col": [[col.r, col.g, col.b, whites[csel]],[],[]]}}; + if (sr==1 || gId(`picker`).style.display !== "block") obj.seg.fx = 0; + if (csel == 1) { + obj = {"seg": {"col": [[],[col.r, col.g, col.b, whites[csel]],[]]}}; + } else if (csel == 2) { + obj = {"seg": {"col": [[],[],[col.r, col.g, col.b, whites[csel]]]}}; + } + requestJson(obj); +} + +function setBalance(b) +{ + var obj = {"seg": {"cct": parseInt(b)}}; + requestJson(obj); +} + +var hc = 0; +setInterval(()=>{if (!isInfo) return; hc+=18; if (hc>300) hc=0; if (hc>200)hc=306; if (hc==144) hc+=36; if (hc==108) hc+=18; +gId('heart').style.color = `hsl(${hc}, 100%, 50%)`;}, 910); + +function openGH() { window.open("https://github.com/Aircoookie/WLED/wiki"); } + +var cnfr = false; +function cnfReset() +{ + if (!cnfr) { + var bt = gId('resetbtn'); + bt.style.color = "#f00"; + bt.innerHTML = "Confirm Reboot"; + cnfr = true; return; + } + window.location.href = "/reset"; +} + +function loadPalettesData(callback = null) +{ + if (palettesData) return; + const lsKey = "wledPalx"; + var palettesDataJson = localStorage.getItem(lsKey); + if (palettesDataJson) { + try { + palettesDataJson = JSON.parse(palettesDataJson); + if (palettesDataJson && palettesDataJson.vid == lastinfo.vid) { + palettesData = palettesDataJson.p; + if (callback) callback(); //redrawPalPrev() + return; + } + } catch (e) {} + } + + palettesData = {}; + getPalettesData(0, ()=>{ + localStorage.setItem(lsKey, JSON.stringify({ + p: palettesData, + vid: lastinfo.vid + })); + if (callback) setTimeout(callback, 99); //redrawPalPrev() + }); +} + +function getPalettesData(page, callback) +{ + fetch(getURL(`/json/palx?page=${page}`), { + method: 'get', + headers: { + "Content-type": "application/json; charset=UTF-8" + } + }) + .then((res)=>{ + if (!res.ok) showErrorToast(); + return res.json(); + }) + .then((json)=>{ + palettesData = Object.assign({}, palettesData, json.p); + if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 50); + else callback(); + }) + .catch((e)=>{ + showToast(e, true); + }); +} + +function search(f,l=null) +{ + f.nextElementSibling.style.display=(f.value!=='')?'block':'none'; + if (!l) return; + var el = gId(l).querySelectorAll('.lstI'); + for (i = 0; i < el.length; i++) { + var it = el[i]; + var itT = it.querySelector('.lstIname').innerText.toUpperCase(); + it.style.display = itT.indexOf(f.value.toUpperCase())>-1?'':'none'; + } +} + +function clean(c) +{ + c.style.display='none'; + var i=c.previousElementSibling; + i.value=''; + i.focus(); + i.dispatchEvent(new Event('input')); +} + +function unfocusSliders() +{ + gId("sliderBri").blur(); + gId("sliderSpeed").blur(); + gId("sliderIntensity").blur(); +} + +//sliding UI +const _C = d.querySelector('.container'), N = 1; + +let iSlide = 0, x0 = null, scrollS = 0, locked = false, w; + +function unify(e) { return e.changedTouches ? e.changedTouches[0] : e; } + +function hasIroClass(classList) +{ + for (var i = 0; i < classList.length; i++) { + var element = classList[i]; + if (element.startsWith('Iro')) return true; + } + return false; +} +//required by rangetouch.js +function lock(e) +{ + var l = e.target.classList; + var pl = e.target.parentElement.classList; + + if (l.contains('noslide') || hasIroClass(l) || hasIroClass(pl)) return; + + x0 = unify(e).clientX; + scrollS = gEBCN("tabcontent")[iSlide].scrollTop; + + _C.classList.toggle('smooth', !(locked = true)); +} +//required by rangetouch.js +function move(e) +{ + if(!locked) return; + var clientX = unify(e).clientX; + var dx = clientX - x0; + var s = Math.sign(dx); + var f = +(s*dx/w).toFixed(2); + + if((clientX != 0) && + (iSlide > 0 || s < 0) && (iSlide < N - 1 || s > 0) && + f > 0.12 && + gEBCN("tabcontent")[iSlide].scrollTop == scrollS) + { + _C.style.setProperty('--i', iSlide -= s); + f = 1 - f; + updateTablinks(iSlide); + } + _C.style.setProperty('--f', f); + _C.classList.toggle('smooth', !(locked = false)); + x0 = null; +} + +function size() +{ + var h = gId('top').clientHeight; + sCol('--th', h + "px"); + sCol("--tp", h - (gId(`briwrap`).style.display === "block" ? 0 : gId(`briwrap`).clientTop) + "px"); + sCol("--bh", "0px"); +} + +function mergeDeep(target, ...sources) +{ + if (!sources.length) return target; + const source = sources.shift(); + + if (isObj(target) && isObj(source)) { + for (const key in source) { + if (isObj(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + mergeDeep(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + return mergeDeep(target, ...sources); +} + +size(); +window.addEventListener('resize', size, false); + +_C.addEventListener('mousedown', lock, false); +_C.addEventListener('touchstart', lock, false); + +_C.addEventListener('mouseout', move, false); +_C.addEventListener('mouseup', move, false); +_C.addEventListener('touchend', move, false); diff --git a/wled00/data/style.css b/wled00/data/style.css index 672965133d..5daca929a4 100644 --- a/wled00/data/style.css +++ b/wled00/data/style.css @@ -1,84 +1,150 @@ +html { + touch-action: manipulation; +} body { - font-family: Verdana, sans-serif; - text-align: center; - background: #222; - color: #fff; - line-height: 200%%; /* %% because of AsyncWebServer */ - margin: 0; + font-family: Verdana, sans-serif; + font-size: 1rem; + text-align: center; + background: #222; + color: #fff; + line-height: 200%; + margin: 0; } hr { - border-color: #666; + border-color: #666; } -a { - color: #28f; - text-decoration: none; +hr.sml { + width: 260px; +} +a, a:hover { + color: #28f; + text-decoration: none; } button, .btn { - background: #333; - color: #fff; - font-family: Verdana, sans-serif; - border: 0.3ch solid #333; - display: inline-block; - font-size: 20px; - margin: 12px 8px 8px; - padding: 1px 6px; - cursor: pointer; - text-decoration: none; + background: #333; + color: #fff; + font-family: Verdana, sans-serif; + border: 0.3ch solid #333; + border-radius: 24px; + display: inline-block; + font-size: 20px; + margin: 12px 8px 8px; + padding: 8px 12px; + min-width: 48px; + cursor: pointer; + text-decoration: none; +} +button.sml { + padding: 8px; + border-radius: 20px; + font-size: 15px; + min-width: 40px; + margin: 0 0 0 10px; +} +#scan { + margin-top: -10px; +} +.toprow { + top: 0; + position: sticky; + background-color:#222; + z-index:1; } .lnk { - border: 0; + border: 0; } .helpB { - text-align: left; - position: absolute; - width: 60px; + text-align: left; + position: absolute; + width: 60px; +} +.hide { + display: none; +} +.err { + color: #f00; +} +.warn { + color: #fa0; } input { - background: #333; - color: #fff; - font-family: Verdana, sans-serif; - border: 0.5ch solid #333; + background: #333; + color: #fff; + font-family: Verdana, sans-serif; + border: 0.5ch solid #333; } input:disabled { - color: #888; + color: #888; } +input[type="text"], +input[type="number"], +select { + font-size: medium; + margin: 2px; + } input[type="number"] { - width: 4em; - margin: 2px; + width: 4em; } input[type="number"].xxl { - width: 100px; + width: 100px; } input[type="number"].xl { - width: 85px; + width: 85px; } input[type="number"].l { - width: 63px; + width: 64px; } input[type="number"].m { - width: 56px; + width: 56px; } input[type="number"].s { - width: 49px; + width: 48px; } input[type="number"].xs { - width: 42px; + width: 40px; } input[type="checkbox"] { - transform: scale(1.5); + transform: scale(1.5); + margin-right: 10px; +} +td input[type="checkbox"] { + margin-right: revert; +} +input[type=file] { + font-size: 16px } select { - background: #333; - color: #fff; - font-family: Verdana, sans-serif; - border: 0.5ch solid #333; + margin: 2px; + background: #333; + color: #fff; + font-family: Verdana, sans-serif; + border: 0.5ch solid #333; +} +select.pin { + max-width: 120px; + text-overflow: ellipsis; +} +tr { + line-height: 100%; } td { - padding: 2px; + padding: 2px; } .d5 { - width: 4.5em !important; + width: 4rem !important; } +.cal { + font-size:1.5rem; + cursor:pointer +} +#TMT table { + width: 100%; +} + +#msg { + display: none; +} + #toast { opacity: 0; background-color: #444; @@ -91,9 +157,9 @@ td { position: fixed; text-align: center; z-index: 5; - transform: translateX(-50%%); /* %% because of AsyncWebServer */ - max-width: 90%%; /* %% because of AsyncWebServer */ - left: 50%%; /* %% because of AsyncWebServer */ + transform: translateX(-50%); + max-width: 90%; + left: 50%; } #toast.show { @@ -107,3 +173,29 @@ td { background-color: #b21; animation: fadein 0.5s; } + +@media screen and (max-width: 767px) { + input[type="text"], + input[type="file"], + input[type="number"], + input[type="email"], + input[type="tel"], + input[type="password"] { + font-size: 16px; + } +} + +@media screen and (max-width: 480px) { + input[type="number"].s { + width: 40px; + } + input[type="number"].xs { + width: 32px; + } + input[type="file"] { + width: 224px; + } + #btns select { + width: 144px; + } +} \ No newline at end of file diff --git a/wled00/data/update.htm b/wled00/data/update.htm index 16dede5cbe..f157f98d88 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -1,52 +1,28 @@ - - - WLED Update - - + + WLED Update + + - -

WLED Software Update

-
- Installed version: ##VERSION##
- Download the latest binary: -
-
-
-
-
Updating...
Please do not close or refresh the page :)
+ +

WLED Software Update

+
+ Installed version: ##VERSION##
+ Download the latest binary: +
+
+
+ +
+
Updating...
Please do not close or refresh the page :)
- \ No newline at end of file diff --git a/wled00/data/welcome.htm b/wled00/data/welcome.htm index 8d02e6658d..671212a388 100644 --- a/wled00/data/welcome.htm +++ b/wled00/data/welcome.htm @@ -1,63 +1,63 @@ - - - - - Welcome! - - - - -
-

Welcome to WLED!

-

Thank you for installing my application!

-Next steps:

-Connect the module to your local WiFi here!
-
-Just trying this out in AP mode?
-
-
- + img { + width: 950px; + max-width: 82%; + image-rendering: pixelated; + image-rendering: crisp-edges; + margin: 4vh 0 0 0; + animation: fi 1s; + } + + @keyframes fi { + from { opacity: 0; } + to { opacity: 1; } + } + + .main { + animation: fi 1.5s .7s both; + } + + + + +
+

Welcome to WLED!

+

Thank you for installing my application!

+ Next steps:

+ Connect the module to your local WiFi here!
+
+ Just trying this out in AP mode?
+
+
+ \ No newline at end of file diff --git a/wled00/dmx.cpp b/wled00/dmx.cpp index 2842195472..bcd7e7f9e1 100644 --- a/wled00/dmx.cpp +++ b/wled00/dmx.cpp @@ -1,10 +1,13 @@ #include "wled.h" /* - * Support for DMX via MAX485. - * Change the output pin in src/dependencies/ESPDMX.cpp if needed. - * Library from: + * Support for DMX Output via MAX485. + * Change the output pin in src/dependencies/ESPDMX.cpp, if needed (ESP8266) + * Change the output pin in src/dependencies/SparkFunDMX.cpp, if needed (ESP32) + * ESP8266 Library from: * https://github.com/Rickgg/ESP-Dmx + * ESP32 Library from: + * https://github.com/sparkfun/SparkFunDMX */ #ifdef WLED_ENABLE_DMX @@ -14,10 +17,16 @@ void handleDMX() // don't act, when in DMX Proxy mode if (e131ProxyUniverse != 0) return; - // TODO: calculate brightness manually if no shutter channel is set - uint8_t brightness = strip.getBrightness(); + bool calc_brightness = true; + + // check if no shutter channel is set + for (byte i = 0; i < DMXChannels; i++) + { + if (DMXFixtureMap[i] == 5) calc_brightness = false; + } + uint16_t len = strip.getLengthTotal(); for (int i = DMXStartLED; i < len; i++) { // uses the amount of LEDs as fixture count @@ -35,16 +44,16 @@ void handleDMX() dmx.write(DMXAddr, 0); break; case 1: // Red - dmx.write(DMXAddr, r); + dmx.write(DMXAddr, calc_brightness ? (r * brightness) / 255 : r); break; case 2: // Green - dmx.write(DMXAddr, g); + dmx.write(DMXAddr, calc_brightness ? (g * brightness) / 255 : g); break; case 3: // Blue - dmx.write(DMXAddr, b); + dmx.write(DMXAddr, calc_brightness ? (b * brightness) / 255 : b); break; case 4: // White - dmx.write(DMXAddr, w); + dmx.write(DMXAddr, calc_brightness ? (w * brightness) / 255 : w); break; case 5: // Shutter channel. Controls the brightness. dmx.write(DMXAddr, brightness); @@ -60,7 +69,11 @@ void handleDMX() } void initDMX() { + #ifdef ESP8266 dmx.init(512); // initialize with bus length + #else + dmx.initWrite(512); // initialize with bus length + #endif } #else diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 6f7f193bdc..68c7ca5a5a 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -12,7 +12,7 @@ //handles RGB data only void handleDDPPacket(e131_packet_t* p) { int lastPushSeq = e131LastSequenceNumber[0]; - + //reject late packets belonging to previous frame (assuming 4 packets max. before push) if (e131SkipOutOfSequence && lastPushSeq) { int sn = p->sequenceNum & 0xF; @@ -25,19 +25,21 @@ void handleDDPPacket(e131_packet_t* p) { } } - uint32_t start = htonl(p->channelOffset) /3; - start += DMXAddress /3; - uint16_t stop = start + htons(p->dataLen) /3; + uint8_t ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel) + + uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed; + start += DMXAddress / ddpChannelsPerLed; + uint16_t stop = start + htons(p->dataLen) / ddpChannelsPerLed; uint8_t* data = p->data; uint16_t c = 0; if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); - - if (!realtimeOverride) { + + if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) { for (uint16_t i = start; i < stop; i++) { - setRealtimePixel(i, data[c], data[c+1], data[c+2], 0); - c+=3; + setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0); + c += ddpChannelsPerLed; } } @@ -58,16 +60,30 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (protocol == P_ARTNET) { + if (p->art_opcode == ARTNET_OPCODE_OPPOLL) { + handleArtnetPollReply(clientIP); + return; + } uni = p->art_universe; dmxChannels = htons(p->art_length); e131_data = p->art_data; seq = p->art_sequence_number; mde = REALTIME_MODE_ARTNET; } else if (protocol == P_E131) { + // Ignore PREVIEW data (E1.31: 6.2.6) + if ((p->options & 0x80) != 0) return; + dmxChannels = htons(p->property_value_count) - 1; + // DMX level data is zero start code. Ignore everything else. (E1.11: 8.5) + if (dmxChannels == 0 || p->property_values[0] != 0) return; uni = htons(p->universe); - dmxChannels = htons(p->property_value_count) -1; e131_data = p->property_values; seq = p->sequence_number; + if (e131Priority != 0) { + if (p->priority < e131Priority ) return; + // track highest priority & skip all lower priorities + if (p->priority >= highPriority.get()) highPriority.set(p->priority); + if (p->priority < highPriority.get()) return; + } } else { //DDP realtimeIP = clientIP; handleDDPPacket(p); @@ -84,115 +100,214 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ #endif // only listen for universes we're handling & allocated memory - if (uni >= (e131Universe + E131_MAX_UNIVERSE_COUNT)) return; + if (uni < e131Universe || uni >= (e131Universe + E131_MAX_UNIVERSE_COUNT)) return; uint8_t previousUniverses = uni - e131Universe; if (e131SkipOutOfSequence) - if (seq < e131LastSequenceNumber[uni-e131Universe] && seq > 20 && e131LastSequenceNumber[uni-e131Universe] < 250){ - DEBUG_PRINT("skipping E1.31 frame (last seq="); - DEBUG_PRINT(e131LastSequenceNumber[uni-e131Universe]); - DEBUG_PRINT(", current seq="); + if (seq < e131LastSequenceNumber[previousUniverses] && seq > 20 && e131LastSequenceNumber[previousUniverses] < 250){ + DEBUG_PRINT(F("skipping E1.31 frame (last seq=")); + DEBUG_PRINT(e131LastSequenceNumber[previousUniverses]); + DEBUG_PRINT(F(", current seq=")); DEBUG_PRINT(seq); - DEBUG_PRINT(", universe="); + DEBUG_PRINT(F(", universe=")); DEBUG_PRINT(uni); DEBUG_PRINTLN(")"); return; } - e131LastSequenceNumber[uni-e131Universe] = seq; + e131LastSequenceNumber[previousUniverses] = seq; // update status info realtimeIP = clientIP; byte wChannel = 0; uint16_t totalLen = strip.getLengthTotal(); + uint16_t availDMXLen = 0; + uint16_t dataOffset = DMXAddress; + + // For legacy DMX start address 0 the available DMX length offset is 0 + const uint16_t dmxLenOffset = (DMXAddress == 0) ? 0 : 1; + + // Check if DMX start address fits in available channels + if (dmxChannels >= DMXAddress) { + availDMXLen = (dmxChannels - DMXAddress) + dmxLenOffset; + } + + // DMX data in Art-Net packet starts at index 0, for E1.31 at index 1 + if (protocol == P_ARTNET && dataOffset > 0) { + dataOffset--; + } switch (DMXMode) { case DMX_MODE_DISABLED: return; // nothing to do break; - case DMX_MODE_SINGLE_RGB: // RGB only + case DMX_MODE_SINGLE_RGB: // 3 channel: [R,G,B] if (uni != e131Universe) return; - if (dmxChannels-DMXAddress+1 < 3) return; + if (availDMXLen < 3) return; + realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride) return; - wChannel = (dmxChannels-DMXAddress+1 > 3) ? e131_data[DMXAddress+3] : 0; + + if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + + wChannel = (availDMXLen > 3) ? e131_data[dataOffset+3] : 0; for (uint16_t i = 0; i < totalLen; i++) - setRealtimePixel(i, e131_data[DMXAddress+0], e131_data[DMXAddress+1], e131_data[DMXAddress+2], wChannel); + setRealtimePixel(i, e131_data[dataOffset+0], e131_data[dataOffset+1], e131_data[dataOffset+2], wChannel); break; - case DMX_MODE_SINGLE_DRGB: // Dimmer + RGB + case DMX_MODE_SINGLE_DRGB: // 4 channel: [Dimmer,R,G,B] if (uni != e131Universe) return; - if (dmxChannels-DMXAddress+1 < 4) return; + if (availDMXLen < 4) return; + realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride) return; - wChannel = (dmxChannels-DMXAddress+1 > 4) ? e131_data[DMXAddress+4] : 0; - if (DMXOldDimmer != e131_data[DMXAddress+0]) { - DMXOldDimmer = e131_data[DMXAddress+0]; - bri = e131_data[DMXAddress+0]; + if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + wChannel = (availDMXLen > 4) ? e131_data[dataOffset+4] : 0; + + if (bri != e131_data[dataOffset+0]) { + bri = e131_data[dataOffset+0]; strip.setBrightness(bri, true); } + for (uint16_t i = 0; i < totalLen; i++) - setRealtimePixel(i, e131_data[DMXAddress+1], e131_data[DMXAddress+2], e131_data[DMXAddress+3], wChannel); + setRealtimePixel(i, e131_data[dataOffset+1], e131_data[dataOffset+2], e131_data[dataOffset+3], wChannel); break; - case DMX_MODE_EFFECT: // Length 1: Apply Preset ID, length 11-13: apply effect config - if (uni != e131Universe) return; - if (dmxChannels-DMXAddress+1 < 11) { - if (dmxChannels-DMXAddress+1 > 1) return; - applyPreset(e131_data[DMXAddress+0], CALL_MODE_NOTIFICATION); + case DMX_MODE_PRESET: // 2 channel: [Dimmer,Preset] + { + if (uni != e131Universe || availDMXLen < 2) return; + + // limit max. selectable preset to 250, even though DMX max. val is 255 + uint8_t dmxValPreset = (e131_data[dataOffset+1] > 250 ? 250 : e131_data[dataOffset+1]); + + // only apply preset if value changed + if (dmxValPreset != 0 && dmxValPreset != currentPreset && + // only apply preset if not in playlist, or playlist changed + (currentPlaylist < 0 || dmxValPreset != currentPlaylist)) { + presetCycCurr = dmxValPreset; + unloadPlaylist(); // applying a preset unloads the playlist + applyPreset(dmxValPreset, CALL_MODE_NOTIFICATION); + } + + // only change brightness if value changed + if (bri != e131_data[dataOffset]) { + bri = e131_data[dataOffset]; + strip.setBrightness(scaledBri(bri), false); + stateUpdated(CALL_MODE_WS_SEND); + } return; + break; } - if (DMXOldDimmer != e131_data[DMXAddress+0]) { - DMXOldDimmer = e131_data[DMXAddress+0]; - bri = e131_data[DMXAddress+0]; - } - if (e131_data[DMXAddress+1] < MODE_COUNT) - effectCurrent = e131_data[DMXAddress+ 1]; - effectSpeed = e131_data[DMXAddress+ 2]; // flickers - effectIntensity = e131_data[DMXAddress+ 3]; - effectPalette = e131_data[DMXAddress+ 4]; - col[0] = e131_data[DMXAddress+ 5]; - col[1] = e131_data[DMXAddress+ 6]; - col[2] = e131_data[DMXAddress+ 7]; - colSec[0] = e131_data[DMXAddress+ 8]; - colSec[1] = e131_data[DMXAddress+ 9]; - colSec[2] = e131_data[DMXAddress+10]; - if (dmxChannels-DMXAddress+1 > 11) + + case DMX_MODE_EFFECT: // 15 channels [bri,effectCurrent,effectSpeed,effectIntensity,effectPalette,effectOption,R,G,B,R2,G2,B2,R3,G3,B3] + case DMX_MODE_EFFECT_W: // 18 channels, same as above but with extra +3 white channels [..,W,W2,W3] + case DMX_MODE_EFFECT_SEGMENT: // 15 channels per segment; + case DMX_MODE_EFFECT_SEGMENT_W: // 18 Channels per segment; { - col[3] = e131_data[DMXAddress+11]; //white - colSec[3] = e131_data[DMXAddress+12]; - } - transitionDelayTemp = 0; // act fast - colorUpdated(CALL_MODE_NOTIFICATION); // don't send UDP - return; // don't activate realtime live mode - break; + if (uni != e131Universe) return; + bool isSegmentMode = DMXMode == DMX_MODE_EFFECT_SEGMENT || DMXMode == DMX_MODE_EFFECT_SEGMENT_W; + uint8_t dmxEffectChannels = (DMXMode == DMX_MODE_EFFECT || DMXMode == DMX_MODE_EFFECT_SEGMENT) ? 15 : 18; + for (uint8_t id = 0; id < strip.getSegmentsNum(); id++) { + Segment& seg = strip.getSegment(id); + if (isSegmentMode) + dataOffset = DMXAddress + id * (dmxEffectChannels + DMXSegmentSpacing); + else + dataOffset = DMXAddress; + // Modify address for Art-Net data + if (protocol == P_ARTNET && dataOffset > 0) + dataOffset--; + // Skip out of universe addresses + if (dataOffset > dmxChannels - dmxEffectChannels + 1) + return; + + if (e131_data[dataOffset+1] < strip.getModeCount()) + if (e131_data[dataOffset+1] != seg.mode) seg.setMode( e131_data[dataOffset+1]); + if (e131_data[dataOffset+2] != seg.speed) seg.speed = e131_data[dataOffset+2]; + if (e131_data[dataOffset+3] != seg.intensity) seg.intensity = e131_data[dataOffset+3]; + if (e131_data[dataOffset+4] != seg.palette) seg.setPalette(e131_data[dataOffset+4]); + + uint8_t segOption = (uint8_t)floor(e131_data[dataOffset+5]/64.0); + if (segOption == 0 && (seg.mirror || seg.reverse )) {seg.setOption(SEG_OPTION_MIRROR, false); seg.setOption(SEG_OPTION_REVERSED, false);} + if (segOption == 1 && (seg.mirror || !seg.reverse)) {seg.setOption(SEG_OPTION_MIRROR, false); seg.setOption(SEG_OPTION_REVERSED, true);} + if (segOption == 2 && (!seg.mirror || seg.reverse )) {seg.setOption(SEG_OPTION_MIRROR, true); seg.setOption(SEG_OPTION_REVERSED, false);} + if (segOption == 3 && (!seg.mirror || !seg.reverse)) {seg.setOption(SEG_OPTION_MIRROR, true); seg.setOption(SEG_OPTION_REVERSED, true);} + uint32_t colors[3]; + byte whites[3] = {0,0,0}; + if (dmxEffectChannels == 18) { + whites[0] = e131_data[dataOffset+15]; + whites[1] = e131_data[dataOffset+16]; + whites[2] = e131_data[dataOffset+17]; + } + colors[0] = RGBW32(e131_data[dataOffset+ 6], e131_data[dataOffset+ 7], e131_data[dataOffset+ 8], whites[0]); + colors[1] = RGBW32(e131_data[dataOffset+ 9], e131_data[dataOffset+10], e131_data[dataOffset+11], whites[1]); + colors[2] = RGBW32(e131_data[dataOffset+12], e131_data[dataOffset+13], e131_data[dataOffset+14], whites[2]); + if (colors[0] != seg.colors[0]) seg.setColor(0, colors[0]); + if (colors[1] != seg.colors[1]) seg.setColor(1, colors[1]); + if (colors[2] != seg.colors[2]) seg.setColor(2, colors[2]); + + // Set segment opacity or global brightness + if (isSegmentMode) { + if (e131_data[dataOffset] != seg.opacity) seg.setOpacity(e131_data[dataOffset]); + } else if ( id == strip.getSegmentsNum()-1 ) { + if (bri != e131_data[dataOffset]) { + bri = e131_data[dataOffset]; + strip.setBrightness(bri, true); + } + } + } + return; + break; + } + case DMX_MODE_MULTIPLE_DRGB: case DMX_MODE_MULTIPLE_RGB: case DMX_MODE_MULTIPLE_RGBW: { - realtimeLock(realtimeTimeoutMs, mde); bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); const uint16_t dmxChannelsPerLed = is4Chan ? 4 : 3; const uint16_t ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; - if (realtimeOverride) return; - uint16_t previousLeds, dmxOffset; + uint8_t stripBrightness = bri; + uint16_t previousLeds, dmxOffset, ledsTotal; + if (previousUniverses == 0) { - if (dmxChannels-DMXAddress < 1) return; - dmxOffset = DMXAddress; + if (availDMXLen < 1) return; + dmxOffset = dataOffset; previousLeds = 0; // First DMX address is dimmer in DMX_MODE_MULTIPLE_DRGB mode. if (DMXMode == DMX_MODE_MULTIPLE_DRGB) { - strip.setBrightness(e131_data[dmxOffset++], true); + stripBrightness = e131_data[dmxOffset++]; + ledsTotal = (availDMXLen - 1) / dmxChannelsPerLed; + } else { + ledsTotal = availDMXLen / dmxChannelsPerLed; } } else { // All subsequent universes start at the first channel. dmxOffset = (protocol == P_ARTNET) ? 0 : 1; - uint16_t ledsInFirstUniverse = (MAX_CHANNELS_PER_UNIVERSE - DMXAddress) / dmxChannelsPerLed; + const uint16_t dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; + uint16_t ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; previousLeds = ledsInFirstUniverse + (previousUniverses - 1) * ledsPerUniverse; + ledsTotal = previousLeds + (dmxChannels / dmxChannelsPerLed); + } + + // All LEDs already have values + if (previousLeds >= totalLen) { + return; + } + + realtimeLock(realtimeTimeoutMs, mde); + if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + + if (ledsTotal > totalLen) { + ledsTotal = totalLen; } - uint16_t ledsTotal = previousLeds + (dmxChannels - dmxOffset +1) / dmxChannelsPerLed; + + if (DMXMode == DMX_MODE_MULTIPLE_DRGB && previousUniverses == 0) { + if (bri != stripBrightness) { + bri = stripBrightness; + strip.setBrightness(bri, true); + } + } + if (!is4Chan) { for (uint16_t i = previousLeds; i < ledsTotal; i++) { setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], 0); @@ -214,3 +329,201 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ e131NewData = true; } + +void handleArtnetPollReply(IPAddress ipAddress) { + ArtPollReply artnetPollReply; + prepareArtnetPollReply(&artnetPollReply); + + uint16_t startUniverse = e131Universe; + uint16_t endUniverse = e131Universe; + + switch (DMXMode) { + case DMX_MODE_DISABLED: + return; // nothing to do + break; + + case DMX_MODE_SINGLE_RGB: + case DMX_MODE_SINGLE_DRGB: + case DMX_MODE_PRESET: + case DMX_MODE_EFFECT: + case DMX_MODE_EFFECT_W: + case DMX_MODE_EFFECT_SEGMENT: + case DMX_MODE_EFFECT_SEGMENT_W: + break; // 1 universe is enough + + case DMX_MODE_MULTIPLE_DRGB: + case DMX_MODE_MULTIPLE_RGB: + case DMX_MODE_MULTIPLE_RGBW: + { + bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); + const uint16_t dmxChannelsPerLed = is4Chan ? 4 : 3; + const uint16_t dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; + const uint16_t dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // For legacy DMX start address 0 + const uint16_t ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; + const uint16_t totalLen = strip.getLengthTotal(); + + if (totalLen > ledsInFirstUniverse) { + const uint16_t ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; + const uint16_t remainLED = totalLen - ledsInFirstUniverse; + + endUniverse += (remainLED / ledsPerUniverse); + + if ((remainLED % ledsPerUniverse) > 0) { + endUniverse++; + } + + if ((endUniverse - startUniverse) > E131_MAX_UNIVERSE_COUNT) { + endUniverse = startUniverse + E131_MAX_UNIVERSE_COUNT - 1; + } + } + break; + } + default: + DEBUG_PRINTLN(F("unknown E1.31 DMX mode")); + return; // nothing to do + break; + } + + for (uint16_t i = startUniverse; i <= endUniverse; ++i) { + sendArtnetPollReply(&artnetPollReply, ipAddress, i); + } +} + +void prepareArtnetPollReply(ArtPollReply *reply) { + // Art-Net + reply->reply_id[0] = 0x41; + reply->reply_id[1] = 0x72; + reply->reply_id[2] = 0x74; + reply->reply_id[3] = 0x2d; + reply->reply_id[4] = 0x4e; + reply->reply_id[5] = 0x65; + reply->reply_id[6] = 0x74; + reply->reply_id[7] = 0x00; + + reply->reply_opcode = ARTNET_OPCODE_OPPOLLREPLY; + + IPAddress localIP = Network.localIP(); + for (uint8_t i = 0; i < 4; i++) { + reply->reply_ip[i] = localIP[i]; + } + + reply->reply_port = ARTNET_DEFAULT_PORT; + + char * numberEnd = versionString; + reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10); + numberEnd++; + reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10); + + // Switch values depend on universe, set before sending + reply->reply_net_sw = 0x00; + reply->reply_sub_sw = 0x00; + + reply->reply_oem_h = 0x00; // TODO add assigned oem code + reply->reply_oem_l = 0x00; + + reply->reply_ubea_ver = 0x00; + + // Indicators in Normal Mode + // All or part of Port-Address programmed by network or Web browser + reply->reply_status_1 = 0xE0; + + reply->reply_esta_man = 0x0000; + + strlcpy((char *)(reply->reply_short_name), serverDescription, 18); + strlcpy((char *)(reply->reply_long_name), serverDescription, 64); + + reply->reply_node_report[0] = '\0'; + + reply->reply_num_ports_h = 0x00; + reply->reply_num_ports_l = 0x01; // One output port + + reply->reply_port_types[0] = 0x80; // Output DMX data + reply->reply_port_types[1] = 0x00; + reply->reply_port_types[2] = 0x00; + reply->reply_port_types[3] = 0x00; + + // No inputs + reply->reply_good_input[0] = 0x00; + reply->reply_good_input[1] = 0x00; + reply->reply_good_input[2] = 0x00; + reply->reply_good_input[3] = 0x00; + + // One output + reply->reply_good_output_a[0] = 0x80; // Data is being transmitted + reply->reply_good_output_a[1] = 0x00; + reply->reply_good_output_a[2] = 0x00; + reply->reply_good_output_a[3] = 0x00; + + // Values depend on universe, set before sending + reply->reply_sw_in[0] = 0x00; + reply->reply_sw_in[1] = 0x00; + reply->reply_sw_in[2] = 0x00; + reply->reply_sw_in[3] = 0x00; + + // Values depend on universe, set before sending + reply->reply_sw_out[0] = 0x00; + reply->reply_sw_out[1] = 0x00; + reply->reply_sw_out[2] = 0x00; + reply->reply_sw_out[3] = 0x00; + + reply->reply_sw_video = 0x00; + reply->reply_sw_macro = 0x00; + reply->reply_sw_remote = 0x00; + + reply->reply_spare[0] = 0x00; + reply->reply_spare[1] = 0x00; + reply->reply_spare[2] = 0x00; + + // A DMX to / from Art-Net device + reply->reply_style = 0x00; + + Network.localMAC(reply->reply_mac); + + for (uint8_t i = 0; i < 4; i++) { + reply->reply_bind_ip[i] = localIP[i]; + } + + reply->reply_bind_index = 1; + + // Product supports web browser configuration + // Node’s IP is DHCP or manually configured + // Node is DHCP capable + // Node supports 15 bit Port-Address (Art-Net 3 or 4) + // Node is able to switch between ArtNet and sACN + reply->reply_status_2 = (staticIP[0] == 0) ? 0x1F : 0x1D; + + // RDM is disabled + // Output style is continuous + reply->reply_good_output_b[0] = 0xC0; + reply->reply_good_output_b[1] = 0xC0; + reply->reply_good_output_b[2] = 0xC0; + reply->reply_good_output_b[3] = 0xC0; + + // Fail-over state: Hold last state + // Node does not support fail-over + reply->reply_status_3 = 0x00; + + for (uint8_t i = 0; i < 21; i++) { + reply->reply_filler[i] = 0x00; + } +} + +void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress) { + reply->reply_net_sw = (uint8_t)((portAddress >> 8) & 0x007F); + reply->reply_sub_sw = (uint8_t)((portAddress >> 4) & 0x000F); + reply->reply_sw_out[0] = (uint8_t)(portAddress & 0x000F); + + snprintf_P((char *)reply->reply_node_report, sizeof(reply->reply_node_report)-1, PSTR("#0001 [%04u] OK - WLED v" TOSTRING(WLED_VERSION)), pollReplyCount); + + if (pollReplyCount < 9999) { + pollReplyCount++; + } else { + pollReplyCount = 0; + } + + notifierUdp.beginPacket(ipAddress, ARTNET_DEFAULT_PORT); + notifierUdp.write(reply->raw, sizeof(ArtPollReply)); + notifierUdp.endPacket(); + + reply->reply_bind_index++; +} \ No newline at end of file diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 5762708975..c65f7a90b4 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -1,24 +1,16 @@ #ifndef WLED_FCN_DECLARE_H #define WLED_FCN_DECLARE_H -#include -#include "src/dependencies/espalexa/EspalexaDevice.h" -#include "src/dependencies/e131/ESPAsyncE131.h" /* * All globally accessible functions are declared here */ //alexa.cpp +#ifndef WLED_DISABLE_ALEXA void onAlexaChange(EspalexaDevice* dev); void alexaInit(); void handleAlexa(); void onAlexaChange(EspalexaDevice* dev); - -//blynk.cpp -#ifndef WLED_DISABLE_BLYNK -void initBlynk(const char* auth, const char* host, uint16_t port); -void handleBlynk(); -void updateBlynk(); #endif //button.cpp @@ -58,20 +50,31 @@ bool getJsonValue(const JsonVariant& element, DestType& destination, const Defau //colors.cpp +// similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) +class NeoGammaWLEDMethod { + public: + static uint8_t Correct(uint8_t value); // apply Gamma to single channel + static uint32_t Correct32(uint32_t color); // apply Gamma to RGBW32 color (WLED specific, not used by NPB) + static void calcGammaTable(float gamma); // re-calculates & fills gamma table + static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; } // get value from Gamma table (WLED specific, not used by NPB) + private: + static uint8_t gammaT[]; +}; +#define gamma32(c) NeoGammaWLEDMethod::Correct32(c) +#define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) +uint32_t color_blend(uint32_t,uint32_t,uint16_t,bool b16=false); +uint32_t color_add(uint32_t,uint32_t, bool fast=false); +uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); //hue, sat to rgb void colorKtoRGB(uint16_t kelvin, byte* rgb); void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb - void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO void colorRGBtoXY(byte* rgb, float* xy); // only defined if huesync disabled TODO - void colorFromDecOrHexString(byte* rgb, char* in); bool colorFromHexString(byte* rgb, const char* in); - uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); uint16_t approximateKelvinFromRGB(uint32_t rgb); - void setRandomColor(byte* rgb); //dmx.cpp @@ -80,6 +83,9 @@ void handleDMX(); //e131.cpp void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol); +void handleArtnetPollReply(IPAddress ipAddress); +void prepareArtnetPollReply(ArtPollReply* reply); +void sendArtnetPollReply(ArtPollReply* reply, IPAddress ipAddress, uint16_t portAddress); //file.cpp bool handleFileRead(AsyncWebServerRequest*, String path); @@ -99,13 +105,22 @@ void sendHuePoll(); void onHueData(void* arg, AsyncClient* client, void *data, size_t len); //improv.cpp +enum ImprovRPCType { + Command_Wifi = 0x01, + Request_State = 0x02, + Request_Info = 0x03, + Request_Scan = 0x04 +}; + void handleImprovPacket(); +void sendImprovRPCResult(ImprovRPCType type, uint8_t n_strings = 0, const char **strings = nullptr); void sendImprovStateResponse(uint8_t state, bool error = false); void sendImprovInfoResponse(); -void sendImprovRPCResponse(uint8_t commandId); +void startImprovWifiScan(); +void handleImprovWifiScan(); +void sendImprovIPRPCResult(ImprovRPCType type); //ir.cpp -//bool decodeIRCustom(uint32_t code); void applyRepeatActions(); byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF); void decodeIR(uint32_t code); @@ -128,11 +143,13 @@ void handleIR(); #include "src/dependencies/json/AsyncJson-v6.h" #include "FX.h" -void deserializeSegment(JsonObject elem, byte it, byte presetId = 0); +bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0); bool deserializeState(JsonObject root, byte callMode = CALL_MODE_DIRECT_CHANGE, byte presetId = 0); -void serializeSegment(JsonObject& root, WS2812FX::Segment& seg, byte id, bool forPreset = false, bool segmentBounds = true); -void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true); +void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset = false, bool segmentBounds = true); +void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true, bool selectedSegmentsOnly = false); void serializeInfo(JsonObject root); +void serializeModeNames(JsonArray root); +void serializeModeData(JsonArray root); void serveJson(AsyncWebServerRequest* request); #ifdef WLED_ENABLE_JSONLIVE bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); @@ -154,9 +171,11 @@ void handleTransitions(); void handleNightlight(); byte scaledBri(byte in); +#ifdef WLED_ENABLE_LOXONE //lx_parser.cpp bool parseLx(int lxValue, byte* rgbw); void parseLxJson(int lxValue, byte segId, bool secondary); +#endif //mqtt.cpp bool initMqtt(); @@ -166,7 +185,7 @@ void publishMqtt(); void handleTime(); void handleNetworkTime(); void sendNTPPacket(); -bool checkNTPResponse(); +bool checkNTPResponse(); void updateLocalTime(); void getTimeString(char* out); bool checkCountdown(); @@ -186,57 +205,97 @@ void shufflePlaylist(); void unloadPlaylist(); int16_t loadPlaylist(JsonObject playlistObject, byte presetId = 0); void handlePlaylist(); +void serializePlaylist(JsonObject obj); //presets.cpp +void initPresetsFile(); +void handlePresets(); bool applyPreset(byte index, byte callMode = CALL_MODE_DIRECT_CHANGE); +void applyPresetWithFallback(uint8_t presetID, uint8_t callMode, uint8_t effectID = 0, uint8_t paletteID = 0); inline bool applyTemporaryPreset() {return applyPreset(255);}; void savePreset(byte index, const char* pname = nullptr, JsonObject saveobj = JsonObject()); inline void saveTemporaryPreset() {savePreset(255);}; void deletePreset(byte index); +bool getPresetName(byte index, String& name); + +//remote.cpp +void handleRemote(); //set.cpp bool isAsterisksOnly(const char* str, byte maxLen); void handleSettingsSet(AsyncWebServerRequest *request, byte subPage); bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=true); -int getNumVal(const String* req, uint16_t pos); -void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); -bool updateVal(const String* req, const char* key, byte* val, byte minv=0, byte maxv=255); //udp.cpp void notify(byte callMode, bool followUp=false); -uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte *buffer, uint8_t bri=255, bool isRGBW=false); +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8_t *buffer, uint8_t bri=255, bool isRGBW=false); void realtimeLock(uint32_t timeoutMs, byte md = REALTIME_MODE_GENERIC); +void exitRealtime(); void handleNotifications(); void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w); void refreshNodeList(); void sendSysInfoUDP(); -//util.cpp -//bool oappend(const char* txt); // append new c string to temp buffer efficiently -//bool oappendi(int i); // append new number to temp buffer efficiently -//void sappend(char stype, const char* key, int val); -//void sappends(char stype, const char* key, char* val); -//void prepareHostname(char* hostname); -//bool isAsterisksOnly(const char* str, byte maxLen); -bool requestJSONBufferLock(uint8_t module=255); -void releaseJSONBufferLock(); -uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen); +//network.cpp +int getSignalQuality(int rssi); +void WiFiEvent(WiFiEvent_t event); //um_manager.cpp +typedef enum UM_Data_Types { + UMT_BYTE = 0, + UMT_UINT16, + UMT_INT16, + UMT_UINT32, + UMT_INT32, + UMT_FLOAT, + UMT_DOUBLE, + UMT_BYTE_ARR, + UMT_UINT16_ARR, + UMT_INT16_ARR, + UMT_UINT32_ARR, + UMT_INT32_ARR, + UMT_FLOAT_ARR, + UMT_DOUBLE_ARR +} um_types_t; +typedef struct UM_Exchange_Data { + // should just use: size_t arr_size, void **arr_ptr, byte *ptr_type + size_t u_size; // size of u_data array + um_types_t *u_type; // array of data types + void **u_data; // array of pointers to data + UM_Exchange_Data() { + u_size = 0; + u_type = nullptr; + u_data = nullptr; + } + ~UM_Exchange_Data() { + if (u_type) delete[] u_type; + if (u_data) delete[] u_data; + } +} um_data_t; +const unsigned int um_data_size = sizeof(um_data_t); // 12 bytes + class Usermod { + protected: + um_data_t *um_data; // um_data should be allocated using new in (derived) Usermod's setup() or constructor public: - virtual void loop() {} - virtual void handleOverlayDraw() {} - virtual bool handleButton(uint8_t b) { return false; } - virtual void setup() {} - virtual void connected() {} - virtual void addToJsonState(JsonObject& obj) {} - virtual void addToJsonInfo(JsonObject& obj) {} - virtual void readFromJsonState(JsonObject& obj) {} - virtual void addToConfig(JsonObject& obj) {} + Usermod() { um_data = nullptr; } + virtual ~Usermod() { if (um_data) delete um_data; } + virtual void setup() = 0; // pure virtual, has to be overriden + virtual void loop() = 0; // pure virtual, has to be overriden + virtual void handleOverlayDraw() {} // called after all effects have been processed, just before strip.show() + virtual bool handleButton(uint8_t b) { return false; } // button overrides are possible here + virtual bool getUMData(um_data_t **data) { if (data) *data = nullptr; return false; }; // usermod data exchange [see examples for audio effects] + virtual void connected() {} // called when WiFi is (re)connected + virtual void appendConfigData() {} // helper function called from usermod settings page to add metadata for entry fields + virtual void addToJsonState(JsonObject& obj) {} // add JSON objects for WLED state + virtual void addToJsonInfo(JsonObject& obj) {} // add JSON objects for UI Info page + virtual void readFromJsonState(JsonObject& obj) {} // process JSON messages received from web server + virtual void addToConfig(JsonObject& obj) {} // add JSON entries that go to cfg.json virtual bool readFromConfig(JsonObject& obj) { return true; } // Note as of 2021-06 readFromConfig() now needs to return a bool, see usermod_v2_example.h - virtual void onMqttConnect(bool sessionPresent) {} - virtual bool onMqttMessage(char* topic, char* payload) { return false; } + virtual void onMqttConnect(bool sessionPresent) {} // fired when MQTT connection is established (so usermod can subscribe) + virtual bool onMqttMessage(char* topic, char* payload) { return false; } // fired upon MQTT message received (wled topic) + virtual void onUpdateBegin(bool) {} // fired prior to and after unsuccessful firmware update + virtual void onStateChange(uint8_t mode) {} // fired upon WLED state change virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} }; @@ -249,8 +308,10 @@ class UsermodManager { void loop(); void handleOverlayDraw(); bool handleButton(uint8_t b); + bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods void setup(); void connected(); + void appendConfigData(); void addToJsonState(JsonObject& obj); void addToJsonInfo(JsonObject& obj); void readFromJsonState(JsonObject& obj); @@ -258,9 +319,11 @@ class UsermodManager { bool readFromConfig(JsonObject& obj); void onMqttConnect(bool sessionPresent); bool onMqttMessage(char* topic, char* payload); + void onUpdateBegin(bool); + void onStateChange(uint8_t); bool add(Usermod* um); Usermod* lookup(uint16_t mod_id); - byte getModCount(); + byte getModCount() {return numMods;}; }; //usermods_list.cpp @@ -271,11 +334,73 @@ void userSetup(); void userConnected(); void userLoop(); +//util.cpp +int getNumVal(const String* req, uint16_t pos); +void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); +bool getVal(JsonVariant elem, byte* val, byte minv=0, byte maxv=255); +bool updateVal(const char* req, const char* key, byte* val, byte minv=0, byte maxv=255); +bool oappend(const char* txt); // append new c string to temp buffer efficiently +bool oappendi(int i); // append new number to temp buffer efficiently +void sappend(char stype, const char* key, int val); +void sappends(char stype, const char* key, char* val); +void prepareHostname(char* hostname); +bool isAsterisksOnly(const char* str, byte maxLen); +bool requestJSONBufferLock(uint8_t module=255); +void releaseJSONBufferLock(); +uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen); +uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var = nullptr); +int16_t extractModeDefaults(uint8_t mode, const char *segVar); +void checkSettingsPIN(const char *pin); +uint16_t crc16(const unsigned char* data_p, size_t length); +um_data_t* simulateSound(uint8_t simulationId); +void enumerateLedmaps(); +uint8_t get_random_wheel_index(uint8_t pos); + +// RAII guard class for the JSON Buffer lock +// Modeled after std::lock_guard +class JSONBufferGuard { + bool holding_lock; + public: + inline JSONBufferGuard(uint8_t module=255) : holding_lock(requestJSONBufferLock(module)) {}; + inline ~JSONBufferGuard() { if (holding_lock) releaseJSONBufferLock(); }; + inline JSONBufferGuard(const JSONBufferGuard&) = delete; // Noncopyable + inline JSONBufferGuard& operator=(const JSONBufferGuard&) = delete; + inline JSONBufferGuard(JSONBufferGuard&& r) : holding_lock(r.holding_lock) { r.holding_lock = false; }; // but movable + inline JSONBufferGuard& operator=(JSONBufferGuard&& r) { holding_lock |= r.holding_lock; r.holding_lock = false; return *this; }; + inline bool owns_lock() const { return holding_lock; } + explicit inline operator bool() const { return owns_lock(); }; + inline void release() { if (holding_lock) releaseJSONBufferLock(); holding_lock = false; } +}; + +#ifdef WLED_ADD_EEPROM_SUPPORT //wled_eeprom.cpp void applyMacro(byte index); void deEEP(); void deEEPSettings(); void clearEEPROM(); +#endif + +//wled_math.cpp +#ifndef WLED_USE_REAL_MATH + template T atan_t(T x); + float cos_t(float phi); + float sin_t(float x); + float tan_t(float x); + float acos_t(float x); + float asin_t(float x); + float floor_t(float x); + float fmod_t(float num, float denom); +#else + #include + #define sin_t sin + #define cos_t cos + #define tan_t tan + #define asin_t asin + #define acos_t acos + #define atan_t atan + #define fmod_t fmod + #define floor_t floor +#endif //wled_serial.cpp void handleSerial(); @@ -283,15 +408,16 @@ void updateBaudRate(uint32_t rate); //wled_server.cpp bool isIp(String str); +void createEditHandler(bool enable); bool captivePortal(AsyncWebServerRequest *request); void initServer(); void serveIndexOrWelcome(AsyncWebServerRequest *request); void serveIndex(AsyncWebServerRequest* request); String msgProcessor(const String& var); void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255); -String settingsProcessor(const String& var); String dmxProcessor(const String& var); void serveSettings(AsyncWebServerRequest* request, bool post = false); +void serveSettingsJS(AsyncWebServerRequest* request); //ws.cpp void handleWs(); @@ -301,8 +427,6 @@ void sendDataWs(AsyncWebSocketClient * client = nullptr); //xml.cpp void XML_response(AsyncWebServerRequest *request, char* dest = nullptr); void URL_response(AsyncWebServerRequest *request); -void sappend(char stype, const char* key, int val); -void sappends(char stype, const char* key, char* val); void getSettingsJS(byte subPage, char* dest); #endif diff --git a/wled00/file.cpp b/wled00/file.cpp index 55ebf081ac..4edbdd6fba 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -27,9 +27,10 @@ // There are no consecutive spaces longer than this in the file, so if more space is required, findSpace() can return false immediately // Actual space may be lower -uint16_t knownLargestSpace = UINT16_MAX; +constexpr size_t MAX_SPACE = UINT16_MAX * 2U; // smallest supported config has 128Kb flash size +static volatile size_t knownLargestSpace = MAX_SPACE; -File f; +static File f; // don't export to other cpp files //wrapper to find out how long closing takes void closeFile() { @@ -44,7 +45,7 @@ void closeFile() { //find() that reads and buffers data from file stream in 256-byte blocks. //Significantly faster, f.find(key) can take SECONDS for multi-kB files -bool bufferedFind(const char *target, bool fromStart = true) { +static bool bufferedFind(const char *target, bool fromStart = true) { #ifdef WLED_DEBUG_FS DEBUGFS_PRINT("Find "); DEBUGFS_PRINTLN(target); @@ -59,8 +60,8 @@ bool bufferedFind(const char *target, bool fromStart = true) { if (fromStart) f.seek(0); while (f.position() < f.size() -1) { - uint16_t bufsize = f.read(buf, FS_BUFSIZE); - uint16_t count = 0; + size_t bufsize = f.read(buf, FS_BUFSIZE); // better to use size_t instead if uint16_t + size_t count = 0; while (count < bufsize) { if(buf[count] != target[index]) index = 0; // reset index if any char does not match @@ -80,7 +81,7 @@ bool bufferedFind(const char *target, bool fromStart = true) { } //find empty spots in file stream in 256-byte blocks. -bool bufferedFindSpace(uint16_t targetLen, bool fromStart = true) { +static bool bufferedFindSpace(size_t targetLen, bool fromStart = true) { #ifdef WLED_DEBUG_FS DEBUGFS_PRINTF("Find %d spaces\n", targetLen); @@ -95,20 +96,20 @@ bool bufferedFindSpace(uint16_t targetLen, bool fromStart = true) { if (!f || !f.size()) return false; - uint16_t index = 0; + size_t index = 0; // better to use size_t instead if uint16_t byte buf[FS_BUFSIZE]; if (fromStart) f.seek(0); while (f.position() < f.size() -1) { - uint16_t bufsize = f.read(buf, FS_BUFSIZE); - uint16_t count = 0; - + size_t bufsize = f.read(buf, FS_BUFSIZE); + size_t count = 0; + while (count < bufsize) { if(buf[count] == ' ') { if(++index >= targetLen) { // return true if space long enough if (fromStart) { f.seek((f.position() - bufsize) + count +1 - targetLen); - knownLargestSpace = UINT16_MAX; //there may be larger spaces after, so we don't know + knownLargestSpace = MAX_SPACE; //there may be larger spaces after, so we don't know } DEBUGFS_PRINTF("Found at pos %d, took %d ms", f.position(), millis() - s); return true; @@ -116,7 +117,7 @@ bool bufferedFindSpace(uint16_t targetLen, bool fromStart = true) { } else { if (!fromStart) return false; if (index) { - if (knownLargestSpace < index || knownLargestSpace == UINT16_MAX) knownLargestSpace = index; + if (knownLargestSpace < index || (knownLargestSpace == MAX_SPACE)) knownLargestSpace = index; index = 0; // reset index if not space } } @@ -129,7 +130,7 @@ bool bufferedFindSpace(uint16_t targetLen, bool fromStart = true) { } //find the closing bracket corresponding to the opening bracket at the file pos when calling this function -bool bufferedFindObjectEnd() { +static bool bufferedFindObjectEnd() { #ifdef WLED_DEBUG_FS DEBUGFS_PRINTLN(F("Find obj end")); uint32_t s = millis(); @@ -142,9 +143,9 @@ bool bufferedFindObjectEnd() { byte buf[FS_BUFSIZE]; while (f.position() < f.size() -1) { - uint16_t bufsize = f.read(buf, FS_BUFSIZE); - uint16_t count = 0; - + size_t bufsize = f.read(buf, FS_BUFSIZE); // better to use size_t instead of uint16_t + size_t count = 0; + while (count < bufsize) { if (buf[count] == '{') objDepth++; if (buf[count] == '}') objDepth--; @@ -161,13 +162,13 @@ bool bufferedFindObjectEnd() { } //fills n bytes from current file pos with ' ' characters -void writeSpace(uint16_t l) +static void writeSpace(size_t l) { byte buf[FS_BUFSIZE]; memset(buf, ' ', FS_BUFSIZE); while (l > 0) { - uint16_t block = (l>FS_BUFSIZE) ? FS_BUFSIZE : l; + size_t block = (l>FS_BUFSIZE) ? FS_BUFSIZE : l; f.write(buf, block); l -= block; } @@ -194,7 +195,7 @@ bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint doCloseFile = true; return true; //nothing to append } - + //if there is enough empty space in file, insert there instead of appending if (!contentLen) contentLen = measureJson(*content); DEBUGFS_PRINTF("CLen %d\n", contentLen); @@ -211,18 +212,18 @@ bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint //permitted space for presets exceeded updateFSInfo(); - + if (f.size() + 9000 > (fsBytesTotal - fsBytesUsed)) { //make sure there is enough space to at least copy the file once errorFlag = ERR_FS_QUOTA; doCloseFile = true; return false; } - + //check if last character in file is '}' (typical) uint32_t eof = f.size() -1; f.seek(eof, SeekSet); if (f.read() == '}') pos = eof; - + if (pos == 0) //not found { DEBUGFS_PRINTLN("not }"); @@ -270,24 +271,24 @@ bool writeObjectToFile(const char* file, const char* key, JsonDocument* content) s = millis(); #endif - uint32_t pos = 0; + size_t pos = 0; f = WLED_FS.open(file, "r+"); if (!f && !WLED_FS.exists(file)) f = WLED_FS.open(file, "w+"); if (!f) { DEBUGFS_PRINTLN(F("Failed to open!")); return false; } - + if (!bufferedFind(key)) //key does not exist in file { return appendObjectToFile(key, content, s); - } - + } + //an object with this key already exists, replace or delete it pos = f.position(); //measure out end of old object bufferedFindObjectEnd(); - uint32_t pos2 = f.position(); + size_t pos2 = f.position(); uint32_t oldLen = pos2 - pos; DEBUGFS_PRINTF("Old obj len %d\n", oldLen); @@ -297,8 +298,8 @@ bool writeObjectToFile(const char* file, const char* key, JsonDocument* content) //2. The new content is smaller than the old, overwrite and fill diff with spaces //3. The new content is larger than the old, but smaller than old + trailing spaces, overwrite with new //4. The new content is larger than old + trailing spaces, delete old and append - - uint32_t contentLen = 0; + + size_t contentLen = 0; if (!content->isNull()) contentLen = measureJson(*content); if (contentLen && contentLen <= oldLen) { //replace and fill diff with spaces @@ -375,15 +376,15 @@ void updateFSInfo() { //Un-comment any file types you need -String getContentType(AsyncWebServerRequest* request, String filename){ +static String getContentType(AsyncWebServerRequest* request, String filename){ if(request->hasArg("download")) return "application/octet-stream"; else if(filename.endsWith(".htm")) return "text/html"; else if(filename.endsWith(".html")) return "text/html"; else if(filename.endsWith(".css")) return "text/css"; -// else if(filename.endsWith(".js")) return "application/javascript"; + else if(filename.endsWith(".js")) return "application/javascript"; else if(filename.endsWith(".json")) return "application/json"; else if(filename.endsWith(".png")) return "image/png"; -// else if(filename.endsWith(".gif")) return "image/gif"; + else if(filename.endsWith(".gif")) return "image/gif"; else if(filename.endsWith(".jpg")) return "image/jpeg"; else if(filename.endsWith(".ico")) return "image/x-icon"; // else if(filename.endsWith(".xml")) return "text/xml"; @@ -394,7 +395,7 @@ String getContentType(AsyncWebServerRequest* request, String filename){ } bool handleFileRead(AsyncWebServerRequest* request, String path){ - DEBUG_PRINTLN("FileRead: " + path); + DEBUG_PRINTLN("WS FileRead: " + path); if(path.endsWith("/")) path += "index.htm"; if(path.indexOf("sec") > -1) return false; String contentType = getContentType(request, path); diff --git a/wled00/html_cpal.h b/wled00/html_cpal.h new file mode 100644 index 0000000000..43000649bd --- /dev/null +++ b/wled00/html_cpal.h @@ -0,0 +1,323 @@ +/* + * Binary array for the Web UI. + * gzip is used for smaller size and improved speeds. + * + * Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui + * to find out how to easily modify the web UI source! + */ + +// Autogenerated from wled00/data/cpal/cpal.htm, do not edit!! +const uint16_t PAGE_cpal_L = 4970; +const uint8_t PAGE_cpal[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x13, 0xbd, 0x3b, 0xfd, 0x77, 0xdb, 0x36, + 0x92, 0xbf, 0xe7, 0xaf, 0x40, 0x98, 0xd4, 0x21, 0x6b, 0x8a, 0x22, 0x29, 0x5b, 0xb2, 0x25, 0xd1, + 0xdd, 0xd4, 0xc9, 0x9e, 0x73, 0xcf, 0x6e, 0xf2, 0x36, 0x3e, 0xb7, 0x3d, 0x9f, 0xf7, 0x99, 0x26, + 0x21, 0x89, 0x0d, 0x45, 0x70, 0x41, 0x48, 0xb6, 0x2b, 0xeb, 0x7f, 0xbf, 0x19, 0x00, 0xa4, 0x48, + 0x7d, 0x38, 0xc9, 0x75, 0xdf, 0xf5, 0xf9, 0x45, 0x20, 0x30, 0x18, 0x0c, 0x06, 0xf3, 0x89, 0x41, + 0x87, 0x2f, 0xdf, 0x7d, 0x3c, 0xbd, 0xfc, 0xfd, 0xd3, 0x7b, 0x32, 0x11, 0xd3, 0xf4, 0x84, 0x0c, + 0xcb, 0x1f, 0x1a, 0xc6, 0xf0, 0x33, 0xa5, 0x22, 0x24, 0x59, 0x38, 0xa5, 0x81, 0x31, 0x4f, 0xe8, + 0x7d, 0xce, 0xb8, 0x30, 0xc8, 0x8b, 0x88, 0x65, 0x82, 0x66, 0x22, 0x30, 0xee, 0x93, 0x58, 0x4c, + 0x82, 0x98, 0xce, 0x93, 0x88, 0xb6, 0xe4, 0x87, 0x9d, 0x64, 0x89, 0x48, 0xc2, 0xb4, 0x55, 0x44, + 0x61, 0x4a, 0x03, 0xcf, 0x9e, 0x42, 0xc7, 0x74, 0x36, 0x2d, 0xbf, 0x8d, 0x12, 0xe9, 0x8b, 0x89, + 0x10, 0x79, 0x8b, 0xfe, 0x6b, 0x96, 0xcc, 0x03, 0xe3, 0x34, 0x8c, 0x26, 0xb4, 0x75, 0x0a, 0x68, + 0x39, 0x4b, 0x0d, 0x52, 0xe1, 0xcf, 0x58, 0x2b, 0xc2, 0x21, 0x9b, 0x40, 0xab, 0x10, 0x8c, 0x43, + 0x6b, 0x3a, 0x2b, 0x44, 0x8b, 0xd3, 0x79, 0x98, 0x26, 0x71, 0x28, 0xe8, 0x76, 0x84, 0x9f, 0x78, + 0x38, 0x9e, 0x86, 0x5b, 0x30, 0x55, 0xe0, 0x75, 0xe8, 0xf7, 0x0f, 0x79, 0xc2, 0x69, 0x51, 0x03, + 0x77, 0x01, 0xee, 0xc5, 0x50, 0x24, 0x22, 0xa5, 0x27, 0xbf, 0x9e, 0xbf, 0x7f, 0x47, 0x4e, 0x61, + 0x55, 0x36, 0x25, 0x9f, 0x60, 0x13, 0x42, 0x50, 0xf2, 0x3e, 0x4e, 0x80, 0x9a, 0x61, 0x5b, 0x41, + 0x90, 0x61, 0x11, 0xf1, 0x24, 0x17, 0x44, 0x3c, 0xe6, 0xc0, 0x29, 0x41, 0x1f, 0x44, 0xfb, 0x8f, + 0x70, 0x1e, 0xaa, 0x5e, 0xe3, 0xe4, 0xc5, 0x68, 0x96, 0x45, 0x22, 0x61, 0x19, 0x19, 0x7f, 0x88, + 0x4d, 0x6a, 0x2d, 0x38, 0x15, 0x33, 0x9e, 0x91, 0xd8, 0x19, 0x53, 0xf1, 0x3e, 0xa5, 0x53, 0x58, + 0xf3, 0xe7, 0x47, 0x39, 0xb4, 0xac, 0x40, 0xa3, 0xf7, 0x0d, 0xc8, 0x88, 0x53, 0xd8, 0xad, 0x06, + 0x46, 0xc0, 0x79, 0xc8, 0x49, 0x1c, 0xc4, 0x2c, 0x9a, 0x61, 0xcf, 0x8b, 0x61, 0x5b, 0xad, 0x86, + 0xc4, 0x88, 0x47, 0x20, 0xea, 0xc5, 0x1d, 0x8b, 0x1f, 0x17, 0x23, 0xd8, 0x51, 0x6b, 0x14, 0x4e, + 0x93, 0xf4, 0xb1, 0xff, 0x96, 0xc3, 0xc1, 0xd8, 0x45, 0x98, 0x15, 0xad, 0x82, 0xf2, 0x64, 0x34, + 0xb8, 0x0b, 0xa3, 0x2f, 0x63, 0xce, 0x66, 0x59, 0xdc, 0x8a, 0x58, 0xca, 0x78, 0xff, 0x95, 0xe7, + 0x79, 0x03, 0x39, 0xa5, 0x48, 0xfe, 0xa4, 0x7d, 0xaf, 0x9b, 0x3f, 0x0c, 0xf4, 0x48, 0x1c, 0xc7, + 0x83, 0x69, 0xc8, 0xc7, 0x49, 0xd6, 0x77, 0x89, 0xe7, 0xc2, 0x40, 0x9a, 0x64, 0xb4, 0x35, 0xa1, + 0xc9, 0x78, 0x22, 0xfa, 0xce, 0xe1, 0xf2, 0x55, 0x1e, 0x72, 0x20, 0xa4, 0x85, 0x3c, 0x0c, 0x61, + 0x88, 0x2f, 0x72, 0x56, 0x24, 0xb8, 0x95, 0x3e, 0xa7, 0x69, 0x28, 0x92, 0x39, 0x1d, 0x48, 0x11, + 0xe9, 0x7b, 0xae, 0xfb, 0xc3, 0x40, 0x4f, 0xf4, 0x01, 0xd3, 0xf2, 0xd5, 0x1d, 0x13, 0xc0, 0xdd, + 0xd3, 0xcd, 0x99, 0xe1, 0x5d, 0xc1, 0xd2, 0x99, 0xa0, 0x7a, 0xe9, 0x96, 0x60, 0x79, 0xff, 0x50, + 0x4e, 0x19, 0xf3, 0x30, 0x4e, 0x70, 0xbd, 0x3b, 0xf6, 0xb0, 0xd8, 0xc4, 0x8b, 0xed, 0xa5, 0x23, + 0x69, 0x6f, 0xc1, 0xdc, 0x2f, 0x94, 0xdb, 0xfa, 0x2b, 0x4f, 0x22, 0xf8, 0xd2, 0x9d, 0x5b, 0x56, + 0xba, 0x63, 0x3c, 0x86, 0x71, 0x44, 0x3f, 0x2b, 0xfa, 0x1d, 0xd8, 0xe8, 0x06, 0x9b, 0x8a, 0x24, + 0x9d, 0x53, 0xae, 0x21, 0xfb, 0x7e, 0xfe, 0x40, 0x60, 0x6e, 0x12, 0x13, 0x3e, 0xbe, 0x0b, 0xcd, + 0xee, 0x91, 0xad, 0xfe, 0x9c, 0x43, 0x6b, 0xf0, 0x67, 0x2b, 0xc9, 0x62, 0xfa, 0xd0, 0xf7, 0x9b, + 0xb4, 0x2c, 0x34, 0x95, 0x1d, 0xe4, 0xa3, 0x22, 0xbe, 0x07, 0x2d, 0xb5, 0xbb, 0x1f, 0x06, 0x82, + 0xc3, 0x19, 0x8d, 0x18, 0x9f, 0xf6, 0x65, 0x0b, 0x98, 0x47, 0x7f, 0x37, 0x5b, 0x30, 0x62, 0x01, + 0xc8, 0x2c, 0x9a, 0xb4, 0x42, 0x29, 0x22, 0xfd, 0x8c, 0x65, 0x74, 0xb9, 0x75, 0x5b, 0x1a, 0x7f, + 0x6f, 0x03, 0xbd, 0x77, 0x88, 0x7c, 0x89, 0x29, 0x88, 0x31, 0xdd, 0xcd, 0x03, 0x3d, 0xfd, 0xb0, + 0x9a, 0x8e, 0xad, 0x6f, 0x60, 0xcc, 0xab, 0xd1, 0x68, 0x54, 0xb2, 0xa5, 0x53, 0xb1, 0xe5, 0xd5, + 0xf1, 0x9d, 0x7f, 0xe4, 0x1f, 0xc9, 0xf5, 0x7d, 0x1f, 0xf6, 0xb7, 0xc1, 0x15, 0x45, 0xfc, 0x6e, + 0x42, 0xbc, 0x8a, 0x10, 0xaf, 0x22, 0x44, 0x36, 0xcb, 0x2d, 0x55, 0x28, 0xbd, 0x92, 0xcc, 0x9a, + 0x40, 0x6f, 0x15, 0xf3, 0xa5, 0x73, 0x37, 0x03, 0xa1, 0xcb, 0xa2, 0x34, 0x2c, 0x8a, 0x45, 0x1e, + 0xc6, 0x71, 0x92, 0x8d, 0xfb, 0x6e, 0x25, 0xe3, 0x03, 0x38, 0x61, 0x91, 0x80, 0xd1, 0x6a, 0x81, + 0xa1, 0x19, 0x67, 0x7d, 0x25, 0xa2, 0x3b, 0x70, 0xad, 0x0b, 0x30, 0x29, 0xf2, 0x30, 0x5b, 0xc4, + 0x49, 0x91, 0xa7, 0xe1, 0x63, 0x3f, 0xc9, 0xa4, 0xaa, 0x8c, 0x52, 0xfa, 0x30, 0x90, 0xc8, 0x5a, + 0x89, 0xa0, 0xd3, 0xa2, 0x1f, 0x81, 0xf8, 0x82, 0x18, 0xd5, 0x58, 0x57, 0x53, 0x3d, 0x90, 0xaa, + 0x75, 0x12, 0xa6, 0x49, 0x1c, 0xa7, 0x74, 0xf9, 0x2a, 0xc9, 0x46, 0xac, 0x42, 0x6e, 0x18, 0x03, + 0xb4, 0x37, 0x1a, 0xe4, 0xab, 0x28, 0x37, 0x75, 0xb2, 0xa6, 0x59, 0x1b, 0x6a, 0x0d, 0x5c, 0xba, + 0xe7, 0x61, 0x5e, 0xd7, 0xaf, 0xca, 0x06, 0x84, 0x33, 0xc1, 0x96, 0x7f, 0x9b, 0xd2, 0x38, 0x09, + 0x89, 0x09, 0x56, 0x5e, 0xd9, 0xff, 0xfe, 0x91, 0x0b, 0x48, 0xac, 0x45, 0x7d, 0x9e, 0xec, 0x5a, + 0x2e, 0x9d, 0x5c, 0x19, 0xcf, 0x45, 0x5d, 0xf5, 0xcb, 0xce, 0xff, 0xd0, 0xea, 0x5c, 0x2c, 0x90, + 0x4d, 0x70, 0x8c, 0x35, 0xa0, 0x4d, 0xc9, 0xab, 0xa6, 0x15, 0x17, 0xc0, 0xef, 0xc5, 0x9a, 0x71, + 0xa8, 0x99, 0x19, 0x09, 0x78, 0xc9, 0xf2, 0x72, 0xcd, 0x51, 0xa2, 0x0c, 0x14, 0xac, 0xf4, 0x17, + 0xd9, 0xb6, 0xc6, 0x26, 0x58, 0xa6, 0xdc, 0xc2, 0x27, 0x69, 0x07, 0xab, 0x03, 0xda, 0x75, 0xec, + 0x5b, 0x28, 0x5a, 0x3f, 0x8a, 0x7f, 0x2b, 0x85, 0x4a, 0xdc, 0x8b, 0x77, 0xc9, 0x7c, 0xab, 0x60, + 0xea, 0xb5, 0x53, 0x3a, 0x6a, 0xe8, 0xbd, 0x3c, 0x23, 0x38, 0x63, 0xf1, 0x19, 0x24, 0xda, 0x76, + 0x0a, 0x9a, 0xc5, 0xd8, 0x5a, 0x44, 0x33, 0x5e, 0x00, 0x25, 0x39, 0x4b, 0x90, 0xae, 0xe5, 0xc4, + 0x5b, 0xd4, 0xe8, 0x71, 0xba, 0x9c, 0x4e, 0x97, 0xe8, 0x92, 0xa4, 0x27, 0x22, 0xc3, 0xb6, 0x0e, + 0x27, 0xd0, 0x25, 0xc1, 0x4f, 0x9c, 0xcc, 0x49, 0x12, 0x43, 0xf8, 0x00, 0x32, 0x02, 0x4e, 0x17, + 0x35, 0x50, 0x7f, 0xe8, 0xc1, 0x17, 0x72, 0x62, 0x60, 0x34, 0x78, 0xf8, 0x07, 0xb8, 0xe0, 0x64, + 0xf4, 0x58, 0x72, 0x4b, 0xb3, 0x04, 0xa7, 0x4c, 0xbc, 0xed, 0x33, 0x36, 0xb9, 0x8e, 0xd0, 0xc5, + 0x7c, 0x5c, 0x81, 0xab, 0x5d, 0x76, 0xd0, 0xcf, 0x95, 0x26, 0x19, 0xdb, 0x9a, 0x19, 0x5c, 0xf6, + 0x40, 0x87, 0x41, 0x30, 0xfa, 0xf9, 0x99, 0x3d, 0x40, 0x64, 0x40, 0x5c, 0xd2, 0xf1, 0xe1, 0xcf, + 0x38, 0x19, 0xe6, 0xa1, 0x98, 0x90, 0x17, 0xa3, 0x24, 0x4d, 0x03, 0xe3, 0x95, 0xeb, 0x76, 0xe0, + 0x58, 0x0c, 0xf0, 0xc9, 0xc6, 0x45, 0x97, 0xf8, 0xfe, 0xe4, 0x68, 0x7e, 0x70, 0xd6, 0xfd, 0xf3, + 0xc2, 0x3b, 0x20, 0xde, 0xc1, 0xe4, 0x60, 0x7e, 0x34, 0x69, 0x1d, 0xc0, 0xd7, 0x11, 0x38, 0xcf, + 0xea, 0xcb, 0xf7, 0x49, 0x17, 0xe1, 0x26, 0xad, 0xa3, 0x3f, 0x8d, 0xf6, 0x09, 0x30, 0x6c, 0x3e, + 0x3e, 0x79, 0x01, 0x24, 0x02, 0x8b, 0x25, 0x87, 0x90, 0x6f, 0xc6, 0xb3, 0x11, 0x08, 0x82, 0x4a, + 0x0e, 0x7b, 0xf8, 0x2f, 0x30, 0xaf, 0x64, 0x21, 0x4e, 0x5f, 0x77, 0xc9, 0x46, 0x8d, 0xf9, 0x75, + 0x07, 0x0a, 0x7b, 0xd1, 0x53, 0xeb, 0x18, 0xbe, 0xef, 0x10, 0x4a, 0xbc, 0xa5, 0x66, 0x62, 0x9c, + 0xa8, 0x4e, 0xb6, 0xae, 0xab, 0x6b, 0x90, 0xa0, 0x9a, 0x95, 0x00, 0xe8, 0x4f, 0xd8, 0xff, 0xe9, + 0x8c, 0x23, 0xdd, 0xe9, 0x23, 0x49, 0x32, 0x32, 0x2b, 0x28, 0x89, 0xd4, 0xde, 0x4b, 0x44, 0x64, + 0x8d, 0xda, 0xbf, 0x4e, 0x34, 0x9a, 0x54, 0xb9, 0x72, 0x0a, 0x9e, 0x88, 0x40, 0xf4, 0x25, 0x26, + 0x94, 0x94, 0x1c, 0x22, 0x54, 0xf2, 0x9a, 0x08, 0x46, 0xc0, 0x4d, 0x90, 0x8c, 0xde, 0x13, 0xa9, + 0x87, 0xa4, 0x00, 0xef, 0x06, 0x81, 0x05, 0x02, 0xab, 0x19, 0xb2, 0x9b, 0xc6, 0x04, 0x58, 0x4a, + 0xee, 0x68, 0xca, 0xee, 0x65, 0xaf, 0x02, 0xc3, 0xe9, 0xd1, 0x24, 0xcc, 0xc6, 0x94, 0x24, 0xa2, + 0x50, 0xa0, 0x8e, 0x5e, 0x10, 0xa1, 0x9a, 0xf3, 0xc0, 0x9b, 0x81, 0xe5, 0xc7, 0x55, 0xcd, 0x30, + 0x8b, 0x31, 0x30, 0x1d, 0x25, 0x7c, 0x6a, 0x21, 0x12, 0xe5, 0xbc, 0x1d, 0xf2, 0x31, 0x8b, 0x28, + 0x19, 0x41, 0x78, 0x5d, 0x4c, 0x68, 0x6c, 0x03, 0x17, 0x4b, 0x4c, 0x21, 0xe7, 0x88, 0x21, 0xc2, + 0x6d, 0x30, 0x32, 0xcb, 0x53, 0x16, 0xc6, 0x80, 0x10, 0xda, 0x38, 0x1a, 0xd3, 0x22, 0xc1, 0xb5, + 0x8a, 0x94, 0x09, 0x87, 0x5c, 0x32, 0xb9, 0x3b, 0x42, 0x1f, 0x12, 0xe0, 0x51, 0x36, 0x2e, 0x79, + 0x5c, 0xc7, 0x97, 0xd3, 0x2c, 0x4a, 0x52, 0x89, 0xd0, 0x81, 0xa8, 0x78, 0x93, 0xe9, 0xdf, 0xcf, + 0x73, 0x29, 0x9d, 0x85, 0x00, 0x43, 0x15, 0x7d, 0xaa, 0xe4, 0xe5, 0x2b, 0xe2, 0x82, 0xe0, 0x3b, + 0x45, 0xe6, 0xed, 0x3c, 0x4c, 0xd2, 0xf0, 0x2e, 0x05, 0x6e, 0x4b, 0xac, 0x5f, 0x93, 0x15, 0xf9, + 0x33, 0x6c, 0x6b, 0x83, 0xa4, 0xc3, 0xf7, 0x17, 0xbb, 0xe2, 0x77, 0x8c, 0xb5, 0x4b, 0x69, 0x40, + 0x2b, 0x80, 0x61, 0x7c, 0x53, 0x81, 0x2c, 0x3b, 0x82, 0x15, 0xa3, 0xa0, 0xe5, 0xd9, 0xf9, 0xc3, + 0x29, 0x4b, 0x83, 0xc5, 0xd2, 0x16, 0xfa, 0x97, 0xd3, 0x48, 0x04, 0xb5, 0xe9, 0x18, 0xf5, 0xff, + 0x8c, 0x21, 0x04, 0xf0, 0x1b, 0xce, 0x1f, 0x3a, 0xff, 0x01, 0x10, 0xa6, 0x65, 0x97, 0x30, 0xe7, + 0x34, 0x1b, 0x43, 0x5e, 0x85, 0xf3, 0x1c, 0x95, 0x55, 0x4d, 0x3f, 0x8e, 0x46, 0x45, 0x70, 0x01, + 0xf6, 0xc6, 0x91, 0xc1, 0x87, 0xd9, 0x04, 0x6d, 0xfb, 0x87, 0xdd, 0xb6, 0x6f, 0xb5, 0x0e, 0x6d, + 0xbd, 0xed, 0xb7, 0x9c, 0x87, 0x8f, 0xc1, 0xf5, 0x4d, 0xf9, 0xfd, 0x0b, 0xa6, 0x70, 0xf0, 0x09, + 0xf6, 0xe5, 0x73, 0x38, 0xa7, 0xc1, 0x1b, 0x69, 0x05, 0x1b, 0x46, 0xd0, 0x3f, 0x5c, 0x19, 0x41, + 0x6c, 0xaf, 0xd9, 0x3c, 0xff, 0x00, 0xfe, 0x4a, 0x9b, 0x27, 0x4d, 0x1e, 0x7a, 0x21, 0x69, 0xed, + 0x7c, 0xdf, 0xf6, 0xfc, 0xb7, 0x9e, 0x6b, 0x7b, 0x08, 0x08, 0x3f, 0xc4, 0xf3, 0x6d, 0xbf, 0xd9, + 0xb3, 0x15, 0xa4, 0x09, 0x81, 0x20, 0x17, 0x3d, 0xf8, 0xe7, 0x1c, 0xc6, 0xbc, 0xde, 0x95, 0x77, + 0x70, 0xe6, 0x75, 0xaf, 0x3c, 0xf7, 0xcc, 0xf3, 0xaf, 0x7a, 0xe7, 0x38, 0xf0, 0xdf, 0x95, 0x8d, + 0x7c, 0x83, 0x3b, 0x41, 0x13, 0xf8, 0xef, 0xdd, 0x09, 0x12, 0x75, 0xda, 0x75, 0x0e, 0x7a, 0xb6, + 0x0f, 0x14, 0x63, 0x43, 0x12, 0x7e, 0x8a, 0xf4, 0x38, 0x87, 0x1d, 0xa2, 0x86, 0x7c, 0xb5, 0xbf, + 0x53, 0xd9, 0x87, 0x9f, 0x7e, 0x39, 0xee, 0x2b, 0x68, 0x3d, 0x55, 0x8f, 0x4b, 0xe8, 0x0b, 0xef, + 0xd0, 0xf1, 0xec, 0x9e, 0xe3, 0xf6, 0x4e, 0xa1, 0xe5, 0x1f, 0xc8, 0x26, 0x81, 0x66, 0xe7, 0x08, + 0x9a, 0x9e, 0x8f, 0xcd, 0x43, 0x68, 0xf9, 0x9d, 0x73, 0xaf, 0xeb, 0xf4, 0x7a, 0xf6, 0x91, 0x73, + 0x08, 0x0b, 0xc0, 0x4f, 0x0f, 0xc6, 0x7a, 0xf6, 0xb1, 0x04, 0x97, 0x23, 0xc7, 0x8e, 0x7f, 0x74, + 0x0e, 0xe0, 0xd0, 0xf4, 0x5c, 0xd9, 0xee, 0x00, 0x10, 0x40, 0xe2, 0xdc, 0x03, 0x6c, 0x22, 0x9a, + 0x53, 0x68, 0x1e, 0xf9, 0x1a, 0xf7, 0x81, 0x73, 0xdc, 0xad, 0x56, 0x54, 0x64, 0x5c, 0xc0, 0x2c, + 0xaf, 0x03, 0xb3, 0x8e, 0x3c, 0x44, 0xe6, 0x1d, 0x23, 0xb2, 0xa3, 0xde, 0xf9, 0x31, 0xf6, 0xc2, + 0x42, 0xc7, 0x9d, 0x33, 0x04, 0xbb, 0x42, 0x34, 0xbd, 0xf3, 0x15, 0x70, 0xed, 0x0c, 0x06, 0x55, + 0xae, 0x0a, 0x92, 0xfa, 0x71, 0x64, 0x62, 0xb6, 0xfa, 0xff, 0x26, 0xe9, 0xb5, 0x44, 0x39, 0x4d, + 0xbe, 0x7c, 0xcc, 0xca, 0xe8, 0x4b, 0x25, 0xcd, 0x53, 0x36, 0xa7, 0x97, 0x3c, 0x2c, 0x26, 0x51, + 0x98, 0x41, 0x8f, 0x0d, 0x76, 0xfb, 0xd4, 0xac, 0x21, 0xa5, 0x0e, 0x83, 0x65, 0xa8, 0xf8, 0xad, + 0xdd, 0x44, 0xff, 0x23, 0xa0, 0xb7, 0x6a, 0x49, 0xb8, 0x9c, 0x47, 0x6d, 0x11, 0x18, 0x86, 0xb5, + 0x00, 0x4d, 0x22, 0x1c, 0x35, 0x9c, 0x05, 0x2f, 0x3d, 0x08, 0xc5, 0xb2, 0x42, 0x90, 0xb0, 0xb1, + 0xdd, 0x7f, 0xcd, 0x28, 0x7f, 0xfc, 0x0c, 0xf6, 0x39, 0x02, 0xcb, 0xfd, 0x36, 0x4d, 0x4d, 0xa3, + 0x91, 0xf6, 0x19, 0xd6, 0x20, 0x19, 0x99, 0xa1, 0x03, 0xa9, 0xdd, 0xfb, 0x30, 0x9a, 0x98, 0xa6, + 0xb0, 0xb9, 0x15, 0x9c, 0x2c, 0x04, 0xf2, 0xe9, 0xad, 0x10, 0x3c, 0x81, 0x20, 0x8d, 0x9a, 0x46, + 0x1c, 0x8a, 0xb0, 0x25, 0xf8, 0x8c, 0x42, 0x50, 0x67, 0x58, 0x41, 0x40, 0xf7, 0xf6, 0x4c, 0x58, + 0xd3, 0xb5, 0x96, 0xb0, 0x13, 0x27, 0x95, 0x94, 0x9e, 0x78, 0xbd, 0xb2, 0xd7, 0x66, 0x96, 0xba, + 0x26, 0x40, 0xec, 0xf4, 0xc4, 0xdd, 0xdb, 0xa3, 0x43, 0xff, 0xf0, 0xd0, 0x82, 0x65, 0x4c, 0xb4, + 0x5c, 0x59, 0xe0, 0x0d, 0xb2, 0x61, 0xe0, 0x75, 0xf7, 0xf6, 0xf8, 0x10, 0x9a, 0xfb, 0xfb, 0x96, + 0x34, 0x60, 0x92, 0xb4, 0x0b, 0x45, 0xd9, 0x7e, 0x66, 0x3d, 0x3d, 0x99, 0x3c, 0xc8, 0xac, 0x01, + 0x4d, 0xc1, 0xe3, 0xf2, 0x80, 0x0e, 0x0c, 0x23, 0x08, 0x04, 0x2c, 0x02, 0xbb, 0x7f, 0x65, 0xec, + 0x9b, 0x5e, 0xb7, 0xd7, 0xeb, 0xf9, 0xde, 0xe1, 0x8f, 0x8a, 0x8f, 0xe0, 0x96, 0xd8, 0xd4, 0xb4, + 0x86, 0x43, 0xd7, 0x72, 0x04, 0xfb, 0x0c, 0xc4, 0x67, 0x63, 0x80, 0xb1, 0x20, 0x14, 0x8e, 0x3f, + 0x8b, 0x90, 0x0b, 0xb3, 0x6b, 0x1b, 0xae, 0x61, 0x59, 0x9a, 0x53, 0x69, 0x10, 0xbd, 0x37, 0x0d, + 0x0c, 0x57, 0x80, 0x0d, 0xa9, 0x23, 0x2d, 0xb8, 0x34, 0x4b, 0x46, 0x83, 0x45, 0x76, 0xea, 0xa0, + 0xb1, 0x6f, 0xd0, 0xc6, 0x57, 0x0b, 0x58, 0x30, 0x5e, 0xec, 0x66, 0x96, 0x4d, 0x9f, 0x01, 0x00, + 0x9c, 0x86, 0x2d, 0x76, 0x00, 0x28, 0x79, 0x30, 0x94, 0xfc, 0x21, 0x0c, 0x1c, 0xfd, 0xfb, 0x39, + 0x0a, 0x06, 0x38, 0x46, 0x0a, 0xb1, 0x13, 0xf0, 0x0b, 0x3d, 0xa2, 0x61, 0x43, 0x28, 0x92, 0xff, + 0x7d, 0xc6, 0xc1, 0x33, 0xf2, 0x4f, 0x9c, 0xe5, 0x12, 0x1f, 0x9a, 0x1f, 0x07, 0x63, 0xe7, 0xe7, + 0x25, 0xf7, 0x47, 0x6a, 0xed, 0xcb, 0x05, 0xf6, 0x0d, 0x30, 0x4b, 0x9a, 0x31, 0x89, 0x64, 0x4c, + 0x92, 0xe5, 0x33, 0x81, 0x02, 0xe2, 0x28, 0x27, 0x24, 0x19, 0x60, 0xd8, 0x89, 0x33, 0x0f, 0xd3, + 0x19, 0x0d, 0x04, 0xb4, 0x36, 0x58, 0xa6, 0xd2, 0x66, 0x04, 0xaa, 0x58, 0xf6, 0x49, 0x75, 0x35, + 0x59, 0x96, 0x6c, 0xd9, 0x8c, 0x5a, 0xcf, 0x9e, 0xe5, 0x78, 0x89, 0x56, 0x2a, 0xcf, 0x76, 0x50, + 0xbd, 0xef, 0x28, 0x3f, 0x4d, 0xbf, 0x94, 0xa7, 0x59, 0xd4, 0x4f, 0xb3, 0xd8, 0x45, 0x5a, 0x75, + 0xa8, 0xc5, 0x3a, 0x85, 0x5b, 0x8f, 0xb6, 0x78, 0x66, 0x71, 0x96, 0xe2, 0xea, 0x00, 0x52, 0xe3, + 0x75, 0x9d, 0xf1, 0x40, 0xf9, 0x8e, 0x11, 0x4d, 0x71, 0x5c, 0xa7, 0x98, 0xa3, 0x96, 0x70, 0xd4, + 0x12, 0x90, 0xef, 0xb8, 0x4e, 0x7e, 0xe3, 0x6a, 0xc4, 0xb0, 0x63, 0x49, 0xb8, 0xea, 0xdc, 0x4a, + 0x73, 0xbc, 0x9b, 0x66, 0x0a, 0xaa, 0xad, 0x66, 0x9e, 0xe2, 0xc6, 0xf1, 0xd2, 0x0e, 0xe1, 0x77, + 0x90, 0xb9, 0x92, 0xa3, 0xd5, 0x1d, 0x83, 0x9c, 0x17, 0x68, 0x19, 0xa8, 0xf6, 0xbe, 0x6b, 0xbc, + 0x6e, 0x89, 0xc2, 0x1c, 0xa2, 0xb6, 0xf8, 0x74, 0x92, 0xa4, 0xb1, 0x99, 0x58, 0x3b, 0x87, 0xd2, + 0xdd, 0x43, 0xa0, 0x04, 0xee, 0xcb, 0x80, 0xef, 0xed, 0x01, 0x93, 0xe4, 0xef, 0x2e, 0xc0, 0xd8, + 0xb2, 0xeb, 0xec, 0x9c, 0x86, 0x5f, 0xe8, 0x05, 0x7d, 0xc7, 0xc3, 0xb1, 0x89, 0x56, 0x06, 0xd5, + 0xd9, 0x82, 0x73, 0xa3, 0xe2, 0x92, 0xb1, 0x54, 0x24, 0xb9, 0xe2, 0x62, 0x7d, 0xac, 0x29, 0x83, + 0x66, 0xcd, 0xfc, 0xae, 0x8f, 0x2c, 0xd4, 0x51, 0xd2, 0xef, 0x34, 0xba, 0x1b, 0x11, 0x19, 0xdd, + 0x30, 0xc1, 0x0a, 0x31, 0x93, 0x91, 0x1d, 0xbd, 0xe6, 0x37, 0x40, 0x99, 0xc3, 0x29, 0x84, 0xb3, + 0x11, 0x6d, 0x1a, 0x4a, 0xbb, 0xa1, 0x67, 0x96, 0xa5, 0x78, 0x3f, 0xf8, 0xbe, 0x79, 0xba, 0x0f, + 0x66, 0x6f, 0x3f, 0x51, 0x66, 0x4b, 0x5c, 0xcf, 0x0f, 0x3e, 0x63, 0xe4, 0x98, 0x55, 0xb9, 0x27, + 0x09, 0xfb, 0x9c, 0x7b, 0xb1, 0xb3, 0xaf, 0xd8, 0xac, 0x50, 0x33, 0xf0, 0x3a, 0xbb, 0x81, 0xb5, + 0x91, 0x85, 0xd7, 0x21, 0xb4, 0x96, 0x2b, 0xd1, 0x51, 0xca, 0x10, 0x18, 0x78, 0x9b, 0x10, 0xf2, + 0x56, 0xd9, 0x6d, 0x42, 0xc6, 0x21, 0x53, 0x68, 0xc3, 0xfe, 0x78, 0xf7, 0x07, 0xba, 0x78, 0xe8, + 0xe4, 0x09, 0x2d, 0x4c, 0x89, 0xcf, 0x5a, 0x1d, 0xc2, 0x35, 0xb8, 0xd8, 0x1b, 0x3c, 0x86, 0x26, + 0xc6, 0xfd, 0xe0, 0xd6, 0x26, 0xaf, 0x17, 0x62, 0x09, 0xff, 0xd0, 0x65, 0xfe, 0x70, 0xbb, 0xb1, + 0xe6, 0x7e, 0x60, 0x58, 0x46, 0x43, 0x84, 0xd7, 0x79, 0x16, 0x34, 0x27, 0xac, 0x64, 0x6b, 0xcd, + 0x8c, 0x63, 0xdc, 0x40, 0x1d, 0xec, 0xc4, 0xaf, 0x70, 0x1c, 0x22, 0x50, 0x5d, 0x16, 0x95, 0x05, + 0xda, 0x1e, 0x5e, 0x6c, 0x99, 0x68, 0x4b, 0x89, 0x70, 0x0a, 0x1e, 0xe9, 0x3b, 0xfb, 0x86, 0x64, + 0x54, 0x42, 0x81, 0x32, 0x20, 0xcd, 0x45, 0x63, 0xa9, 0x7c, 0xc7, 0x4a, 0x73, 0x69, 0xa3, 0x9f, + 0x25, 0xb3, 0xa6, 0x7e, 0x80, 0x00, 0xdd, 0xbf, 0x08, 0x5c, 0x9b, 0x7f, 0x53, 0x54, 0xc6, 0x02, + 0xee, 0xc8, 0x03, 0xb3, 0x43, 0x68, 0x49, 0xab, 0x9a, 0x05, 0xac, 0x15, 0xee, 0x7b, 0xab, 0x50, + 0x2f, 0xdd, 0x49, 0xd7, 0x00, 0x17, 0xe3, 0x81, 0x49, 0x03, 0xfa, 0xf4, 0x74, 0x0f, 0x09, 0x2c, + 0xbb, 0x77, 0xd4, 0x88, 0x74, 0x69, 0x40, 0x34, 0x04, 0x05, 0xc5, 0xaf, 0x89, 0x98, 0x98, 0x86, + 0xbc, 0xc6, 0x46, 0x3b, 0xfc, 0xf4, 0x44, 0x9d, 0x9c, 0x4b, 0xb0, 0x77, 0x74, 0x14, 0xce, 0x52, + 0xa4, 0x43, 0x04, 0xfc, 0x27, 0xea, 0x48, 0x18, 0x5a, 0x5c, 0xbb, 0x37, 0xc8, 0x21, 0x00, 0xf8, + 0xad, 0x4f, 0xcb, 0x16, 0x98, 0x51, 0x96, 0x4d, 0xd9, 0xac, 0xa0, 0xb3, 0x3c, 0x28, 0xe4, 0x97, + 0x04, 0x07, 0x6a, 0x22, 0x9a, 0xd6, 0x7b, 0xc0, 0x4c, 0xe9, 0x4f, 0x09, 0x8e, 0x74, 0x07, 0xc9, + 0x6a, 0x58, 0x7d, 0xaf, 0xd8, 0x97, 0x98, 0x5c, 0x71, 0x8d, 0x05, 0x10, 0x0d, 0xf1, 0x6f, 0xde, + 0x08, 0x7b, 0x7a, 0xe2, 0x1b, 0x1b, 0x91, 0x1c, 0x49, 0x03, 0xf6, 0x13, 0xdf, 0xb6, 0x19, 0x5e, + 0xb6, 0x06, 0xa2, 0x95, 0xc2, 0x9e, 0x53, 0x5b, 0x52, 0xf8, 0x89, 0x15, 0x1f, 0xaa, 0x68, 0x36, + 0x10, 0x2d, 0x13, 0xd8, 0x0f, 0x2c, 0x01, 0x75, 0x85, 0x91, 0xba, 0xaa, 0x6e, 0x42, 0xb7, 0x33, + 0x19, 0xbf, 0xda, 0x2c, 0x8d, 0x2f, 0x35, 0x3c, 0x7d, 0x5e, 0xf7, 0x35, 0x5a, 0x34, 0xdf, 0xba, + 0xa9, 0x8c, 0xf8, 0x0a, 0xc3, 0xcb, 0x40, 0x0f, 0x60, 0x20, 0xa8, 0x41, 0xba, 0x07, 0x3f, 0x89, + 0x49, 0x52, 0x7c, 0x94, 0x21, 0x53, 0xe0, 0xf6, 0x4b, 0x2c, 0xde, 0xb1, 0x5f, 0x1f, 0xe8, 0xf5, + 0x6b, 0x1f, 0x1d, 0x29, 0xb6, 0xdb, 0xc2, 0xa4, 0x4c, 0x5a, 0x19, 0x8d, 0xa3, 0x1e, 0x1f, 0x69, + 0x15, 0xfa, 0x3f, 0x59, 0x54, 0xb9, 0x48, 0x7d, 0xc5, 0xaf, 0x22, 0x6b, 0xf8, 0xf8, 0xbf, 0x80, + 0x67, 0xcd, 0x3d, 0xec, 0x42, 0x43, 0x9f, 0x0d, 0x63, 0x4b, 0x6e, 0x6c, 0xfa, 0x4d, 0xba, 0xe9, + 0x2f, 0x6b, 0xda, 0x5f, 0x80, 0x8f, 0xac, 0xab, 0x46, 0x36, 0x4b, 0xd3, 0x0d, 0xed, 0x68, 0x76, + 0xa2, 0x82, 0x54, 0x3d, 0x2b, 0x1d, 0x69, 0x02, 0x55, 0x5d, 0x4b, 0x5a, 0x42, 0x81, 0x56, 0x64, + 0x20, 0xb3, 0xb4, 0x04, 0x91, 0x5a, 0x11, 0xa4, 0x35, 0x5a, 0x36, 0x49, 0x5f, 0xac, 0xef, 0x5a, + 0xd6, 0x4f, 0x0d, 0xfb, 0x16, 0xec, 0xfb, 0xb3, 0x82, 0xba, 0x24, 0x7d, 0xf2, 0x0c, 0x0c, 0x3a, + 0x3d, 0x6b, 0x79, 0x5b, 0x63, 0x44, 0x33, 0xee, 0xd2, 0x76, 0x10, 0x03, 0xc0, 0x38, 0x99, 0x83, + 0xba, 0xa2, 0x60, 0xbe, 0xab, 0x1d, 0x78, 0x50, 0xb7, 0xd4, 0x36, 0x8e, 0x9e, 0xae, 0x0e, 0x55, + 0x06, 0x05, 0xeb, 0x33, 0x1a, 0x12, 0xa0, 0x56, 0x2b, 0x0f, 0x1f, 0x8e, 0x7d, 0x85, 0xa2, 0x2e, + 0x9e, 0xdf, 0x8b, 0xa8, 0x92, 0xa2, 0x75, 0x74, 0xdf, 0x8a, 0x68, 0x87, 0x50, 0xda, 0x1c, 0x0e, + 0x9d, 0x72, 0xa5, 0x99, 0xbf, 0x05, 0x9e, 0xab, 0x3b, 0x7e, 0x6b, 0xb0, 0x61, 0x97, 0x9b, 0x70, + 0x1e, 0x5a, 0x8d, 0xf9, 0x7a, 0xf2, 0xef, 0xdf, 0x36, 0xf9, 0x71, 0xdf, 0xeb, 0xd8, 0x42, 0x46, + 0xd5, 0x02, 0xfd, 0x87, 0x81, 0x1f, 0x19, 0x84, 0xcd, 0x67, 0x97, 0x17, 0xe7, 0xfa, 0x36, 0x67, + 0xcb, 0x75, 0x0d, 0x79, 0x98, 0xa6, 0x59, 0x11, 0x18, 0x58, 0xb6, 0xef, 0xb7, 0xdb, 0xf7, 0xf7, + 0xf7, 0xce, 0x7d, 0xc7, 0x61, 0x7c, 0xdc, 0xf6, 0x5d, 0xd7, 0xc5, 0xfb, 0x08, 0x83, 0xa8, 0xd7, + 0x08, 0x06, 0x16, 0x55, 0x0d, 0xa2, 0xee, 0x7f, 0xf4, 0x97, 0xbe, 0xec, 0xd1, 0xb7, 0x44, 0x78, + 0xe7, 0xd3, 0x7f, 0x75, 0x74, 0x04, 0x13, 0xdd, 0x01, 0x74, 0x72, 0xf6, 0x85, 0xf6, 0x09, 0x74, + 0xe0, 0x7f, 0x65, 0x87, 0x2e, 0x67, 0x91, 0x16, 0x16, 0x57, 0x74, 0x57, 0x0c, 0xf4, 0x86, 0x78, + 0xb3, 0xd6, 0x27, 0xae, 0xe3, 0xd9, 0xe4, 0x68, 0xa0, 0xae, 0xfb, 0x8f, 0xed, 0xce, 0xd5, 0xc1, + 0xd9, 0xc1, 0x55, 0xf7, 0xec, 0xf0, 0xca, 0x3b, 0x7e, 0xeb, 0xdb, 0xbe, 0xbc, 0xd3, 0x72, 0x49, + 0xcf, 0xf6, 0xbd, 0x33, 0xaf, 0x57, 0xeb, 0xc1, 0x7b, 0x96, 0x63, 0x00, 0xf4, 0x5d, 0x98, 0xe1, + 0x1d, 0x5e, 0x75, 0xce, 0x8e, 0x2f, 0x7a, 0x76, 0xf7, 0x0c, 0xef, 0xbb, 0x8e, 0xcf, 0x7a, 0x57, + 0x5d, 0x40, 0x76, 0x74, 0xe5, 0xf5, 0xce, 0x3c, 0xef, 0xea, 0x08, 0xc6, 0xf0, 0xd6, 0x45, 0x7e, + 0x1e, 0xc2, 0xa7, 0xd7, 0xa9, 0xdf, 0x80, 0x09, 0x6d, 0x4e, 0xca, 0xca, 0x4f, 0x60, 0x94, 0x65, + 0x53, 0xa3, 0x1a, 0x93, 0x76, 0x47, 0x1f, 0xae, 0xb2, 0xa9, 0xe5, 0x08, 0x44, 0x10, 0x7a, 0xe0, + 0x77, 0x35, 0x10, 0x3b, 0x78, 0x19, 0xda, 0x88, 0xec, 0x21, 0xd6, 0x10, 0xcf, 0xa7, 0x37, 0xc2, + 0x51, 0x25, 0x86, 0x5f, 0x58, 0x4c, 0x1d, 0x15, 0x15, 0xac, 0xa6, 0xae, 0xcb, 0xe7, 0x2e, 0xd0, + 0x35, 0xb8, 0x1d, 0xca, 0xf3, 0xdc, 0xf4, 0x0d, 0x60, 0x6b, 0x5d, 0x87, 0xbf, 0x3a, 0x7b, 0xc7, + 0xda, 0xdf, 0xb8, 0xea, 0x96, 0x24, 0x66, 0x7b, 0x54, 0xf8, 0x4c, 0xba, 0xd8, 0x0c, 0xaa, 0xbe, + 0x12, 0xe6, 0x6d, 0xc4, 0xa0, 0x0b, 0xa9, 0x4d, 0xea, 0x66, 0x5a, 0x29, 0x16, 0x62, 0x00, 0x9b, + 0x0c, 0x8a, 0x88, 0x3e, 0x1c, 0x7a, 0xd0, 0xc7, 0xcb, 0x1f, 0x53, 0xfe, 0xee, 0xdc, 0x1a, 0x0e, + 0x22, 0xa5, 0xaa, 0xf3, 0x9b, 0x88, 0xad, 0x07, 0xaf, 0x93, 0x2f, 0xbf, 0xd6, 0x33, 0x35, 0x24, + 0x48, 0xd6, 0xf6, 0x30, 0xae, 0x93, 0x5f, 0xb2, 0xa8, 0x65, 0x0d, 0xca, 0x3b, 0xb8, 0x5f, 0x51, + 0xd1, 0x86, 0x5d, 0xd7, 0xfd, 0xa9, 0x94, 0x4d, 0x5d, 0x48, 0xc0, 0x57, 0x3b, 0x19, 0x35, 0xfa, + 0x1b, 0xdd, 0xaa, 0x6e, 0x69, 0xd4, 0xd6, 0x0c, 0xd3, 0xe8, 0x3f, 0x3f, 0x7f, 0xfc, 0xc5, 0x54, + 0x97, 0x74, 0x34, 0x78, 0xb3, 0x28, 0xcb, 0x08, 0x46, 0xff, 0xfa, 0xcd, 0x40, 0xbf, 0xa2, 0x59, + 0xcb, 0x42, 0xc4, 0x5a, 0x12, 0x02, 0xa9, 0xa0, 0x4c, 0x42, 0x04, 0x86, 0x43, 0x26, 0x85, 0xdc, + 0xc2, 0x46, 0x26, 0x42, 0x16, 0x82, 0x39, 0x88, 0x6d, 0xbc, 0x5e, 0x70, 0xa7, 0x80, 0xed, 0x53, + 0xd3, 0xb3, 0x96, 0x06, 0x26, 0x23, 0x08, 0x73, 0xb3, 0x04, 0x55, 0xa8, 0x05, 0x90, 0xf2, 0xfd, + 0x93, 0xa0, 0xff, 0x25, 0xcb, 0x2e, 0x78, 0x30, 0xaa, 0x00, 0x23, 0xc9, 0x5b, 0xd1, 0x69, 0xdf, + 0xb6, 0x35, 0x81, 0x98, 0xda, 0x38, 0x7f, 0x14, 0x2c, 0xbb, 0x6d, 0x24, 0xbe, 0xd5, 0x1c, 0x48, + 0x8d, 0x94, 0xff, 0xe2, 0x01, 0x56, 0x9e, 0x7e, 0xbb, 0x38, 0x3f, 0x03, 0x1b, 0xf8, 0x0f, 0x0a, + 0x69, 0x6f, 0x21, 0x20, 0x64, 0xc7, 0xce, 0x9f, 0x53, 0x76, 0x07, 0x49, 0xd4, 0x8d, 0xbd, 0xc0, + 0x00, 0xb5, 0x6f, 0x80, 0x12, 0xa7, 0x58, 0x3e, 0x02, 0x54, 0x6d, 0x44, 0x6d, 0x2c, 0x21, 0xd4, + 0xde, 0x22, 0x79, 0xb8, 0x88, 0x61, 0x9b, 0x65, 0x02, 0xcc, 0xd0, 0x62, 0xb0, 0xb1, 0x14, 0x6e, + 0x38, 0xfd, 0x22, 0x87, 0x3e, 0x7a, 0x49, 0x1f, 0x84, 0x6d, 0x90, 0x16, 0x31, 0xa4, 0x6e, 0x60, + 0xf4, 0x2b, 0x66, 0x78, 0x43, 0xc6, 0x60, 0x37, 0x9f, 0x21, 0xe5, 0x0e, 0xc7, 0xa5, 0xfc, 0x7c, + 0x10, 0x74, 0x0a, 0x87, 0x9d, 0xd2, 0xf8, 0x53, 0x98, 0x62, 0x4d, 0x44, 0x07, 0xcf, 0x08, 0x8a, + 0xb4, 0x38, 0x13, 0x4e, 0x47, 0x81, 0xd1, 0x06, 0x72, 0xec, 0x6d, 0xe4, 0x50, 0xce, 0xf1, 0xce, + 0x8b, 0xae, 0x91, 0x63, 0xbc, 0xc7, 0xfe, 0x3e, 0x91, 0xb7, 0x7b, 0x8d, 0x01, 0xf2, 0x59, 0x12, + 0xd3, 0x5f, 0xa7, 0x0d, 0x43, 0x8f, 0x64, 0x4a, 0xd9, 0x4c, 0x98, 0x72, 0x73, 0x4b, 0xdb, 0xa3, + 0x1d, 0x4b, 0xae, 0xca, 0xc0, 0xbc, 0x99, 0xc6, 0xa7, 0x8f, 0x9f, 0x2f, 0xe1, 0x74, 0xdb, 0x8a, + 0xcf, 0x86, 0x8a, 0xd4, 0x43, 0xc9, 0xcb, 0xbf, 0x33, 0x3e, 0x7d, 0x07, 0x81, 0x45, 0x29, 0x34, + 0xa1, 0x36, 0x89, 0x2a, 0xdc, 0x80, 0xdc, 0x1a, 0xaf, 0x10, 0xb9, 0xac, 0x84, 0x9b, 0xa1, 0x65, + 0xbf, 0xf4, 0x96, 0x61, 0xf1, 0x98, 0x45, 0x64, 0xf5, 0xc6, 0x8b, 0x8a, 0x0f, 0xd9, 0x88, 0x81, + 0x2c, 0x26, 0x23, 0x13, 0xc2, 0xa2, 0x60, 0xb5, 0x7d, 0x06, 0x27, 0x06, 0x3d, 0xe5, 0x15, 0xae, + 0x6b, 0x09, 0xfe, 0x58, 0x69, 0x4a, 0x78, 0x1f, 0x26, 0x82, 0x8c, 0xa8, 0x00, 0x61, 0x2c, 0xfd, + 0x9c, 0xb1, 0x0f, 0xe0, 0xfb, 0x86, 0x3c, 0xc4, 0xb6, 0x2c, 0x52, 0xa2, 0x16, 0x7d, 0x15, 0xb2, + 0xaa, 0xc1, 0x02, 0xa5, 0x1a, 0x9a, 0x4a, 0x19, 0x83, 0x9c, 0xa4, 0x5e, 0x11, 0x52, 0x43, 0x42, + 0x0f, 0xd9, 0x12, 0x63, 0x59, 0x8f, 0x33, 0x4d, 0x55, 0xd1, 0x82, 0x34, 0x05, 0x7f, 0xc1, 0x97, + 0x0b, 0xab, 0x05, 0x82, 0x0f, 0x7b, 0x81, 0x65, 0xa9, 0x55, 0x1d, 0x91, 0x3c, 0x35, 0xbc, 0xdc, + 0x92, 0x17, 0xc7, 0xcd, 0x5e, 0x03, 0x8c, 0x43, 0xc6, 0x04, 0x49, 0x62, 0x38, 0xe8, 0x64, 0xf4, + 0x48, 0x90, 0x05, 0x10, 0xaa, 0xad, 0xb1, 0xac, 0xb9, 0x30, 0xe0, 0xae, 0x97, 0xb1, 0x34, 0xb7, + 0x02, 0x77, 0x80, 0x17, 0xda, 0xa8, 0xdf, 0x90, 0x73, 0x0c, 0xc4, 0x30, 0xa0, 0x03, 0xb1, 0xbf, + 0xbf, 0xb2, 0x34, 0xb7, 0x9a, 0x13, 0xaf, 0x17, 0xc0, 0x89, 0xe5, 0x4a, 0xbd, 0x84, 0x56, 0xaf, + 0xc1, 0x8a, 0xd9, 0x4d, 0x16, 0xd2, 0x15, 0x97, 0xc4, 0x1a, 0x97, 0x14, 0x01, 0xf9, 0xac, 0x98, + 0x40, 0x5e, 0xa8, 0xb7, 0x2e, 0xd6, 0xb7, 0x7e, 0x2b, 0xe5, 0x53, 0x21, 0xc3, 0x12, 0x2a, 0xaa, + 0x2d, 0x19, 0x71, 0x36, 0x95, 0xd7, 0x16, 0x7d, 0x72, 0x0b, 0x12, 0xb3, 0x5c, 0x6e, 0xd9, 0xd2, + 0xd0, 0x03, 0x43, 0xb3, 0xb9, 0x52, 0xb9, 0xfb, 0xfe, 0xb5, 0x6b, 0xf7, 0xca, 0x3f, 0x48, 0xcb, + 0xaa, 0x8f, 0x9b, 0x65, 0x79, 0xbf, 0x23, 0x02, 0x5c, 0x0c, 0x2d, 0x79, 0x41, 0xcd, 0x86, 0x46, + 0xa2, 0x14, 0xae, 0xa9, 0xa3, 0xac, 0x3e, 0x00, 0xf5, 0xc8, 0x46, 0xcd, 0x34, 0x2c, 0xb0, 0x83, + 0x0f, 0x97, 0xa2, 0x0a, 0xbf, 0x60, 0x42, 0x1c, 0xf9, 0xda, 0xb3, 0x26, 0x24, 0x68, 0x56, 0xe8, + 0x89, 0x7f, 0x70, 0x68, 0xe9, 0xcb, 0x4c, 0xa2, 0x00, 0x91, 0x02, 0x91, 0x64, 0x33, 0xba, 0x54, + 0xb8, 0x78, 0xa0, 0xfb, 0xf1, 0x84, 0xb0, 0x2e, 0x31, 0xa8, 0xaf, 0xc3, 0x46, 0x84, 0xcb, 0x55, + 0x5e, 0xaa, 0x8d, 0x26, 0x85, 0xfc, 0x05, 0xde, 0x3f, 0x3d, 0x1d, 0xbc, 0x0c, 0x02, 0xaa, 0x59, + 0x62, 0x2d, 0x64, 0x71, 0xe5, 0x8e, 0xd3, 0xf0, 0xcb, 0x72, 0x85, 0x40, 0x20, 0x02, 0x6a, 0xc1, + 0x7c, 0x23, 0x9b, 0x4d, 0xef, 0x20, 0x8a, 0x05, 0x9f, 0x06, 0xa6, 0x0e, 0x7a, 0xc5, 0xd3, 0x93, + 0x18, 0xba, 0xf0, 0xcf, 0x09, 0xb0, 0xe8, 0xe9, 0xe9, 0xe5, 0x2f, 0x72, 0x1c, 0x16, 0xf8, 0x90, + 0x09, 0x3a, 0x06, 0xb3, 0x22, 0xac, 0x06, 0xd2, 0x25, 0x12, 0xc1, 0xbe, 0xb2, 0x99, 0x30, 0xe0, + 0xd7, 0x5c, 0x93, 0xd4, 0xf2, 0x6e, 0x90, 0x71, 0xf2, 0x1a, 0x34, 0x08, 0x21, 0x83, 0x5f, 0x89, + 0xdc, 0xb5, 0xe3, 0x38, 0xe1, 0xcd, 0x80, 0x42, 0x67, 0x80, 0x07, 0xc4, 0xd5, 0x01, 0x82, 0x2e, + 0x2c, 0x4b, 0x90, 0x75, 0x9f, 0x03, 0xcc, 0x76, 0xa6, 0x61, 0xbe, 0xba, 0xf3, 0x32, 0x17, 0xb0, + 0x3e, 0xf8, 0xb8, 0x51, 0x1a, 0xe2, 0xdd, 0x07, 0xb2, 0x1f, 0xbe, 0xf0, 0x67, 0x69, 0xa1, 0x93, + 0x2c, 0x18, 0x07, 0x2b, 0x86, 0x5e, 0x20, 0x38, 0xa1, 0xb2, 0x5f, 0xda, 0xd3, 0x14, 0x92, 0x9b, + 0x29, 0x7a, 0x70, 0x53, 0x01, 0x43, 0x54, 0xbf, 0x29, 0x49, 0x40, 0x5e, 0xa9, 0x97, 0x30, 0x06, + 0x78, 0x8c, 0x4b, 0x7c, 0xa5, 0x80, 0x0f, 0x61, 0x91, 0xa1, 0x55, 0x8d, 0x1c, 0xd0, 0x90, 0x69, + 0x52, 0x14, 0xc9, 0x58, 0xc9, 0xed, 0x23, 0x9b, 0x71, 0x72, 0xc7, 0xd9, 0x7d, 0x01, 0x9c, 0x24, + 0xbf, 0xb3, 0x19, 0x29, 0x26, 0x6c, 0x96, 0xc6, 0x24, 0xe7, 0xec, 0x2e, 0xbc, 0x4b, 0x1f, 0x89, + 0x36, 0x8e, 0xfa, 0x4d, 0xc1, 0x34, 0x04, 0x39, 0x82, 0x30, 0x05, 0x96, 0xc9, 0x62, 0x82, 0x02, + 0x00, 0xba, 0x24, 0x9f, 0x1d, 0xc0, 0x84, 0x9c, 0x72, 0x98, 0x30, 0xc2, 0x07, 0x14, 0xf8, 0x98, + 0xa0, 0x5c, 0x53, 0x51, 0x81, 0x57, 0x84, 0x70, 0x4a, 0x60, 0xfe, 0xc1, 0x67, 0x92, 0x3b, 0x0a, + 0x60, 0x54, 0x23, 0x47, 0x55, 0x9a, 0x50, 0x4e, 0x1d, 0x30, 0xd4, 0x17, 0x48, 0x1c, 0x7c, 0xcb, + 0x49, 0x71, 0x85, 0xe4, 0x25, 0x58, 0xee, 0x72, 0xb2, 0x36, 0x1f, 0xef, 0x92, 0x79, 0x51, 0x0f, + 0x94, 0xb6, 0x0e, 0x57, 0x07, 0xb8, 0xf1, 0x02, 0xd7, 0xa8, 0x99, 0x4e, 0xb1, 0x65, 0x78, 0xed, + 0xcd, 0x02, 0x5a, 0x0e, 0xc5, 0x6f, 0xe4, 0x9b, 0x49, 0x9d, 0x08, 0x43, 0x28, 0x08, 0xab, 0x20, + 0xae, 0x48, 0x52, 0x81, 0x49, 0x2f, 0x9e, 0x1a, 0x64, 0x66, 0x53, 0x69, 0x3b, 0xda, 0xff, 0xd4, + 0xf8, 0xff, 0x27, 0x7e, 0xdd, 0x86, 0xe3, 0x5d, 0x93, 0x70, 0x6e, 0xad, 0x47, 0xd1, 0x95, 0xcd, + 0xe3, 0x60, 0xf3, 0xf8, 0x70, 0x8b, 0x11, 0x19, 0xf0, 0x95, 0x11, 0x64, 0x41, 0x1d, 0xe0, 0x9a, + 0xdf, 0xd8, 0x61, 0xb0, 0xfe, 0x74, 0x58, 0x67, 0xc0, 0xa1, 0x53, 0x7b, 0xac, 0x63, 0xec, 0x73, + 0x3b, 0x54, 0x55, 0x10, 0xf4, 0xc6, 0xe8, 0x9a, 0x2b, 0x4e, 0x18, 0xa5, 0xad, 0xc9, 0x4a, 0x59, + 0xfe, 0x42, 0x1f, 0x0b, 0x93, 0x59, 0x20, 0xf4, 0x80, 0x05, 0x9d, 0x22, 0xb8, 0x5b, 0xbc, 0x72, + 0x97, 0xa9, 0x91, 0x32, 0x48, 0x85, 0xbc, 0x6c, 0x05, 0x83, 0x6f, 0xb2, 0xeb, 0xec, 0x66, 0x55, + 0x02, 0xdc, 0x41, 0x4c, 0x5a, 0x27, 0xa6, 0x8c, 0xa1, 0x81, 0xa8, 0xaa, 0x42, 0xb6, 0x63, 0x9e, + 0x2a, 0x7c, 0xad, 0x5e, 0xb6, 0xe1, 0x3e, 0x92, 0xf5, 0x7d, 0xd4, 0x86, 0x57, 0xd5, 0xab, 0x0d, + 0x84, 0x55, 0x29, 0x4b, 0x3e, 0x35, 0xd1, 0xcf, 0xdc, 0x10, 0x5f, 0xe1, 0xe0, 0x23, 0x51, 0x08, + 0x7a, 0x83, 0x52, 0xa6, 0xd0, 0xb3, 0x37, 0xa3, 0x3c, 0xf0, 0x0d, 0x58, 0x9d, 0xd9, 0x76, 0xb5, + 0x61, 0x7c, 0xa6, 0xf8, 0x7e, 0x47, 0xbd, 0x68, 0xaa, 0xbd, 0x24, 0xc2, 0xe7, 0x37, 0x04, 0xf0, + 0x63, 0x49, 0x6b, 0x95, 0xff, 0xea, 0xf7, 0x19, 0x76, 0xb1, 0xbe, 0x89, 0x8a, 0xa2, 0x72, 0x0b, + 0xd1, 0xce, 0x2d, 0x44, 0x72, 0x0b, 0xe5, 0x9b, 0x3d, 0xdc, 0x42, 0xb4, 0x6d, 0x0b, 0x48, 0x38, + 0xc4, 0x3a, 0xf8, 0x88, 0x42, 0xd2, 0x1f, 0xed, 0xb8, 0x9a, 0x39, 0x65, 0xf9, 0xa3, 0xa2, 0x16, + 0xe2, 0xdf, 0x65, 0xa9, 0x7a, 0xb8, 0x05, 0xb5, 0x99, 0xdb, 0xa6, 0xe1, 0xc1, 0x72, 0x03, 0xda, + 0xa3, 0xbd, 0xbd, 0x67, 0x11, 0xbe, 0x5e, 0x6c, 0x9b, 0xb4, 0x1d, 0x7d, 0xd4, 0x64, 0x10, 0x52, + 0x0c, 0x7d, 0x6b, 0x0c, 0xaa, 0xf6, 0x8b, 0x75, 0xb4, 0xed, 0x92, 0x5c, 0xbd, 0x31, 0x45, 0x89, + 0x03, 0x95, 0xca, 0x03, 0xc3, 0xa8, 0xf4, 0x8b, 0x82, 0x7e, 0xd1, 0x21, 0x4a, 0x6b, 0xa9, 0x57, + 0x10, 0xdc, 0xfb, 0x56, 0x15, 0x30, 0xe0, 0x08, 0x3a, 0x0a, 0x74, 0x41, 0x4a, 0xba, 0xb1, 0x3a, + 0xae, 0x5c, 0x90, 0x1a, 0xdb, 0xf7, 0x6e, 0xac, 0x1c, 0x92, 0x85, 0x57, 0xaf, 0x17, 0x55, 0x07, + 0x56, 0x2e, 0x44, 0x1b, 0x5c, 0xc3, 0x8f, 0x9e, 0xeb, 0x2e, 0x7f, 0xb0, 0xc9, 0xad, 0x2c, 0xaf, + 0x2f, 0x10, 0x4e, 0xbe, 0x16, 0xaf, 0xc3, 0x62, 0xad, 0x43, 0x7f, 0xf9, 0x8d, 0xaf, 0x0e, 0x7e, + 0x79, 0xd6, 0x06, 0x2e, 0xcc, 0x3f, 0x7c, 0x88, 0x2c, 0x82, 0x5c, 0xe7, 0x25, 0xae, 0xdd, 0xf2, + 0xb7, 0xd5, 0x11, 0x3f, 0x4c, 0xc1, 0x36, 0x07, 0xb7, 0xbb, 0xea, 0x33, 0xb8, 0x54, 0xbe, 0xb4, + 0x6e, 0x4b, 0x1b, 0xa0, 0x2a, 0xa1, 0x1b, 0x4f, 0x5a, 0x41, 0x98, 0x4b, 0x9b, 0x10, 0x04, 0xd9, + 0x4f, 0xc9, 0x5a, 0xb9, 0xb0, 0x1f, 0xe9, 0x75, 0xd5, 0xeb, 0xc9, 0x73, 0xbc, 0x7a, 0x30, 0xe4, + 0xfb, 0x1b, 0x9b, 0xbf, 0x0c, 0x64, 0x10, 0xb9, 0xb7, 0xd7, 0x9c, 0x14, 0xe1, 0x7b, 0x86, 0xb5, + 0x7a, 0x64, 0xb8, 0x5e, 0xbb, 0x6c, 0x2c, 0x4a, 0x1b, 0xa3, 0xa1, 0x05, 0xae, 0xb2, 0xd9, 0xb1, + 0x5c, 0x99, 0xfe, 0xba, 0x84, 0x53, 0xbc, 0x17, 0xdd, 0xa8, 0x14, 0xbe, 0x91, 0x55, 0xf0, 0xeb, + 0x24, 0xfe, 0x67, 0xb3, 0x7c, 0x7e, 0xf3, 0x66, 0x95, 0x17, 0x62, 0x36, 0x42, 0x77, 0xe5, 0xca, + 0xba, 0xa4, 0xbb, 0x89, 0x18, 0x75, 0xb1, 0x86, 0x57, 0x5f, 0xc4, 0xdd, 0xd8, 0x64, 0x6d, 0xa0, + 0x71, 0x7b, 0xdd, 0x18, 0x6e, 0xdc, 0xe0, 0x7d, 0x17, 0x41, 0x03, 0x15, 0x23, 0xd7, 0x22, 0xc3, + 0x71, 0xcd, 0xb3, 0x19, 0xfb, 0xd4, 0xda, 0x76, 0x8f, 0x5a, 0x1a, 0x70, 0xc3, 0xb2, 0xd6, 0x94, + 0x42, 0x6c, 0xd3, 0x08, 0x08, 0xf7, 0xaa, 0x60, 0x6f, 0xab, 0x52, 0x08, 0xa5, 0x11, 0x4c, 0x3e, + 0x1e, 0x51, 0x1f, 0x4a, 0xf6, 0x59, 0x00, 0x92, 0x7f, 0xc9, 0xce, 0xe8, 0x83, 0xa9, 0xba, 0x6d, + 0x21, 0x25, 0x5e, 0xfe, 0x74, 0x6e, 0x2c, 0x25, 0xd5, 0xf2, 0xe5, 0x0d, 0xb7, 0x59, 0x95, 0x44, + 0x16, 0x11, 0x67, 0xc0, 0x59, 0xd7, 0x76, 0xeb, 0x67, 0x5c, 0xa1, 0x82, 0x00, 0xc9, 0xe6, 0x2b, + 0x3f, 0x68, 0xd2, 0xe1, 0xd0, 0xeb, 0x42, 0x64, 0x38, 0x3c, 0x7a, 0xe2, 0xcd, 0x07, 0x2a, 0x3a, + 0xb1, 0x43, 0xb2, 0x0c, 0xd7, 0xc0, 0x2b, 0x53, 0xb0, 0xa2, 0x66, 0xb7, 0xc5, 0xca, 0x10, 0x74, + 0x9f, 0x2d, 0xab, 0x1c, 0xae, 0x5c, 0x7d, 0x57, 0xee, 0x8c, 0xb7, 0x1d, 0xcf, 0x00, 0x41, 0x12, + 0x9d, 0xfc, 0x49, 0x4b, 0xb0, 0x46, 0x05, 0x7d, 0xe7, 0x4b, 0x87, 0xc6, 0x73, 0x26, 0xfd, 0x74, + 0xc9, 0xd5, 0xbf, 0xf8, 0xac, 0x67, 0xe3, 0xaa, 0xa9, 0xfe, 0xff, 0xfe, 0xb4, 0xd5, 0xff, 0xbf, + 0xf5, 0xbf, 0x04, 0xfa, 0x03, 0x25, 0xd7, 0x35, 0x00, 0x00 +}; diff --git a/wled00/html_other.h b/wled00/html_other.h index 712d2f2cae..f9c9a5ee04 100644 --- a/wled00/html_other.h +++ b/wled00/html_other.h @@ -6,16 +6,22 @@ */ // Autogenerated from wled00/data/usermod.htm, do not edit!! -const char PAGE_usermod[] PROGMEM = R"=====(No usermod custom web page set.)====="; +const uint16_t PAGE_usermod_length = 81; +const uint8_t PAGE_usermod[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xb3, 0x51, 0x74, 0xf1, 0x77, 0x0e, + 0x89, 0x0c, 0x70, 0x55, 0xc8, 0x28, 0xc9, 0xcd, 0xb1, 0xb3, 0x81, 0x90, 0x49, 0xf9, 0x29, 0x95, + 0x76, 0x7e, 0xf9, 0x0a, 0xa5, 0xc5, 0xa9, 0x45, 0xb9, 0xf9, 0x29, 0x0a, 0xc9, 0xa5, 0xc5, 0x25, + 0xf9, 0xb9, 0x0a, 0xe5, 0xa9, 0x49, 0x0a, 0x05, 0x89, 0xe9, 0xa9, 0x0a, 0xc5, 0xa9, 0x25, 0x7a, + 0x36, 0xfa, 0x60, 0x55, 0x36, 0xfa, 0x60, 0x2d, 0x00, 0x1e, 0x93, 0x65, 0xc7, 0x48, 0x00, 0x00, + 0x00 +}; // Autogenerated from wled00/data/msg.htm, do not edit!! const char PAGE_msg[] PROGMEM = R"=====( WLED Message

%MSG%)====="; +function B(){window.history.back()}function RS(){window.location="../settings"}function RP(){top.location.href="../"} +

%MSG%)====="; #ifdef WLED_ENABLE_DMX @@ -35,70 +41,340 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; #endif // Autogenerated from wled00/data/update.htm, do not edit!! -const char PAGE_update[] PROGMEM = R"=====( -WLED Update

WLED Software Update

-Installed version: 0.13.1
Download the latest binary: -


Updating...
-Please do not close or refresh the page :)
)====="; +const uint16_t PAGE_update_length = 614; +const uint8_t PAGE_update[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0x75, 0x93, 0x4f, 0x6f, 0xdc, 0x20, + 0x10, 0xc5, 0xef, 0xfe, 0x14, 0x84, 0xd3, 0xae, 0xd4, 0x40, 0x1b, 0xe5, 0xd2, 0x2d, 0x90, 0x76, + 0x9b, 0xa8, 0x8a, 0x54, 0x29, 0x91, 0x92, 0xb4, 0xea, 0xa9, 0xc2, 0x30, 0xb6, 0xe9, 0x62, 0x70, + 0x60, 0xbc, 0xab, 0x55, 0x94, 0xef, 0x5e, 0x61, 0xef, 0xa6, 0x55, 0xff, 0x5c, 0x2c, 0x63, 0xde, + 0x3c, 0x98, 0xdf, 0x3c, 0x8b, 0x93, 0xcb, 0x9b, 0x8f, 0xf7, 0xdf, 0x6e, 0xaf, 0x48, 0x87, 0xbd, + 0x57, 0xe2, 0xf0, 0x04, 0x6d, 0x95, 0xe8, 0x01, 0x35, 0x31, 0x31, 0x20, 0x04, 0x94, 0x74, 0xe7, + 0x2c, 0x76, 0xd2, 0xc2, 0xd6, 0x19, 0x38, 0x9d, 0x16, 0x94, 0x04, 0xdd, 0x83, 0xa4, 0x5b, 0x07, + 0xbb, 0x21, 0x26, 0xa4, 0xaa, 0x12, 0xe8, 0xd0, 0x83, 0xfa, 0xfa, 0xf9, 0xea, 0x92, 0x3c, 0x0c, + 0x56, 0x23, 0x08, 0x3e, 0x7f, 0x12, 0xd9, 0x24, 0x37, 0xa0, 0xaa, 0x9a, 0x31, 0x18, 0x74, 0x31, + 0x90, 0xf5, 0x62, 0xf9, 0xb4, 0x73, 0xc1, 0xc6, 0x1d, 0xeb, 0x5c, 0xc6, 0x98, 0xf6, 0xac, 0xd6, + 0x66, 0xb3, 0x58, 0x3e, 0xbf, 0x48, 0x1e, 0x16, 0xcb, 0x27, 0x1b, 0xcd, 0xd8, 0x43, 0x40, 0xd6, + 0x02, 0x5e, 0x79, 0x28, 0xaf, 0xeb, 0xfd, 0xb5, 0x5d, 0xd0, 0xb1, 0xa1, 0x4b, 0x96, 0x71, 0xef, + 0x81, 0x59, 0x97, 0x07, 0xaf, 0xf7, 0x92, 0x86, 0x18, 0x80, 0xbe, 0xfa, 0x6f, 0x49, 0x9f, 0xdb, + 0xbf, 0x6b, 0x6a, 0x1f, 0xcd, 0x86, 0x3e, 0x57, 0x82, 0x1f, 0xae, 0x78, 0xb8, 0x2a, 0xc9, 0xc9, + 0x48, 0xca, 0x33, 0x20, 0xba, 0xd0, 0x66, 0x9e, 0xd9, 0x8f, 0x7c, 0x31, 0xc8, 0xb7, 0x54, 0xfd, + 0xa6, 0x2c, 0x56, 0xaa, 0x7a, 0xef, 0xfa, 0x02, 0x80, 0x8c, 0xc9, 0x2f, 0xe8, 0x6c, 0x6f, 0x72, + 0xa6, 0xcb, 0x77, 0x82, 0xcf, 0x0a, 0xc1, 0x67, 0xa4, 0x75, 0xb4, 0x7b, 0x12, 0x83, 0x8f, 0xda, + 0x4a, 0xfa, 0x09, 0xf0, 0xcb, 0x62, 0x49, 0x95, 0xe8, 0xce, 0x54, 0x35, 0x21, 0xbb, 0x8b, 0x0d, + 0xee, 0x74, 0x82, 0x17, 0x76, 0xdd, 0x99, 0x12, 0x4d, 0x4c, 0x3d, 0xe9, 0x01, 0xbb, 0x68, 0x25, + 0xbd, 0xbd, 0xb9, 0xbb, 0xa7, 0x44, 0x4f, 0x78, 0x24, 0x65, 0x7c, 0x9c, 0x84, 0x94, 0x38, 0x2b, + 0x0b, 0x10, 0x52, 0x41, 0x30, 0xb8, 0x1f, 0x40, 0xd2, 0x7e, 0xf4, 0xe8, 0x06, 0x9d, 0x90, 0x17, + 0x83, 0x53, 0xab, 0x51, 0x53, 0x12, 0x43, 0x1e, 0xeb, 0xde, 0xa1, 0xa4, 0x0f, 0xe5, 0xe4, 0xeb, + 0x90, 0x51, 0x7b, 0x0f, 0x96, 0x6c, 0x21, 0x65, 0x17, 0xc3, 0x8a, 0x88, 0x3c, 0xe8, 0x40, 0x2a, + 0xe3, 0x75, 0xce, 0x92, 0x66, 0x37, 0x50, 0xf5, 0x9a, 0xbd, 0x39, 0x67, 0xe7, 0x82, 0x97, 0x1d, + 0x25, 0xea, 0xa4, 0x2e, 0xe3, 0x6e, 0x6a, 0x81, 0x60, 0x07, 0xc4, 0x6b, 0x84, 0x8c, 0xa4, 0x76, + 0x41, 0xa7, 0xfd, 0x8a, 0x08, 0x4d, 0xaa, 0x2e, 0x41, 0x23, 0x69, 0x87, 0x38, 0xe4, 0x15, 0xe7, + 0xad, 0xc3, 0x6e, 0xac, 0x99, 0x89, 0x3d, 0xff, 0xe0, 0x92, 0x89, 0x31, 0x6e, 0x1c, 0xf0, 0xd2, + 0x2f, 0x4f, 0xe0, 0x41, 0x67, 0xc8, 0x94, 0xa0, 0x4e, 0x2d, 0xa0, 0xa4, 0xdf, 0x6b, 0xaf, 0xc3, + 0x86, 0x2a, 0xe1, 0xfa, 0x96, 0x54, 0xd3, 0x04, 0x8e, 0x3e, 0xae, 0x6f, 0x59, 0xee, 0x1c, 0x78, + 0x9b, 0x99, 0x8b, 0x07, 0xdb, 0xa3, 0xc5, 0x9f, 0xd6, 0x2c, 0x6f, 0xdb, 0x8b, 0x89, 0xbd, 0x6c, + 0xbc, 0xc6, 0xd3, 0xfc, 0x38, 0xea, 0x04, 0x25, 0xa1, 0x5c, 0x4f, 0x3d, 0x08, 0x17, 0x86, 0x11, + 0xc9, 0xcc, 0xaa, 0x71, 0x1e, 0x8e, 0x69, 0x3e, 0x12, 0x4d, 0xf0, 0x38, 0xba, 0x04, 0x76, 0x56, + 0xd7, 0x23, 0x62, 0x0c, 0x07, 0xf9, 0xcc, 0x90, 0xaa, 0x6a, 0x1e, 0xd3, 0x89, 0xe0, 0xf3, 0xf6, + 0x3f, 0xa4, 0xf3, 0xa2, 0x80, 0x37, 0xde, 0x99, 0x8d, 0xa4, 0xeb, 0xc2, 0x7d, 0xad, 0xcd, 0xe6, + 0x57, 0xd1, 0x34, 0x20, 0x25, 0xac, 0xdb, 0x56, 0xd3, 0x1c, 0x4b, 0x4a, 0x95, 0xa8, 0xd5, 0xe4, + 0xee, 0x42, 0xcb, 0x18, 0x13, 0xbc, 0x9e, 0xcc, 0x6f, 0xa7, 0x66, 0x89, 0x8d, 0x24, 0x44, 0x24, + 0xc6, 0xc7, 0x0c, 0x24, 0x26, 0x92, 0xa0, 0x49, 0x90, 0xbb, 0x69, 0x1e, 0x83, 0x6e, 0x81, 0xac, + 0x96, 0x82, 0x5b, 0xb7, 0x2d, 0xed, 0x96, 0xc8, 0x95, 0xfc, 0x95, 0x1f, 0xfb, 0x27, 0xb5, 0x43, + 0x19, 0xed, 0xee, 0x03, 0x00, 0x00 +}; // Autogenerated from wled00/data/welcome.htm, do not edit!! -const char PAGE_welcome[] PROGMEM = R"=====(Welcome! -

Welcome to WLED!

-Thank you for installing my application!

Next steps:

-Connect the module to your local WiFi here!

-Just trying this out in AP mode?

-)====="; +const uint16_t PAGE_welcome_length = 1537; +const uint8_t PAGE_welcome[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0x95, 0x56, 0x59, 0x8f, 0xab, 0x3a, + 0x12, 0x7e, 0xcf, 0xaf, 0xa0, 0x33, 0x1a, 0x9d, 0x87, 0x9c, 0x6e, 0xb6, 0x90, 0xbd, 0x73, 0x87, + 0x90, 0xa5, 0xb3, 0x41, 0x36, 0xb2, 0xbd, 0x19, 0x30, 0x60, 0x02, 0x36, 0xb1, 0x0d, 0x21, 0x69, + 0xf5, 0x7f, 0xbf, 0x4a, 0x72, 0x7a, 0x74, 0xae, 0xe6, 0x4a, 0x57, 0x63, 0x04, 0x72, 0x7d, 0x45, + 0x55, 0x7d, 0x85, 0xed, 0x2a, 0x3a, 0x2f, 0x7d, 0xcb, 0xd8, 0x1c, 0x16, 0x03, 0x21, 0xe4, 0x49, + 0xdc, 0xed, 0xfc, 0x7a, 0x42, 0xe0, 0x75, 0x3b, 0x09, 0xe4, 0x40, 0x70, 0x43, 0x40, 0x19, 0xe4, + 0xef, 0xe5, 0x8c, 0xfb, 0xaf, 0x8d, 0xf2, 0x2f, 0xb4, 0xe4, 0x12, 0xcc, 0x21, 0xe6, 0xef, 0xe5, + 0x0b, 0xf2, 0x78, 0xf8, 0xee, 0xc1, 0x1c, 0xb9, 0xf0, 0xf5, 0x21, 0x94, 0x05, 0x0c, 0x12, 0xf8, + 0x5e, 0xce, 0x11, 0xbc, 0xa4, 0x84, 0xf2, 0x6f, 0x9b, 0x27, 0xca, 0x43, 0x98, 0xc0, 0x57, 0x97, + 0xc4, 0x84, 0x96, 0x7f, 0x73, 0xf3, 0x2f, 0xe5, 0x31, 0xca, 0xdd, 0x0e, 0x47, 0x3c, 0x86, 0xdd, + 0x1d, 0x8c, 0x5d, 0x92, 0xc0, 0x97, 0x8e, 0xf8, 0x94, 0x3b, 0x8c, 0x5f, 0x63, 0xd8, 0x2d, 0x39, + 0xc4, 0xbb, 0x7e, 0xfa, 0x04, 0xf3, 0x57, 0x1f, 0x24, 0x28, 0xbe, 0xb6, 0xb6, 0x90, 0x7a, 0x00, + 0x83, 0x9f, 0x1f, 0x30, 0xce, 0x21, 0x47, 0x2e, 0xf8, 0xc9, 0x00, 0x66, 0xaf, 0x0c, 0x52, 0xe4, + 0xb7, 0x39, 0x2c, 0xf8, 0x2b, 0x88, 0x51, 0x80, 0x5b, 0x2e, 0xc4, 0x1c, 0xd2, 0xb6, 0x03, 0xdc, + 0x53, 0x40, 0x49, 0x86, 0xbd, 0x27, 0x87, 0xd6, 0x3d, 0x70, 0x3b, 0x01, 0x34, 0x40, 0xb8, 0x25, + 0xb5, 0x7f, 0x61, 0xbe, 0xef, 0x7f, 0x39, 0x19, 0xe7, 0x04, 0x7f, 0x92, 0x8c, 0xc7, 0x08, 0xc3, + 0xbb, 0x2e, 0xa3, 0x8c, 0xd0, 0x56, 0x4a, 0xd0, 0xc3, 0x53, 0x0a, 0x3c, 0x0f, 0xe1, 0xa0, 0xd5, + 0x48, 0x8b, 0x6f, 0x7b, 0x59, 0x4a, 0x8b, 0xf6, 0xe3, 0x1b, 0xb4, 0x14, 0xf5, 0x3e, 0x7f, 0xc4, + 0xe7, 0x14, 0x60, 0xe6, 0x13, 0x9a, 0xb4, 0xb2, 0x34, 0x85, 0xd4, 0x05, 0x0c, 0xb6, 0x7f, 0xcf, + 0x20, 0xfc, 0x66, 0xfe, 0x44, 0x19, 0xba, 0xc1, 0x96, 0xdc, 0x4c, 0x8b, 0xbf, 0xe1, 0xaa, 0xaa, + 0xea, 0x6f, 0x14, 0xdb, 0x0e, 0xa1, 0x1e, 0xa4, 0x2d, 0x49, 0x60, 0x24, 0x46, 0x9e, 0xf0, 0x1b, + 0xf6, 0x4a, 0x81, 0x87, 0x32, 0xd6, 0x52, 0xb4, 0xb4, 0xf8, 0x42, 0x49, 0xf0, 0xf9, 0x64, 0xd5, + 0xd4, 0xa4, 0x07, 0xdb, 0xe2, 0xb9, 0x52, 0xad, 0x86, 0xf2, 0xef, 0x36, 0x4a, 0x40, 0x00, 0x5f, + 0x29, 0xc4, 0x1e, 0xa4, 0xf7, 0x7c, 0x52, 0x54, 0xc0, 0x18, 0x70, 0xe8, 0xfd, 0x8f, 0xc6, 0xa5, + 0x88, 0xa5, 0xaf, 0xd0, 0x0b, 0x20, 0xfb, 0xce, 0xb8, 0x9a, 0x87, 0x82, 0x74, 0xbf, 0xda, 0x00, + 0xa3, 0x04, 0x70, 0x44, 0x70, 0xcb, 0x47, 0x82, 0xcc, 0xbe, 0xfe, 0x73, 0x82, 0x57, 0x9f, 0x82, + 0x04, 0x32, 0xc1, 0x47, 0x9f, 0x3e, 0x25, 0xc9, 0x27, 0x49, 0x81, 0x8b, 0xf8, 0xb5, 0x25, 0x7d, + 0x71, 0xf2, 0x5f, 0x41, 0xfe, 0xfa, 0x7a, 0x4b, 0x00, 0xc2, 0x9f, 0x7f, 0x75, 0xf0, 0xa6, 0x31, + 0xe1, 0xad, 0xce, 0x04, 0x87, 0xf0, 0xf0, 0xab, 0xd4, 0x11, 0x9f, 0xcb, 0xdf, 0x11, 0x9f, 0x3b, + 0xf3, 0xbe, 0x0b, 0xba, 0x1d, 0x94, 0x04, 0x02, 0x88, 0xf9, 0x7b, 0xb9, 0x2c, 0x94, 0x18, 0x75, + 0xdf, 0xcb, 0x1e, 0xe0, 0xa0, 0xf5, 0x60, 0x2d, 0xa6, 0x38, 0x68, 0x3b, 0x80, 0xc1, 0x5a, 0xf5, + 0x27, 0xda, 0xf6, 0xac, 0xd5, 0x45, 0x9a, 0x8e, 0x02, 0xa2, 0xeb, 0xba, 0x6e, 0xae, 0xed, 0x70, + 0x60, 0x07, 0xba, 0xae, 0x8f, 0xa4, 0xbb, 0xec, 0x1b, 0xfa, 0x5c, 0xd7, 0xf5, 0x3e, 0xb8, 0xcd, + 0xac, 0xec, 0x0e, 0xe8, 0x7b, 0x73, 0xbd, 0x92, 0xc6, 0x3a, 0x65, 0x55, 0xb7, 0xb6, 0xbc, 0x03, + 0x2b, 0xbc, 0xb4, 0xe5, 0x9e, 0xae, 0x1b, 0x45, 0x74, 0xc9, 0x1b, 0x87, 0xa5, 0xad, 0xeb, 0x7a, + 0x6f, 0x66, 0x0f, 0x0a, 0x7b, 0xf5, 0xd0, 0xf7, 0x1a, 0x72, 0x60, 0xd8, 0xe2, 0x6d, 0x7a, 0x16, + 0x45, 0x31, 0x21, 0x75, 0xb6, 0x9b, 0x9b, 0x0d, 0xc7, 0xaa, 0x1c, 0xc7, 0x07, 0x7e, 0x04, 0xfa, + 0xa2, 0x49, 0xf5, 0x45, 0xe5, 0x63, 0xce, 0x0c, 0x34, 0xaa, 0x6c, 0xf4, 0xb1, 0x85, 0xe7, 0x6b, + 0x69, 0x7a, 0x36, 0xed, 0xfa, 0x74, 0x81, 0x67, 0xf6, 0xd1, 0xa2, 0xe7, 0x5a, 0x2e, 0x8a, 0x62, + 0xd5, 0xd0, 0x83, 0x51, 0x48, 0xc0, 0xac, 0x22, 0xe6, 0x35, 0x23, 0x20, 0x83, 0x62, 0xbe, 0x79, + 0x10, 0x8a, 0x93, 0xaa, 0xd5, 0xb8, 0x4f, 0x8e, 0xde, 0x70, 0x62, 0xd9, 0xe2, 0x3f, 0x8c, 0x8b, + 0xde, 0x33, 0xf5, 0xb3, 0x7a, 0x37, 0x30, 0xf6, 0xbd, 0xf1, 0x6e, 0x7f, 0xcf, 0xaf, 0xde, 0xd7, + 0x75, 0xdd, 0xba, 0x5c, 0x3e, 0x3e, 0x9c, 0x5a, 0x78, 0xba, 0xab, 0x4c, 0x29, 0x1e, 0x2c, 0xb7, + 0xab, 0xf1, 0xba, 0xae, 0x6e, 0xa3, 0xed, 0xcc, 0x98, 0xf7, 0xf4, 0xc1, 0x61, 0x4c, 0x1b, 0xc6, + 0x61, 0x7a, 0xa2, 0x5e, 0xa4, 0xfa, 0xf2, 0x44, 0xad, 0xdf, 0xc0, 0x7e, 0x68, 0xa4, 0x1b, 0xab, + 0x92, 0x22, 0xd0, 0x0f, 0x9c, 0xc5, 0xb9, 0x99, 0x16, 0xcd, 0xed, 0x52, 0x3a, 0x5f, 0x69, 0x3e, + 0x8d, 0xaa, 0xe7, 0x5a, 0x22, 0x1d, 0xa9, 0x1c, 0x56, 0xe6, 0xf5, 0x62, 0x28, 0xdf, 0x56, 0x09, + 0xde, 0xdd, 0xce, 0xdb, 0xa6, 0x28, 0x79, 0x4a, 0xc4, 0xf9, 0x88, 0x70, 0x4b, 0xce, 0xf2, 0xa6, + 0x67, 0x5b, 0xce, 0x05, 0x46, 0x1a, 0x39, 0xa7, 0x75, 0xff, 0xb6, 0xdb, 0x2e, 0xe2, 0x06, 0xae, + 0x35, 0x41, 0x4a, 0x6f, 0xc4, 0xb2, 0x6d, 0xc7, 0xc9, 0xbd, 0xb1, 0xb3, 0x55, 0xad, 0xd9, 0x05, + 0xf1, 0xbd, 0x5b, 0xcb, 0xd7, 0x23, 0xd9, 0xab, 0x27, 0x8d, 0xb1, 0xea, 0xc3, 0xf5, 0xc0, 0x94, + 0x22, 0xc5, 0x80, 0xa6, 0x63, 0x1d, 0xaa, 0xf3, 0x02, 0x05, 0x91, 0x35, 0x33, 0xdc, 0x2a, 0x3b, + 0xb2, 0xcd, 0x56, 0x89, 0x65, 0xd7, 0xb8, 0x5e, 0xab, 0x97, 0xf1, 0x78, 0x36, 0x9b, 0x45, 0x7a, + 0xc1, 0xaf, 0xa7, 0x98, 0x9f, 0x15, 0xea, 0x6c, 0xec, 0xca, 0x19, 0x49, 0xb2, 0xa9, 0xd1, 0xbd, + 0x69, 0x29, 0x31, 0x04, 0x43, 0x6b, 0x45, 0x50, 0x04, 0x94, 0x58, 0x9b, 0xcf, 0x35, 0x20, 0x29, + 0xc0, 0x6d, 0x1e, 0x80, 0x5c, 0x5b, 0x9f, 0x34, 0x1e, 0x80, 0x05, 0xb5, 0xd3, 0xe8, 0x98, 0x39, + 0x52, 0x6f, 0x56, 0x3b, 0x9c, 0xd7, 0xc5, 0xf4, 0xe2, 0x7c, 0xd4, 0xeb, 0x7b, 0xdb, 0x4e, 0xd6, + 0xa7, 0xc9, 0x7e, 0x1d, 0x37, 0x96, 0x1c, 0xcc, 0xb3, 0xeb, 0x24, 0x3c, 0x6b, 0x09, 0x98, 0x69, + 0x78, 0x33, 0xdd, 0xa6, 0x47, 0x43, 0x56, 0xb7, 0x09, 0x9f, 0xa7, 0x9b, 0xe1, 0x46, 0x09, 0xaa, + 0xf9, 0x24, 0xda, 0x64, 0x23, 0x7f, 0x7e, 0xbb, 0xed, 0x7d, 0x8e, 0xec, 0x23, 0x0e, 0x3d, 0x1e, + 0x38, 0x72, 0x41, 0xfc, 0xfc, 0x9a, 0xae, 0x08, 0xd6, 0x36, 0x91, 0x89, 0x8b, 0x83, 0xd9, 0xbc, + 0x4d, 0x48, 0x6d, 0xaa, 0xd1, 0x6c, 0x3d, 0xbe, 0x2d, 0xf9, 0x28, 0xdb, 0x1e, 0xb1, 0x54, 0x34, + 0x65, 0x3a, 0xcd, 0xbd, 0x8f, 0x5e, 0x9e, 0xa8, 0xcd, 0x41, 0x7d, 0x7d, 0x3d, 0x56, 0xaf, 0x52, + 0x6d, 0x74, 0x6b, 0xf4, 0xfa, 0xbd, 0xe1, 0xf4, 0xc6, 0xf6, 0x49, 0xe8, 0x5e, 0xae, 0x3e, 0x1b, + 0x1d, 0x9b, 0xdb, 0xd4, 0x09, 0x09, 0x32, 0x10, 0x06, 0xc5, 0xc2, 0x4c, 0x46, 0xbb, 0x64, 0xb7, + 0xa3, 0xa6, 0xad, 0x44, 0x3d, 0xe9, 0x5c, 0xfb, 0xc8, 0xad, 0xd0, 0x94, 0x27, 0x36, 0x37, 0x50, + 0xb1, 0xe4, 0xa9, 0x12, 0x68, 0xf2, 0x71, 0x6b, 0xef, 0xc7, 0x8b, 0xb5, 0xb2, 0x9a, 0xeb, 0xfd, + 0x4a, 0x65, 0xa3, 0xe0, 0x83, 0xd8, 0xa7, 0xfd, 0xc8, 0x9a, 0xf7, 0xad, 0xea, 0xc5, 0xa8, 0x47, + 0x89, 0x79, 0x88, 0x54, 0xaf, 0xae, 0x62, 0xba, 0xcf, 0x82, 0xc6, 0x81, 0x37, 0x33, 0xb3, 0xd7, + 0x28, 0x4c, 0x5b, 0x76, 0xa7, 0xe6, 0x7e, 0x17, 0xdb, 0x43, 0x73, 0x6f, 0x44, 0x5b, 0x39, 0x3d, + 0x86, 0xa3, 0xcd, 0xa0, 0xa1, 0x26, 0x4a, 0xbe, 0xf3, 0x0f, 0xbe, 0x68, 0x8e, 0xa2, 0x6a, 0x2f, + 0x90, 0x6f, 0x99, 0x36, 0xe9, 0xab, 0xe2, 0x1c, 0x7f, 0x68, 0xc7, 0x9d, 0x3f, 0xb3, 0x4e, 0xcc, + 0x49, 0x46, 0xbb, 0x51, 0x14, 0xec, 0xe7, 0xa6, 0x26, 0x1a, 0xca, 0x68, 0x7f, 0x18, 0x0d, 0x87, + 0xbb, 0xa6, 0x99, 0xf0, 0x18, 0xd6, 0xf6, 0x19, 0x97, 0x46, 0x49, 0x65, 0xc2, 0xc0, 0xd5, 0x88, + 0x1a, 0x37, 0x09, 0x87, 0xd1, 0x28, 0x3f, 0x4c, 0x6e, 0x5b, 0xbf, 0x0a, 0xd6, 0xb7, 0xc4, 0x3c, + 0x30, 0x5a, 0xc9, 0xea, 0x4b, 0x65, 0x32, 0xf6, 0xc8, 0x5e, 0x3d, 0x58, 0xcb, 0xc8, 0x62, 0x71, + 0xc2, 0x76, 0x72, 0x34, 0x51, 0x65, 0x45, 0xc2, 0x83, 0x66, 0xe0, 0x93, 0x7a, 0x33, 0xdc, 0x00, + 0xd3, 0x73, 0xd9, 0x79, 0xbb, 0xaa, 0xc9, 0x52, 0x9c, 0x5b, 0xd5, 0x7a, 0x1a, 0xc7, 0x00, 0x36, + 0x57, 0xf0, 0xd8, 0x90, 0xb4, 0xdb, 0x94, 0x28, 0x40, 0x45, 0xe0, 0xa0, 0xb9, 0x75, 0x2d, 0xdd, + 0x24, 0x5b, 0x43, 0xaf, 0x79, 0x13, 0xed, 0xa3, 0x6e, 0x4a, 0x8c, 0x8a, 0x80, 0x2d, 0x2e, 0x3d, + 0x18, 0x3a, 0x75, 0xe4, 0x0f, 0xc2, 0x8c, 0xad, 0x1e, 0x47, 0x6b, 0x10, 0x0f, 0x37, 0xa7, 0x75, + 0xb6, 0x4c, 0x0c, 0xa3, 0xdc, 0x2d, 0x75, 0x3c, 0x94, 0x0b, 0x6e, 0x0c, 0x18, 0x7b, 0x2f, 0xdf, + 0x8b, 0x52, 0xb9, 0xdb, 0x09, 0xe5, 0xef, 0x5e, 0x24, 0x70, 0x22, 0xec, 0x66, 0x83, 0xfe, 0x4b, + 0x47, 0x0c, 0xe5, 0x6e, 0x27, 0x54, 0xbb, 0xa5, 0x4d, 0x08, 0xf0, 0x49, 0xb8, 0x92, 0x4c, 0xf0, + 0x09, 0x15, 0x10, 0x66, 0x1c, 0xc4, 0x31, 0xc2, 0x81, 0x90, 0x5c, 0x05, 0x90, 0xa6, 0x31, 0x72, + 0x1f, 0x15, 0xed, 0x6e, 0xa1, 0x76, 0x3b, 0x4e, 0xd7, 0x84, 0x05, 0x17, 0x18, 0x87, 0x29, 0x6b, + 0x75, 0x44, 0xa7, 0xdb, 0x71, 0xe8, 0xe3, 0x2e, 0x19, 0x04, 0x63, 0xe8, 0x72, 0x81, 0x87, 0x50, + 0x48, 0x88, 0x97, 0xc5, 0x8f, 0x60, 0x57, 0x92, 0x51, 0x21, 0x26, 0x2e, 0x88, 0x85, 0x1d, 0x1a, + 0x22, 0x21, 0x84, 0x14, 0xbe, 0x3c, 0x6d, 0x1e, 0xbd, 0x49, 0x28, 0x11, 0xec, 0xc6, 0xc8, 0x3d, + 0xbd, 0xff, 0xb8, 0x20, 0xec, 0x91, 0xcb, 0xdb, 0xfd, 0xe5, 0x7b, 0xc0, 0xb7, 0x90, 0x42, 0xff, + 0xbd, 0xfc, 0x26, 0x32, 0xc8, 0x39, 0xc2, 0x01, 0x13, 0x2f, 0xc8, 0x47, 0xe5, 0x1f, 0xdd, 0x87, + 0x9f, 0x6f, 0xb0, 0x23, 0x3e, 0xfd, 0x3c, 0x79, 0xa0, 0x6e, 0x69, 0x92, 0x31, 0x2e, 0x70, 0x7a, + 0xbd, 0x67, 0xc0, 0x43, 0xc4, 0x04, 0x92, 0x71, 0x01, 0x61, 0x41, 0x5f, 0xdc, 0x59, 0xc1, 0x3f, + 0x3a, 0x22, 0xea, 0xfe, 0x7f, 0xf1, 0xff, 0x60, 0x31, 0xf2, 0x20, 0x65, 0xe5, 0x1f, 0xdd, 0x0d, + 0x79, 0xa4, 0x77, 0xef, 0xf8, 0x94, 0xc4, 0xec, 0xe5, 0xaf, 0xd1, 0x45, 0x0f, 0xe5, 0xdd, 0x52, + 0x47, 0x7c, 0x56, 0x76, 0xf1, 0xf1, 0x1b, 0xf2, 0x27, 0x5c, 0xd5, 0x96, 0x6f, 0x9c, 0x08, 0x00, + 0x00 +}; // Autogenerated from wled00/data/liveview.htm, do not edit!! -const char PAGE_liveview[] PROGMEM = R"=====( -WLED Live Preview
)====="; - - -// Autogenerated from wled00/data/liveviewws.htm, do not edit!! -const char PAGE_liveviewws[] PROGMEM = R"=====( -WLED Live Preview
)====="; +const uint16_t PAGE_liveview_length = 963; +const uint8_t PAGE_liveview[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0x9d, 0x55, 0x6d, 0x6f, 0xdb, 0x36, + 0x10, 0xfe, 0x9e, 0x5f, 0xa1, 0x32, 0xab, 0x4b, 0xc2, 0x32, 0x2d, 0xbb, 0xaf, 0xb3, 0x44, 0x07, + 0xed, 0x9a, 0x0f, 0x03, 0x82, 0x25, 0x80, 0x33, 0x04, 0x43, 0x10, 0xa0, 0xb4, 0x78, 0xb6, 0xd8, + 0xd0, 0xa4, 0x41, 0x9e, 0xac, 0x19, 0xaa, 0xfe, 0xfb, 0x40, 0xd9, 0x71, 0xb0, 0xa1, 0xd8, 0xd0, + 0xe9, 0x83, 0x78, 0x24, 0xef, 0xb9, 0xf7, 0x3b, 0x16, 0x2f, 0x3e, 0x5f, 0xff, 0x72, 0xfb, 0xc7, + 0xcd, 0x65, 0x52, 0xe1, 0xc6, 0xcc, 0x8b, 0xe3, 0x1f, 0xa4, 0x9a, 0x17, 0x1b, 0x40, 0x99, 0x58, + 0xb9, 0x01, 0x41, 0x76, 0x1a, 0x9a, 0xad, 0xf3, 0x48, 0x92, 0xb3, 0xd2, 0x59, 0x04, 0x8b, 0x82, + 0x34, 0x5a, 0x61, 0x25, 0x14, 0xec, 0x74, 0x09, 0xa3, 0x7e, 0x93, 0x6a, 0xab, 0x51, 0x4b, 0x33, + 0x0a, 0xa5, 0x34, 0x20, 0x26, 0xe9, 0x46, 0x5b, 0xbd, 0xa9, 0x37, 0x4f, 0x7b, 0x72, 0x94, 0x79, + 0x56, 0x56, 0xd2, 0x07, 0x40, 0x41, 0x6a, 0x5c, 0x8d, 0x3e, 0x90, 0xbf, 0xa9, 0xc2, 0x0a, 0x36, + 0x30, 0x2a, 0x9d, 0x71, 0x9e, 0x24, 0x27, 0x65, 0xe7, 0xd3, 0xfe, 0x23, 0xf3, 0x02, 0x35, 0x1a, + 0x98, 0x9f, 0xdd, 0x5d, 0x5d, 0x7e, 0x4e, 0xae, 0xf4, 0x0e, 0x92, 0x1b, 0x0f, 0xd1, 0xbc, 0x62, + 0x7c, 0xb8, 0x29, 0x02, 0xee, 0x23, 0xc3, 0xd2, 0xa9, 0x7d, 0xbb, 0x91, 0x7e, 0xad, 0xed, 0x2c, + 0xeb, 0xce, 0x4b, 0x69, 0x77, 0xed, 0x52, 0x96, 0x8f, 0x6b, 0xef, 0x6a, 0xab, 0x66, 0xe7, 0x59, + 0x96, 0xe5, 0x2b, 0x6d, 0x10, 0xfc, 0x6c, 0xe9, 0xf5, 0xba, 0x42, 0x0b, 0x21, 0xd0, 0xc9, 0xfb, + 0xb7, 0x2f, 0x59, 0xde, 0x7b, 0x33, 0x9b, 0x64, 0xd9, 0xcb, 0xbc, 0x82, 0x78, 0x77, 0xa0, 0xb7, + 0x2e, 0x68, 0xd4, 0xce, 0xce, 0xe4, 0x32, 0x38, 0x53, 0x23, 0x74, 0x67, 0xc5, 0xf8, 0xa0, 0xae, + 0x08, 0xa5, 0xd7, 0x5b, 0x9c, 0x9f, 0xed, 0xa4, 0x4f, 0x9a, 0x90, 0xe2, 0xc6, 0xd5, 0x28, 0x6c, + 0x6d, 0x4c, 0xbe, 0xaa, 0x6d, 0x19, 0x51, 0x49, 0xbd, 0x55, 0x12, 0x81, 0xb2, 0x56, 0xaf, 0xa8, + 0x72, 0x65, 0xbd, 0x01, 0x8b, 0xbc, 0xd2, 0x4a, 0x81, 0x65, 0x1e, 0xb0, 0xf6, 0x36, 0x29, 0x0d, + 0x48, 0x7f, 0xab, 0x37, 0xe0, 0x6a, 0xa4, 0xbd, 0x0c, 0x96, 0xee, 0x9c, 0x56, 0x07, 0x5a, 0x04, + 0xc0, 0xa7, 0xcb, 0x83, 0xb0, 0x74, 0xfa, 0x36, 0x63, 0x2c, 0x5f, 0x01, 0x96, 0x15, 0x25, 0x7c, + 0xfc, 0x35, 0x38, 0x3b, 0x36, 0x7a, 0x07, 0x84, 0x71, 0xac, 0xc0, 0x52, 0x10, 0x73, 0x0a, 0xdc, + 0x3d, 0x7e, 0xfb, 0x46, 0xbf, 0x27, 0xfb, 0x5f, 0xc4, 0x66, 0x8c, 0xa5, 0xc0, 0xa3, 0x40, 0xca, + 0xd8, 0xb3, 0xb4, 0x36, 0x3a, 0x88, 0x82, 0x18, 0x6d, 0x41, 0xfa, 0xd1, 0xda, 0x4b, 0xa5, 0xc1, + 0x22, 0xfd, 0x39, 0x53, 0xb0, 0x4e, 0x49, 0x6a, 0x05, 0x70, 0x03, 0x2a, 0x70, 0x03, 0x76, 0x8d, + 0x55, 0xbe, 0x72, 0x9e, 0x6a, 0x91, 0xe5, 0xba, 0xb0, 0xb9, 0x1e, 0x0e, 0x59, 0x8f, 0x77, 0x47, + 0xa6, 0x7b, 0xfd, 0x90, 0xbb, 0x23, 0xe7, 0xfc, 0xdd, 0x60, 0x40, 0x9d, 0x70, 0x3c, 0xd4, 0xcb, + 0x80, 0x5e, 0xdb, 0x35, 0x9d, 0x32, 0x96, 0xe2, 0x50, 0x90, 0x73, 0x32, 0x74, 0xa9, 0x2e, 0xec, + 0x68, 0x32, 0x18, 0xd0, 0x78, 0x90, 0x12, 0xd6, 0xc5, 0x95, 0x91, 0xf4, 0x14, 0xc8, 0x35, 0xe0, + 0xa5, 0x81, 0x48, 0x7e, 0xda, 0xff, 0xaa, 0x28, 0x89, 0x09, 0x27, 0x8c, 0xf7, 0xe9, 0xe1, 0xcf, + 0x89, 0x17, 0x98, 0xfe, 0x48, 0x20, 0xde, 0x64, 0xac, 0x63, 0xbc, 0x94, 0x31, 0xbe, 0xf4, 0x29, + 0x93, 0x14, 0x58, 0xfb, 0xc3, 0xd1, 0xec, 0x18, 0xeb, 0x4e, 0xa5, 0xb0, 0x38, 0x54, 0x41, 0xa3, + 0xad, 0x72, 0x0d, 0x37, 0xae, 0x94, 0xf1, 0x98, 0x57, 0x1e, 0x56, 0x5c, 0x5b, 0x05, 0x7f, 0x5e, + 0xaf, 0x28, 0xb9, 0x68, 0x02, 0x61, 0xf3, 0x8c, 0xb5, 0xe8, 0xf7, 0x6d, 0x13, 0x04, 0xba, 0x2d, + 0x3f, 0x22, 0x9a, 0xd0, 0x1d, 0x6c, 0x02, 0xd6, 0x76, 0x51, 0x50, 0x18, 0x0c, 0x9a, 0xc0, 0x3d, + 0x48, 0xb5, 0x5f, 0xa0, 0x44, 0x10, 0x42, 0xdc, 0xc1, 0x72, 0xe1, 0xca, 0x47, 0x40, 0x7e, 0x7d, + 0x73, 0xf9, 0x1b, 0x6b, 0x02, 0x0f, 0x60, 0x15, 0x25, 0xed, 0x2b, 0xb3, 0x7b, 0x35, 0x43, 0x5f, + 0x43, 0x47, 0x58, 0x0e, 0x26, 0x40, 0x6b, 0x00, 0x13, 0x10, 0xff, 0xb0, 0x26, 0x45, 0x01, 0x7c, + 0x2b, 0xb1, 0x8a, 0x7d, 0x99, 0x5a, 0x81, 0x3c, 0x18, 0x5d, 0x02, 0x9d, 0xa4, 0xc8, 0xc1, 0xaa, + 0x70, 0xa7, 0xb1, 0xa2, 0x64, 0x4c, 0xd8, 0xc5, 0x68, 0x32, 0x8b, 0x45, 0x9a, 0x64, 0x8c, 0x87, + 0xad, 0xd1, 0xd8, 0x9f, 0xa6, 0x5a, 0x00, 0x77, 0x5e, 0xaf, 0xb5, 0xe5, 0x1e, 0xb6, 0x46, 0x96, + 0x40, 0x49, 0x85, 0xb8, 0x25, 0x29, 0x89, 0x8e, 0xe5, 0xf6, 0x29, 0xf1, 0x31, 0xab, 0x7a, 0x28, + 0xc8, 0x98, 0x0c, 0xed, 0x7d, 0xf6, 0xc0, 0x52, 0xda, 0x04, 0x61, 0xa1, 0x49, 0x4e, 0x1e, 0x50, + 0x3d, 0x24, 0xe3, 0x08, 0x62, 0xdc, 0x59, 0xb7, 0x05, 0x2b, 0x4e, 0xa9, 0x60, 0xed, 0xf7, 0x1d, + 0xeb, 0xba, 0x26, 0xf0, 0xa5, 0xb6, 0xd2, 0xef, 0x6f, 0xf7, 0x5b, 0x10, 0x44, 0x7a, 0x2f, 0xf7, + 0xcb, 0x7a, 0xb5, 0x02, 0x4f, 0xd2, 0x26, 0x70, 0xa9, 0xd4, 0xe5, 0x0e, 0x2c, 0x5e, 0xe9, 0x80, + 0x60, 0xc1, 0x53, 0xb2, 0x81, 0x10, 0xe4, 0x1a, 0x48, 0x1a, 0xcb, 0x3c, 0xc6, 0x5c, 0xaf, 0x28, + 0xb9, 0x77, 0xcb, 0xaf, 0x50, 0x62, 0xf2, 0x31, 0xc2, 0x3f, 0xf5, 0xf0, 0x07, 0x22, 0x84, 0x40, + 0xb7, 0xe8, 0x8b, 0x94, 0x97, 0xd2, 0x18, 0x0a, 0x5c, 0x49, 0x94, 0x8c, 0x1d, 0x23, 0x19, 0x8d, + 0xff, 0x5d, 0x5b, 0xfc, 0xd0, 0xa3, 0x28, 0x44, 0x3d, 0x07, 0x8e, 0x5c, 0xaf, 0xe8, 0xfb, 0x77, + 0x2f, 0x04, 0x44, 0x47, 0x0f, 0x4d, 0x9f, 0x47, 0xcc, 0x7f, 0x76, 0x54, 0x8c, 0x54, 0xea, 0xc4, + 0x54, 0x08, 0xb8, 0x9f, 0x3c, 0x5c, 0xbc, 0x99, 0x4d, 0x8f, 0xad, 0xe5, 0x8e, 0xad, 0x25, 0x5e, + 0x33, 0x1c, 0x8a, 0x2f, 0x7e, 0xbd, 0xa4, 0x3f, 0xb5, 0x70, 0xaf, 0x1f, 0xba, 0xb4, 0x5f, 0x87, + 0x93, 0x13, 0x35, 0x7d, 0xe8, 0xd8, 0x97, 0xbe, 0x91, 0x5e, 0x3f, 0x37, 0x52, 0xfe, 0xbf, 0x1b, + 0xa9, 0x7b, 0xae, 0xc1, 0xd2, 0xd9, 0xe0, 0x0c, 0x70, 0xf0, 0xde, 0x79, 0x4a, 0x6e, 0x00, 0x1e, + 0x93, 0xbb, 0x45, 0xd2, 0x6f, 0x67, 0x24, 0x05, 0xd6, 0x75, 0xac, 0x8b, 0xa5, 0x76, 0x1a, 0x82, + 0xfd, 0xe8, 0x3c, 0xcc, 0xcc, 0x62, 0x7c, 0x78, 0x6f, 0xe2, 0xc4, 0x4e, 0x9c, 0x35, 0x4e, 0x2a, + 0x41, 0x16, 0x94, 0x91, 0x79, 0xa1, 0xf4, 0x2e, 0xd1, 0x4a, 0x1c, 0x6c, 0x98, 0x17, 0x63, 0xa5, + 0x77, 0xf3, 0x62, 0x1c, 0x19, 0x23, 0x2a, 0xbe, 0x55, 0x7f, 0x01, 0x51, 0x65, 0x4f, 0x41, 0xc1, + 0x06, 0x00, 0x00 +}; + + +// Autogenerated from wled00/data/liveviewws2D.htm, do not edit!! +const uint16_t PAGE_liveviewws2D_length = 873; +const uint8_t PAGE_liveviewws2D[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0x6d, 0x14, 0x6b, 0x6f, 0xdb, 0x36, + 0xf0, 0xbb, 0x7f, 0x85, 0xc2, 0x0d, 0x29, 0x69, 0x33, 0x94, 0xed, 0xb6, 0x5b, 0x16, 0x8b, 0x1e, + 0xd6, 0x34, 0xc0, 0x0a, 0x64, 0xab, 0x81, 0x64, 0x08, 0x06, 0xc3, 0x40, 0x69, 0xea, 0x6c, 0x71, + 0x95, 0x48, 0x83, 0x3c, 0x5b, 0xd6, 0x6c, 0xfd, 0xf7, 0x81, 0xb2, 0x93, 0x65, 0xe8, 0xf4, 0x81, + 0xba, 0xf7, 0xfb, 0x2e, 0xbb, 0xf8, 0xf8, 0xf9, 0xf6, 0xf1, 0xcf, 0xd9, 0x5d, 0x52, 0x60, 0x55, + 0x4e, 0xb3, 0xf3, 0x0b, 0x2a, 0x9f, 0x66, 0x15, 0xa0, 0x4a, 0xac, 0xaa, 0x40, 0x92, 0x9d, 0x81, + 0x7a, 0xe3, 0x3c, 0x92, 0xa4, 0xa7, 0x9d, 0x45, 0xb0, 0x28, 0x49, 0x6d, 0x72, 0x2c, 0x64, 0x0e, + 0x3b, 0xa3, 0xe1, 0xaa, 0x43, 0xb8, 0xb1, 0x06, 0x8d, 0x2a, 0xaf, 0x82, 0x56, 0x25, 0xc8, 0x11, + 0xaf, 0x8c, 0x35, 0xd5, 0xb6, 0x7a, 0xc6, 0xc9, 0xd9, 0x66, 0x4f, 0x17, 0xca, 0x07, 0x40, 0x49, + 0xb6, 0xb8, 0xba, 0xba, 0x26, 0xff, 0x71, 0x85, 0x05, 0x54, 0x70, 0xa5, 0x5d, 0xe9, 0x3c, 0x49, + 0x5e, 0x9c, 0x7d, 0x37, 0xee, 0x3e, 0x32, 0xcd, 0xd0, 0x60, 0x09, 0xd3, 0xde, 0xd3, 0xfd, 0xdd, + 0xc7, 0xe4, 0xde, 0xec, 0x20, 0x99, 0x79, 0x88, 0xe1, 0x65, 0xe9, 0x89, 0x93, 0x05, 0x6c, 0x4a, + 0x98, 0x2e, 0x5d, 0xde, 0x1c, 0x2a, 0xe5, 0xd7, 0xc6, 0xde, 0x0c, 0xdb, 0x2c, 0x3d, 0x51, 0xb3, + 0xf4, 0x94, 0x5a, 0xe4, 0x4e, 0x33, 0xad, 0xec, 0x4e, 0x85, 0xa4, 0x67, 0x72, 0x49, 0x22, 0x4c, + 0xa6, 0x59, 0x7a, 0xa2, 0x4d, 0xb3, 0xa0, 0xbd, 0xd9, 0xe0, 0xb4, 0xb7, 0x53, 0x3e, 0xd1, 0x32, + 0x77, 0x7a, 0x5b, 0x81, 0x45, 0xb1, 0x06, 0xbc, 0x2b, 0x21, 0x82, 0x1f, 0x9a, 0x4f, 0x39, 0x3d, + 0xa9, 0x31, 0x5e, 0x42, 0x1e, 0x24, 0x21, 0x1c, 0x0b, 0xef, 0x10, 0x4b, 0xc8, 0xe5, 0xc5, 0x68, + 0xb2, 0xda, 0x5a, 0x8d, 0xc6, 0xd9, 0x24, 0x00, 0xde, 0x76, 0x66, 0x29, 0x3b, 0x68, 0x71, 0xaa, + 0x9b, 0xf8, 0xe9, 0xba, 0x5f, 0x1b, 0x9b, 0xbb, 0x5a, 0x18, 0x6b, 0xc1, 0x3f, 0x75, 0x05, 0xd4, + 0xa2, 0x00, 0xb3, 0x2e, 0xf0, 0x1b, 0xf6, 0xaf, 0x1d, 0xb9, 0x7d, 0x65, 0x69, 0xd2, 0x45, 0x86, + 0x7b, 0xa9, 0x63, 0x50, 0xb7, 0xb1, 0x50, 0x7b, 0xa4, 0x64, 0x9c, 0x13, 0x36, 0x31, 0x2b, 0xaa, + 0x71, 0xcf, 0x0e, 0x51, 0xa4, 0x0e, 0x13, 0xf4, 0xcd, 0xa1, 0x0e, 0x12, 0xdd, 0x46, 0x9c, 0x6d, + 0xd6, 0xa1, 0xd5, 0x0a, 0x75, 0x41, 0x91, 0x1d, 0x5a, 0xb3, 0xa2, 0x75, 0xb8, 0xbc, 0xac, 0x83, + 0xf0, 0xa0, 0xf2, 0xe6, 0x01, 0x15, 0x82, 0x94, 0xf2, 0x09, 0x96, 0x0f, 0x4e, 0x7f, 0x05, 0x14, + 0x9f, 0x67, 0x77, 0xbf, 0xb3, 0x3a, 0x88, 0x00, 0x36, 0xa7, 0xe4, 0xf0, 0xa6, 0xdc, 0xbd, 0xb9, + 0x41, 0xbf, 0x85, 0x96, 0xb0, 0x09, 0x94, 0x01, 0x0e, 0x25, 0x60, 0x82, 0xf2, 0x6c, 0xbb, 0x74, + 0x5a, 0xc5, 0xb4, 0x39, 0x48, 0x14, 0x1b, 0x85, 0x45, 0xec, 0x2b, 0xf7, 0x12, 0x44, 0x28, 0x8d, + 0x06, 0x3a, 0xe2, 0x20, 0xc0, 0xe6, 0xe1, 0xc9, 0x60, 0x41, 0x49, 0x4a, 0xd8, 0xcf, 0x57, 0xa3, + 0x9b, 0x9d, 0x33, 0x79, 0x32, 0x64, 0x22, 0x6c, 0x4a, 0x83, 0x1d, 0x95, 0x5b, 0x89, 0xc2, 0x79, + 0xb3, 0x36, 0x56, 0x78, 0xd8, 0x94, 0x4a, 0x03, 0x25, 0x05, 0xe2, 0x86, 0x70, 0x52, 0x07, 0xc2, + 0x26, 0x5e, 0x94, 0x60, 0xd7, 0x58, 0x4c, 0x47, 0x97, 0x97, 0xd4, 0x0e, 0x24, 0x49, 0xc9, 0xc0, + 0xcf, 0x87, 0x0b, 0xc6, 0x69, 0x1d, 0xa4, 0x85, 0x3a, 0x79, 0xc9, 0x80, 0xda, 0x01, 0x49, 0xa3, + 0x12, 0x13, 0xce, 0xba, 0x0d, 0x58, 0x49, 0x99, 0x9c, 0x1e, 0xfe, 0x3f, 0xa5, 0xb6, 0xad, 0x83, + 0x58, 0x1a, 0xab, 0x7c, 0xf3, 0xd8, 0x6c, 0x40, 0x12, 0xe5, 0xbd, 0x6a, 0x96, 0xdb, 0xd5, 0x0a, + 0x3c, 0xe1, 0x75, 0x10, 0x2a, 0xcf, 0xef, 0x76, 0x60, 0xf1, 0xde, 0x04, 0x04, 0x0b, 0x9e, 0x92, + 0x0a, 0x42, 0x50, 0x6b, 0x20, 0x1c, 0xe5, 0xf4, 0x10, 0xab, 0x6d, 0x56, 0x94, 0xcc, 0xdd, 0xf2, + 0x2f, 0xd0, 0x98, 0xfc, 0x12, 0xd5, 0x3f, 0x74, 0xea, 0x0b, 0x22, 0xa5, 0x44, 0xf7, 0x80, 0xde, + 0xd8, 0xb5, 0xd0, 0xaa, 0x2c, 0x29, 0x8a, 0x5c, 0xa1, 0x62, 0xec, 0x5c, 0xc3, 0x18, 0xf6, 0x1f, + 0xc6, 0xe2, 0x75, 0xa7, 0x45, 0x21, 0xfa, 0x39, 0x49, 0xc4, 0xa6, 0xfe, 0xf8, 0xc3, 0x85, 0xc4, + 0xf9, 0x70, 0x71, 0x3c, 0x8e, 0x23, 0x30, 0x5a, 0x1c, 0x8f, 0x17, 0xb1, 0xd1, 0x1e, 0x70, 0xeb, + 0xed, 0x24, 0x9a, 0xf0, 0x12, 0xe7, 0xe3, 0x45, 0x2c, 0xde, 0xfc, 0xed, 0x82, 0x2b, 0xf9, 0x9b, + 0xc2, 0x42, 0x54, 0xc6, 0xd2, 0xf3, 0xe0, 0xa5, 0xfe, 0x65, 0xc8, 0x52, 0xcb, 0xb8, 0x39, 0x09, + 0xac, 0x4a, 0xe7, 0x3c, 0x7d, 0x96, 0xb9, 0x52, 0x7d, 0xcf, 0xd2, 0xf1, 0x69, 0xc4, 0x40, 0xbe, + 0x9b, 0xac, 0x9c, 0xa7, 0x8d, 0x14, 0xef, 0x27, 0x4d, 0x66, 0x27, 0xcd, 0x60, 0xc0, 0x22, 0x61, + 0x1f, 0x09, 0xfb, 0xcc, 0x4f, 0xf6, 0x83, 0x01, 0xd3, 0xb8, 0x17, 0x2b, 0x53, 0x96, 0x0f, 0x71, + 0xcd, 0xe4, 0x17, 0xbf, 0x5e, 0xd2, 0xef, 0x0f, 0x38, 0x87, 0x45, 0xcb, 0xbb, 0xff, 0x60, 0xf4, + 0x02, 0x8d, 0x17, 0x2d, 0xfb, 0xc2, 0xa3, 0xc2, 0x12, 0xd6, 0xc6, 0xce, 0x14, 0x16, 0x94, 0x75, + 0xb8, 0xf2, 0x9a, 0xee, 0xfb, 0x6a, 0x60, 0x78, 0xd3, 0x57, 0x5c, 0xbc, 0xeb, 0x2b, 0x3e, 0xe4, + 0xe3, 0x7e, 0x17, 0xe1, 0xec, 0xd3, 0x49, 0x26, 0x3a, 0xa1, 0x8c, 0xc3, 0x40, 0xbe, 0x6d, 0xff, + 0x9d, 0x61, 0xed, 0x6c, 0x70, 0x25, 0x08, 0xf0, 0xde, 0x79, 0x4a, 0x66, 0x00, 0x5f, 0x93, 0xa7, + 0x87, 0xa4, 0x43, 0x6f, 0x08, 0x47, 0xd6, 0xb6, 0xac, 0x3d, 0xcf, 0xe7, 0xb7, 0xed, 0xf3, 0x10, + 0xcc, 0xdf, 0xcf, 0xdd, 0x7b, 0xde, 0xe0, 0xe3, 0x91, 0xbe, 0xda, 0xb6, 0xd7, 0x9b, 0x3d, 0xe4, + 0x01, 0xf0, 0xd1, 0x54, 0xe0, 0xb6, 0x48, 0xbb, 0x41, 0x7a, 0xbd, 0xf6, 0x2d, 0x1f, 0xbf, 0x1f, + 0x32, 0xd6, 0xb2, 0x5e, 0x96, 0x9e, 0x8f, 0x48, 0x96, 0x9e, 0xee, 0x4d, 0xda, 0x5d, 0xd7, 0x7f, + 0x00, 0x47, 0xe8, 0xd7, 0x03, 0x73, 0x05, 0x00, 0x00 +}; // Autogenerated from wled00/data/404.htm, do not edit!! -const char PAGE_404[] PROGMEM = R"=====(Not found -

404 Not Found

Akemi does not know where you are headed...

- -)====="; +const uint16_t PAGE_404_length = 878; +const uint8_t PAGE_404[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0x6d, 0x54, 0x5b, 0x6f, 0xb3, 0x38, + 0x10, 0x7d, 0xcf, 0xaf, 0x60, 0x59, 0xad, 0xbe, 0x87, 0x36, 0x81, 0x24, 0xa4, 0x4d, 0x08, 0x61, + 0x65, 0x08, 0xb9, 0xf4, 0x92, 0xe6, 0x46, 0xd3, 0xf4, 0xcd, 0x60, 0x07, 0xdc, 0x80, 0x4d, 0x6d, + 0x93, 0x90, 0x56, 0xfd, 0xef, 0x2b, 0xa0, 0x9f, 0xb6, 0xd2, 0xee, 0x48, 0x5c, 0xe6, 0x8c, 0x7c, + 0x7c, 0x66, 0x3c, 0x63, 0xeb, 0x8f, 0xf1, 0x93, 0xbb, 0xdd, 0x2f, 0x3d, 0x25, 0x96, 0x69, 0x62, + 0x5b, 0xdf, 0x6f, 0x0c, 0x91, 0x6d, 0xa5, 0x58, 0x42, 0x25, 0x8c, 0x21, 0x17, 0x58, 0x8e, 0xd4, + 0x5c, 0x1e, 0x9a, 0x7d, 0xf5, 0x1b, 0x6d, 0x84, 0x8c, 0x4a, 0x4c, 0xe5, 0x48, 0x3d, 0x13, 0x24, + 0xe3, 0x11, 0xc2, 0x27, 0x12, 0xe2, 0x66, 0xe5, 0xa8, 0x0a, 0x85, 0x29, 0x1e, 0xa9, 0x27, 0x82, + 0xcf, 0x19, 0xe3, 0xf2, 0xf7, 0x9a, 0x1a, 0x95, 0x31, 0x4e, 0x71, 0x33, 0x64, 0x09, 0xe3, 0xea, + 0x0f, 0x9a, 0x3f, 0x3b, 0x95, 0xa9, 0xb6, 0x25, 0x89, 0x4c, 0xb0, 0xbd, 0x60, 0x52, 0x39, 0xb0, + 0x9c, 0x22, 0x4b, 0xab, 0x01, 0x4b, 0xc8, 0x4b, 0x82, 0xed, 0x46, 0xc0, 0xd0, 0xe5, 0xf3, 0xc0, + 0xa8, 0x6c, 0x1e, 0x60, 0x4a, 0x92, 0x8b, 0xf9, 0x8c, 0x39, 0x82, 0x14, 0x5e, 0xcf, 0x70, 0x72, + 0xc2, 0x92, 0x84, 0xf0, 0x5a, 0x40, 0x2a, 0x9a, 0x02, 0x73, 0x72, 0x18, 0x4a, 0x5c, 0xc8, 0x26, + 0x4c, 0x48, 0x44, 0xcd, 0x10, 0x53, 0x89, 0xf9, 0x30, 0x80, 0xe1, 0x31, 0xe2, 0x25, 0x73, 0x2d, + 0xc2, 0x2c, 0x77, 0x1e, 0xa6, 0x90, 0x47, 0x84, 0x9a, 0xfa, 0xf0, 0x1b, 0x3b, 0x1c, 0x0e, 0x5f, + 0x24, 0x8d, 0x3e, 0xab, 0x84, 0x4c, 0x43, 0xd7, 0xb3, 0x62, 0x98, 0xc2, 0xa2, 0x4e, 0xd0, 0xec, + 0xe9, 0x7f, 0x0d, 0x49, 0x0a, 0x23, 0xdc, 0xe4, 0x98, 0x22, 0xcc, 0x09, 0x8d, 0xcc, 0x8c, 0x14, + 0x38, 0x81, 0x12, 0xa3, 0xff, 0x44, 0x42, 0x4e, 0x44, 0xd6, 0xc4, 0x28, 0xc2, 0xe2, 0xf7, 0x3e, + 0x9d, 0x5e, 0x56, 0x28, 0xba, 0xd2, 0x6c, 0xeb, 0xe5, 0xf7, 0x2b, 0xc8, 0xa5, 0x64, 0xf4, 0x93, + 0xe5, 0x32, 0x21, 0x14, 0x97, 0x2a, 0x72, 0x2e, 0x18, 0x37, 0x33, 0x46, 0x2a, 0xcd, 0x19, 0x44, + 0xa8, 0x64, 0xea, 0x57, 0x2a, 0x2a, 0x86, 0x72, 0xe5, 0xb0, 0x56, 0xd3, 0xe9, 0x96, 0xff, 0x55, + 0xa6, 0x92, 0x43, 0x2a, 0x0e, 0x8c, 0xa7, 0x66, 0x9e, 0x65, 0x98, 0x87, 0x50, 0xe0, 0xe1, 0xcf, + 0x5a, 0xc5, 0xbf, 0x6b, 0x54, 0xa3, 0x82, 0x7c, 0x60, 0xb3, 0x3d, 0xc8, 0x8a, 0xff, 0xa9, 0x4a, + 0xb7, 0xdb, 0xfd, 0x51, 0x8c, 0x61, 0xc0, 0x38, 0xc2, 0xdc, 0xd4, 0x15, 0xc1, 0x12, 0x82, 0x94, + 0x1f, 0x58, 0x93, 0x43, 0x44, 0x72, 0x51, 0xe5, 0xf4, 0xd5, 0xb0, 0xb4, 0xfa, 0x9c, 0x2c, 0xad, + 0xee, 0xa1, 0xf2, 0xb8, 0x6c, 0x8b, 0xa4, 0x91, 0x02, 0x13, 0x39, 0x52, 0x55, 0xa5, 0x21, 0x78, + 0x38, 0x52, 0x11, 0x94, 0xd0, 0xac, 0x0a, 0xa5, 0x65, 0x34, 0x1a, 0x06, 0x50, 0xe0, 0x1b, 0xe3, + 0x9a, 0x3c, 0x3b, 0x4f, 0xeb, 0xb3, 0x7e, 0x3f, 0x8d, 0x18, 0x00, 0x00, 0x2c, 0x36, 0x7e, 0xec, + 0xf9, 0x11, 0x00, 0xc0, 0x2d, 0x5d, 0x10, 0xb9, 0xe0, 0x11, 0x00, 0xe0, 0x78, 0xd9, 0x9c, 0x4f, + 0x2b, 0xe4, 0x65, 0xb1, 0x59, 0xeb, 0x73, 0xc0, 0x85, 0x11, 0xde, 0xac, 0x4a, 0x60, 0x4d, 0x57, + 0x7e, 0xdb, 0x01, 0xc0, 0x2d, 0xde, 0xce, 0xa7, 0xfe, 0x7e, 0xe5, 0x97, 0x60, 0xe0, 0x7b, 0x85, + 0xbf, 0xae, 0xe2, 0x4e, 0xbf, 0x1d, 0xb9, 0xbe, 0xf6, 0x71, 0xff, 0xae, 0x95, 0x36, 0x08, 0x76, + 0x6d, 0xe6, 0x82, 0x68, 0x1a, 0x33, 0x58, 0x86, 0xa7, 0xcb, 0x87, 0x97, 0x7e, 0xc5, 0x7c, 0x87, + 0x26, 0x77, 0x4f, 0xbe, 0xf6, 0xaf, 0x81, 0xc9, 0x62, 0x89, 0x9d, 0x79, 0x15, 0x0b, 0xbd, 0xf8, + 0x35, 0x3c, 0x03, 0x30, 0x16, 0xa5, 0x7b, 0x0b, 0xc0, 0x8e, 0xef, 0xc8, 0xea, 0x58, 0x0a, 0x45, + 0x1b, 0x7f, 0xed, 0x3c, 0x8f, 0xe3, 0x65, 0x11, 0x0e, 0x82, 0x31, 0xf3, 0x23, 0x0f, 0x2c, 0x56, + 0x38, 0x58, 0x6a, 0x13, 0x3f, 0x9f, 0x3d, 0xbe, 0x39, 0xd3, 0xbd, 0xe6, 0x5c, 0x75, 0xbd, 0xfd, + 0xfa, 0x76, 0x3d, 0xd3, 0xdf, 0x5d, 0xed, 0xd5, 0x09, 0x6f, 0x66, 0x67, 0x37, 0x79, 0x8b, 0x66, + 0x4f, 0x57, 0xc5, 0xeb, 0xfc, 0x79, 0x33, 0xef, 0x88, 0x7d, 0x34, 0x83, 0xd3, 0x5b, 0xcf, 0xd9, + 0xc5, 0xfd, 0xb7, 0x1d, 0x2b, 0xb6, 0xdc, 0x75, 0x26, 0x68, 0x7c, 0x77, 0xe5, 0x8c, 0x8d, 0x24, + 0x98, 0xcf, 0x0a, 0x10, 0x7e, 0xf4, 0xc1, 0x12, 0x3c, 0x3f, 0x6c, 0x05, 0x7f, 0xf5, 0x0c, 0xbc, + 0x1a, 0xf7, 0xde, 0x3f, 0x64, 0x37, 0x04, 0x93, 0xed, 0x9e, 0x1d, 0x5d, 0x63, 0xef, 0x2e, 0x06, + 0xd3, 0x4b, 0x10, 0xe5, 0xc6, 0x05, 0xac, 0xa4, 0x33, 0x79, 0x58, 0xbd, 0xcc, 0xf2, 0x19, 0x70, + 0xc0, 0xed, 0xdd, 0x23, 0x7e, 0xf2, 0x5c, 0xcd, 0xd3, 0x77, 0xbd, 0xfc, 0x32, 0x88, 0x4e, 0xc6, + 0x89, 0xf5, 0x56, 0xb3, 0xfb, 0x0e, 0xb9, 0xbd, 0xbc, 0x77, 0xdc, 0xbe, 0x0f, 0x9c, 0x47, 0x63, + 0x96, 0x3e, 0x5c, 0xb9, 0x9b, 0xed, 0x8b, 0xbb, 0x9d, 0xb4, 0xc7, 0xdc, 0x7d, 0xb9, 0xb9, 0x9a, + 0x66, 0x83, 0x0f, 0xa7, 0x87, 0xaa, 0x6c, 0x81, 0x97, 0x4c, 0xb6, 0xc7, 0x4d, 0xbe, 0x4a, 0x5d, + 0x57, 0xb5, 0x1b, 0x56, 0xdc, 0xb6, 0x0d, 0xdd, 0x50, 0xca, 0x79, 0x9d, 0xd4, 0xf3, 0x1a, 0xb7, + 0x6d, 0x2b, 0xb0, 0xc1, 0x11, 0xa7, 0x44, 0x41, 0x0c, 0x0b, 0x85, 0x32, 0xa9, 0x1c, 0x29, 0x3b, + 0x2b, 0xe7, 0x18, 0x73, 0xac, 0x5c, 0x58, 0xae, 0x40, 0x8e, 0x95, 0xb2, 0x41, 0x30, 0x6a, 0xb5, + 0x5a, 0x96, 0x16, 0xd8, 0x56, 0xc0, 0xab, 0xa7, 0x61, 0xd5, 0xc3, 0xa0, 0x30, 0x1a, 0x26, 0x24, + 0x3c, 0x8e, 0x7e, 0x9d, 0x09, 0x45, 0xec, 0xdc, 0x4a, 0x58, 0x08, 0x25, 0x61, 0xb4, 0x15, 0x73, + 0x7c, 0x18, 0xa9, 0xad, 0x96, 0xf6, 0xb7, 0x48, 0x08, 0xc2, 0x5c, 0xa8, 0xbf, 0x6c, 0x07, 0x86, + 0x47, 0x45, 0x32, 0xa5, 0xbc, 0x4e, 0x38, 0x4b, 0x84, 0xa5, 0xd5, 0x2c, 0x76, 0xc3, 0xd2, 0xea, + 0x0e, 0xd4, 0xaa, 0x8b, 0xed, 0x1f, 0xd9, 0xa5, 0x2c, 0x90, 0xee, 0x04, 0x00, 0x00 +}; // Autogenerated from wled00/data/favicon.ico, do not edit!! @@ -116,3 +392,758 @@ const uint8_t favicon[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 }; + +// Autogenerated from wled00/data/iro.js, do not edit!! +const uint16_t iroJs_length = 10040; +const uint8_t iroJs[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xc5, 0x7d, 0x6b, 0x93, 0xdb, 0x36, + 0xb2, 0xe8, 0xf7, 0x53, 0x75, 0xfe, 0x83, 0x86, 0x89, 0x55, 0x80, 0x04, 0x61, 0x28, 0xcd, 0x68, + 0xec, 0xa1, 0x8c, 0xa3, 0x4a, 0x9c, 0x6c, 0xe2, 0xdd, 0x78, 0x9d, 0x13, 0x7b, 0xb3, 0x7b, 0x22, + 0x6b, 0x53, 0x14, 0x05, 0x49, 0xf0, 0x50, 0xa0, 0x02, 0x82, 0x9a, 0x99, 0x88, 0xfc, 0xef, 0xb7, + 0x1a, 0x00, 0x1f, 0x7a, 0xcc, 0x23, 0x7b, 0xcf, 0xde, 0x5b, 0x49, 0x59, 0x20, 0x1e, 0x0d, 0xa0, + 0xd1, 0xdd, 0xe8, 0x6e, 0x34, 0x30, 0xe7, 0x9d, 0xb3, 0xff, 0xfc, 0x8f, 0x56, 0xa7, 0x25, 0x54, + 0x42, 0x3f, 0xa7, 0xad, 0xed, 0x90, 0x0e, 0xe9, 0xc0, 0xe4, 0x0c, 0xfc, 0xfe, 0x55, 0x6f, 0xe0, + 0x0f, 0xfa, 0xad, 0x3f, 0x87, 0x6b, 0x9e, 0xb6, 0xbe, 0x09, 0xa5, 0xe0, 0xb1, 0x29, 0xfa, 0x41, + 0x44, 0x5c, 0xa6, 0x7c, 0xde, 0xca, 0xe4, 0x9c, 0xab, 0xd6, 0xbb, 0x1f, 0x7f, 0x68, 0x0d, 0xa8, + 0x6f, 0x8a, 0x96, 0x42, 0xaf, 0xb2, 0x19, 0x8d, 0x92, 0xf5, 0xf9, 0xe7, 0x10, 0xda, 0x9d, 0x5b, + 0xc8, 0x50, 0x78, 0xfe, 0x9f, 0xff, 0x71, 0xb6, 0xc8, 0x64, 0xa4, 0x45, 0x22, 0x91, 0x26, 0x12, + 0xef, 0xbc, 0x64, 0xf6, 0x99, 0x47, 0xda, 0x63, 0x4c, 0xdf, 0x6f, 0x78, 0xb2, 0x68, 0xf1, 0xbb, + 0x4d, 0xa2, 0x74, 0xda, 0x6e, 0x7b, 0x00, 0x7a, 0x21, 0x24, 0x9f, 0x7b, 0x67, 0x65, 0xe1, 0x3a, + 0x99, 0x67, 0x31, 0x1f, 0xdb, 0x1f, 0xea, 0xaa, 0x32, 0x89, 0x70, 0xe0, 0x95, 0x60, 0x6b, 0x48, + 0xb6, 0x75, 0xbb, 0x6d, 0x7f, 0x69, 0xb8, 0x9e, 0x8f, 0x6d, 0x12, 0x49, 0x1c, 0x20, 0xcd, 0x74, + 0x9e, 0xa7, 0x3c, 0x5e, 0x60, 0x2a, 0x54, 0x02, 0x30, 0x0a, 0xa4, 0x57, 0x22, 0x25, 0xd5, 0xf8, + 0xf0, 0xce, 0xcb, 0x52, 0xde, 0x4a, 0xb5, 0x12, 0x91, 0xf6, 0x46, 0xdb, 0x50, 0xb5, 0xd6, 0x24, + 0x25, 0x92, 0x08, 0x92, 0x90, 0x3b, 0xb6, 0x2b, 0xc8, 0x67, 0x36, 0x99, 0x12, 0xc5, 0xce, 0xc3, + 0x48, 0xe8, 0x9c, 0xdf, 0xa1, 0x71, 0x90, 0xe6, 0xcb, 0x5c, 0xe6, 0x9b, 0xfc, 0x4b, 0x9c, 0xab, + 0xcd, 0x2a, 0x5f, 0x2a, 0x31, 0xcf, 0x93, 0xdb, 0x34, 0x5f, 0xcb, 0x28, 0x97, 0xfa, 0x36, 0x17, + 0x92, 0x4f, 0xa2, 0xd5, 0x34, 0xff, 0x3d, 0x49, 0xf2, 0x7f, 0x26, 0x6a, 0x9e, 0xff, 0xb3, 0xd7, + 0x3b, 0x17, 0xa3, 0xb2, 0xcf, 0xd6, 0x3b, 0x8b, 0x95, 0x45, 0xa2, 0x10, 0xf4, 0x27, 0x5a, 0x42, + 0xb6, 0x24, 0xd6, 0x13, 0x31, 0x65, 0x72, 0x22, 0xa6, 0x23, 0xc5, 0x75, 0xa6, 0x64, 0x4b, 0x17, + 0x55, 0x8b, 0x7b, 0xa4, 0xf1, 0x0e, 0xea, 0x4a, 0xa6, 0xe9, 0x26, 0x54, 0x5c, 0xea, 0xbf, 0x26, + 0x73, 0x3e, 0x92, 0xed, 0xb6, 0xa4, 0x8a, 0xaf, 0x93, 0x2d, 0x7f, 0xb3, 0x12, 0xf1, 0x1c, 0x69, + 0x5c, 0x37, 0x5a, 0x41, 0x37, 0x44, 0xd8, 0x86, 0x8a, 0x70, 0x92, 0x91, 0x84, 0xc4, 0x2c, 0x54, + 0xcb, 0x6c, 0xcd, 0xa5, 0x4e, 0x47, 0x62, 0x81, 0x24, 0x7b, 0x87, 0x76, 0x05, 0x91, 0x98, 0x5c, + 0xbc, 0xae, 0x0a, 0x68, 0xcc, 0xe5, 0x52, 0xaf, 0x30, 0x0c, 0x50, 0xb0, 0x89, 0x80, 0xe9, 0x5f, + 0x8c, 0xd4, 0x51, 0x85, 0x91, 0xea, 0x76, 0xb1, 0xa0, 0x9b, 0x2c, 0x5d, 0xa1, 0x78, 0xa2, 0xa6, + 0xd8, 0x40, 0xcc, 0xe2, 0xf8, 0x8c, 0x89, 0x76, 0x1b, 0x49, 0x1a, 0xc1, 0x98, 0x14, 0x97, 0x4c, + 0x60, 0x62, 0xf3, 0x75, 0xbb, 0xed, 0x12, 0x74, 0xce, 0x17, 0x61, 0x16, 0xeb, 0x1f, 0x55, 0xb2, + 0x49, 0x4d, 0x57, 0x1c, 0xf0, 0x70, 0x90, 0xbf, 0x4d, 0xc4, 0xbc, 0xe5, 0x33, 0xc6, 0xe4, 0x84, + 0x4f, 0x01, 0xe6, 0x84, 0x4f, 0x0f, 0xda, 0x4e, 0xf8, 0x14, 0x97, 0x28, 0x4b, 0x98, 0xa4, 0x37, + 0xfc, 0xde, 0x75, 0x86, 0x32, 0x06, 0xd8, 0x59, 0x60, 0x20, 0x8f, 0x98, 0x6b, 0xde, 0x32, 0x9f, + 0xae, 0x34, 0x69, 0xe4, 0x42, 0x9b, 0xc8, 0xa0, 0x2b, 0x21, 0x59, 0x03, 0x85, 0x36, 0x4f, 0x10, + 0x65, 0x91, 0xc8, 0xd9, 0x0e, 0xc8, 0x2e, 0xd0, 0x64, 0x03, 0x3d, 0x07, 0x92, 0xdc, 0xf0, 0xfb, + 0x40, 0x10, 0xc5, 0x17, 0x81, 0x22, 0x32, 0x00, 0xc0, 0x44, 0xd8, 0x1f, 0x1e, 0xf8, 0x24, 0xb1, + 0xc9, 0xd8, 0xfe, 0x44, 0xee, 0x27, 0x91, 0xa9, 0x56, 0x59, 0xa4, 0x13, 0x15, 0xd8, 0xe9, 0x15, + 0xe5, 0xf0, 0xd7, 0x74, 0x2b, 0x93, 0x39, 0x6f, 0xb7, 0x5d, 0x02, 0x71, 0x4c, 0x78, 0x3d, 0x9a, + 0xf7, 0x40, 0x05, 0x25, 0x71, 0x54, 0xc8, 0xad, 0xcb, 0xdf, 0x5a, 0xba, 0x02, 0x02, 0xa7, 0x66, + 0x80, 0x4c, 0x13, 0xf3, 0x11, 0x25, 0x52, 0xf3, 0x3b, 0xcd, 0x1a, 0x75, 0x6f, 0x6d, 0x5d, 0xb7, + 0x60, 0x8c, 0x49, 0x5c, 0x01, 0x16, 0xe3, 0x5b, 0xa4, 0xa9, 0x20, 0x9a, 0x0a, 0x2a, 0xa9, 0x90, + 0x73, 0x7e, 0xf7, 0x7e, 0x81, 0x34, 0xee, 0xf6, 0xb1, 0x99, 0xc1, 0xa8, 0x22, 0xdc, 0x91, 0x7c, + 0xad, 0xa9, 0x2c, 0xa9, 0x41, 0x02, 0x35, 0x94, 0x04, 0x80, 0x04, 0xd3, 0x54, 0x4e, 0xe4, 0x14, + 0x97, 0x2b, 0x2e, 0x68, 0x52, 0xf6, 0x21, 0x68, 0xe2, 0xa6, 0x7c, 0x82, 0xa3, 0x35, 0x85, 0x04, + 0x8c, 0xc1, 0x76, 0x57, 0x8f, 0x39, 0xac, 0xb8, 0x80, 0x88, 0x9a, 0xd4, 0x80, 0xc9, 0xa9, 0xc0, + 0x35, 0x61, 0x45, 0x96, 0xb5, 0x34, 0x4d, 0xe0, 0x83, 0xce, 0xc2, 0x94, 0x33, 0x83, 0x79, 0xc9, + 0xfc, 0x3f, 0x3c, 0xe2, 0xdd, 0x1e, 0x18, 0x18, 0xf8, 0x4c, 0xf1, 0xf0, 0xa6, 0x70, 0x33, 0x81, + 0x21, 0x15, 0xf5, 0x08, 0x39, 0x8c, 0x10, 0x9d, 0x69, 0xba, 0x68, 0xb7, 0x91, 0xa6, 0x0b, 0x76, + 0xe6, 0xe3, 0x76, 0xbb, 0xcf, 0x18, 0x4b, 0x2d, 0x9b, 0x68, 0x9c, 0xe7, 0xe2, 0x8c, 0xb1, 0x35, + 0x9d, 0xf3, 0x59, 0x92, 0xc9, 0x88, 0xff, 0xc4, 0x41, 0xb8, 0x0a, 0xb9, 0xc4, 0xed, 0x36, 0x12, + 0xa7, 0x0a, 0x08, 0x3a, 0x91, 0x99, 0xe7, 0x12, 0xa3, 0x0c, 0x37, 0x68, 0x35, 0x43, 0x16, 0x39, + 0x8e, 0x62, 0x4b, 0x7e, 0x37, 0xab, 0x95, 0xd2, 0x34, 0x51, 0x1a, 0xed, 0x4b, 0x65, 0x37, 0x05, + 0x49, 0xe7, 0x94, 0xf7, 0x34, 0xfc, 0x5b, 0xe0, 0x91, 0x86, 0x91, 0x26, 0x1b, 0x84, 0x47, 0xd8, + 0xce, 0x42, 0x31, 0xc1, 0x2c, 0x99, 0x92, 0x8c, 0x21, 0xce, 0x90, 0x64, 0x1a, 0xd3, 0x39, 0xa6, + 0x09, 0x01, 0x6e, 0xdb, 0x90, 0x98, 0x49, 0x9a, 0x11, 0x49, 0x33, 0x76, 0xd6, 0x27, 0x89, 0x99, + 0x84, 0x11, 0x98, 0x37, 0x28, 0x21, 0x9c, 0x18, 0xf1, 0xc2, 0x31, 0x91, 0xf4, 0x96, 0x58, 0x30, + 0x67, 0x8c, 0x25, 0x34, 0xb9, 0x95, 0x5c, 0x7d, 0xf8, 0xf9, 0xbb, 0x6f, 0x63, 0x0e, 0x12, 0x85, + 0x58, 0xce, 0x21, 0x31, 0xb1, 0xd4, 0x98, 0x8d, 0x6f, 0x11, 0xc7, 0x41, 0x86, 0xc9, 0x1c, 0x09, + 0x68, 0xae, 0xce, 0x58, 0xd6, 0x6e, 0x87, 0x88, 0xe3, 0xe6, 0x94, 0x3f, 0x20, 0x98, 0xaa, 0xae, + 0x27, 0x4b, 0x52, 0x8b, 0x83, 0x88, 0x84, 0x64, 0x41, 0x56, 0x64, 0x4b, 0xe6, 0x64, 0x49, 0x66, + 0x20, 0x76, 0x34, 0x95, 0x79, 0xfe, 0x99, 0x6c, 0xd8, 0xac, 0x5c, 0x7d, 0xb1, 0x40, 0x31, 0x63, + 0x77, 0xed, 0x36, 0x8a, 0x99, 0x5d, 0xf0, 0x6c, 0x9c, 0x4d, 0xfc, 0x69, 0xb0, 0x01, 0xe2, 0x23, + 0xbe, 0x25, 0x3f, 0x4c, 0x22, 0xe6, 0x13, 0x41, 0x25, 0xfb, 0x0a, 0x09, 0x2a, 0xeb, 0x9d, 0x43, + 0x57, 0xdc, 0x73, 0xc6, 0x6c, 0x5a, 0x53, 0xc1, 0x80, 0x6d, 0x80, 0x4e, 0x78, 0xb7, 0xef, 0xe6, + 0xc2, 0xd0, 0x82, 0xcd, 0x26, 0xd1, 0x14, 0xe7, 0xf9, 0x02, 0x86, 0x71, 0xc3, 0xef, 0x19, 0x5b, + 0xc0, 0x0f, 0x7c, 0x01, 0xad, 0x33, 0xf8, 0x86, 0x04, 0x86, 0x7a, 0x0e, 0xdb, 0x23, 0x1e, 0xa7, + 0xbc, 0x05, 0x8b, 0x17, 0x32, 0x7f, 0x14, 0xbe, 0xde, 0x8c, 0xc2, 0x6e, 0xd7, 0x74, 0x63, 0xe0, + 0x85, 0x40, 0xa6, 0x8f, 0x03, 0xdb, 0x41, 0xad, 0x12, 0x9a, 0xa5, 0xda, 0x85, 0x99, 0x68, 0x21, + 0x16, 0x68, 0xc5, 0x6e, 0x90, 0x24, 0x9a, 0x2c, 0xd8, 0x22, 0xcf, 0xef, 0x2a, 0x0c, 0x5a, 0x79, + 0x45, 0x52, 0x4c, 0x50, 0xc8, 0xb4, 0x13, 0x9e, 0x0b, 0xf8, 0x3d, 0x63, 0x61, 0xbb, 0x8d, 0x96, + 0x6c, 0x99, 0xe7, 0x93, 0x29, 0xb6, 0xd4, 0x1c, 0x12, 0x4d, 0xa3, 0x3c, 0x5f, 0x11, 0x5d, 0xca, + 0xf7, 0x55, 0x43, 0xa4, 0xcc, 0xdb, 0x6d, 0x34, 0x67, 0xab, 0x4a, 0xf4, 0xd3, 0x18, 0xaf, 0xe0, + 0x5f, 0xa2, 0xa9, 0x45, 0xb8, 0x9d, 0xa2, 0x58, 0xa0, 0x8c, 0xc1, 0x28, 0x56, 0x67, 0x2c, 0xce, + 0x73, 0xdb, 0x76, 0xd5, 0xd8, 0xe5, 0xf0, 0x4e, 0x07, 0x15, 0xd0, 0x38, 0xcf, 0xe3, 0x46, 0xd9, + 0x19, 0x48, 0x2e, 0x49, 0xc3, 0xcd, 0x86, 0xcb, 0xb9, 0xdd, 0x02, 0x57, 0xd8, 0xc0, 0xb5, 0xdb, + 0x2b, 0x8b, 0x09, 0xa0, 0x0f, 0x6d, 0xd9, 0x96, 0x4a, 0x7e, 0xa7, 0x3f, 0x88, 0x59, 0x6c, 0x79, + 0xcd, 0x62, 0x94, 0x0d, 0x40, 0x00, 0x6c, 0x19, 0x5b, 0x61, 0x83, 0xa1, 0x96, 0x1e, 0x81, 0xc8, + 0x4b, 0xb9, 0xd2, 0x5f, 0xf3, 0x45, 0xa2, 0x38, 0x5a, 0x91, 0x18, 0x17, 0x5e, 0xb2, 0x71, 0x42, + 0x4a, 0x18, 0xdc, 0x9a, 0xdd, 0x6d, 0x1b, 0xc6, 0x19, 0x67, 0x9e, 0x87, 0x8b, 0x98, 0xad, 0x9a, + 0xd0, 0xc9, 0x09, 0xa9, 0x56, 0xb5, 0x13, 0x34, 0x66, 0x2b, 0x5c, 0x94, 0x02, 0x24, 0xea, 0x76, + 0x89, 0x2e, 0x30, 0x11, 0x34, 0x61, 0x73, 0x87, 0xa9, 0xac, 0xdd, 0xae, 0x21, 0x9c, 0xed, 0x43, + 0x30, 0x3b, 0x65, 0xc4, 0xb2, 0x92, 0x88, 0xa3, 0x5e, 0x6f, 0x84, 0x5d, 0xb3, 0x49, 0x34, 0x6d, + 0xb7, 0xef, 0x11, 0xfc, 0xe2, 0x91, 0xad, 0xb7, 0x69, 0x56, 0x98, 0x99, 0x0a, 0x7f, 0x45, 0xf0, + 0x4b, 0x0c, 0x49, 0x02, 0x0f, 0x2c, 0x1d, 0x48, 0x7f, 0x14, 0xbd, 0x5e, 0x56, 0x60, 0xbb, 0x5d, + 0xfc, 0x2d, 0x5a, 0x42, 0xc5, 0xe5, 0xa4, 0xdb, 0xad, 0x7e, 0x1a, 0xcc, 0xf7, 0x55, 0xa9, 0x5e, + 0x54, 0x6b, 0x23, 0x1c, 0xf3, 0x63, 0x47, 0xf9, 0x3a, 0xcf, 0xbd, 0x59, 0x92, 0xc4, 0x3c, 0x6c, + 0x8a, 0x77, 0x2c, 0xdb, 0x6d, 0xa7, 0x34, 0x48, 0xd3, 0x10, 0xe3, 0x8a, 0x10, 0xbe, 0x52, 0x2a, + 0xbc, 0xa7, 0x22, 0x35, 0xbf, 0x48, 0x63, 0x5c, 0x6e, 0x35, 0x8a, 0xf9, 0x23, 0xf5, 0x5a, 0x37, + 0x15, 0x8f, 0xaf, 0x90, 0x9e, 0xa8, 0xa9, 0x19, 0x82, 0x6b, 0xef, 0x80, 0x8e, 0x25, 0x3a, 0xc1, + 0xa4, 0x0f, 0x0e, 0xa7, 0x14, 0x83, 0x40, 0x90, 0x62, 0x81, 0x3c, 0x50, 0x05, 0xe5, 0xb2, 0x51, + 0x23, 0xcf, 0x3d, 0x99, 0xad, 0x67, 0x5c, 0x9d, 0x68, 0x15, 0x19, 0xd8, 0xc4, 0x09, 0x30, 0x33, + 0x9b, 0x51, 0xdd, 0x21, 0x4d, 0xec, 0x56, 0xc2, 0xcc, 0xae, 0x54, 0x6e, 0xb0, 0x23, 0xab, 0xc8, + 0x45, 0xc8, 0x32, 0x2c, 0xd1, 0x76, 0xb3, 0x26, 0xba, 0x52, 0x5d, 0x2a, 0x7d, 0x46, 0x9a, 0xcd, + 0x27, 0x21, 0xb2, 0x00, 0x64, 0x04, 0xba, 0x2a, 0x10, 0xf5, 0x3a, 0x2c, 0xca, 0x75, 0xf0, 0x7a, + 0x9e, 0x51, 0x94, 0xfc, 0xe9, 0x58, 0xd3, 0x94, 0x1b, 0xed, 0x88, 0x2b, 0x7d, 0x0f, 0x42, 0x12, + 0x07, 0x7a, 0x22, 0xa7, 0xec, 0x68, 0x22, 0xa2, 0xdd, 0x3e, 0x83, 0xed, 0x49, 0x51, 0xcd, 0x53, + 0x8d, 0x24, 0x1e, 0x8b, 0xae, 0xb7, 0xb9, 0xf3, 0x02, 0xb7, 0xa2, 0x63, 0xcf, 0x0b, 0x1a, 0x7d, + 0xfd, 0x84, 0xaa, 0xdd, 0xc5, 0x0a, 0x5a, 0x27, 0x75, 0x49, 0x64, 0x50, 0x77, 0xc3, 0xef, 0x61, + 0x08, 0x48, 0x32, 0x3e, 0xf6, 0xa2, 0x38, 0x4c, 0xd3, 0xbf, 0x86, 0x6b, 0x6e, 0x46, 0xe5, 0xbe, + 0xbd, 0x40, 0x06, 0x2e, 0xd5, 0xc8, 0x35, 0xb5, 0x02, 0x89, 0xf3, 0xdc, 0x2b, 0xb5, 0x1a, 0x53, + 0x5c, 0x13, 0x86, 0x97, 0xea, 0xfb, 0xd8, 0x42, 0xc2, 0x46, 0x60, 0x68, 0x6a, 0x72, 0xc8, 0xd1, + 0x72, 0x09, 0x9c, 0xd1, 0x28, 0x4d, 0x3f, 0x82, 0xbe, 0x23, 0xac, 0x24, 0x38, 0xb5, 0xaa, 0xaa, + 0xdd, 0x46, 0x75, 0x45, 0xcf, 0x23, 0x8a, 0x59, 0x69, 0xaf, 0x0c, 0xcd, 0x25, 0xa0, 0x8b, 0x2a, + 0x2c, 0xda, 0x6d, 0x93, 0x12, 0x79, 0xbe, 0x40, 0x30, 0x57, 0xcf, 0x33, 0xeb, 0x2b, 0x4c, 0xa5, + 0xd8, 0x14, 0x61, 0xd5, 0x6e, 0x8b, 0x49, 0x3c, 0x05, 0x2c, 0x4e, 0xe2, 0xa9, 0xad, 0x19, 0x13, + 0xc8, 0xc2, 0x05, 0xf4, 0xef, 0x25, 0xe5, 0xba, 0xb4, 0xdb, 0x9e, 0x9d, 0xd8, 0xa4, 0x3f, 0x1d, + 0xa3, 0x94, 0xc9, 0x33, 0x83, 0x2c, 0xd0, 0x4a, 0x37, 0x71, 0x18, 0x71, 0x74, 0xfe, 0x26, 0xdc, + 0xe8, 0x4c, 0xf1, 0x2f, 0xcf, 0xa1, 0x27, 0x4c, 0x24, 0x43, 0x28, 0x62, 0x92, 0xea, 0xe4, 0x87, + 0xe4, 0x96, 0xab, 0x37, 0x61, 0xca, 0x11, 0xc6, 0xa0, 0x26, 0x8f, 0xa3, 0x40, 0x62, 0x9a, 0xc6, + 0x22, 0xe2, 0x68, 0x80, 0x89, 0x18, 0x23, 0x95, 0xe7, 0x9a, 0x86, 0xf3, 0xf9, 0xb7, 0x5b, 0x2e, + 0xf5, 0x0f, 0x22, 0xd5, 0x5c, 0x72, 0x85, 0x24, 0xd9, 0x1a, 0x69, 0xae, 0xa9, 0xce, 0x73, 0xf8, + 0x97, 0xed, 0x0a, 0x8c, 0x81, 0x16, 0x80, 0x26, 0x9c, 0xed, 0x70, 0xaa, 0x09, 0x0e, 0xbc, 0x58, + 0xa4, 0xda, 0x03, 0x09, 0xdb, 0x6e, 0x7b, 0x3a, 0x5c, 0x9a, 0x55, 0x72, 0x9f, 0x8b, 0x44, 0xad, + 0x5d, 0xfa, 0x8c, 0xb7, 0xdb, 0xd2, 0xa8, 0xee, 0x63, 0x43, 0x63, 0x4d, 0xca, 0x09, 0x4e, 0xc9, + 0xb1, 0x76, 0xdb, 0x9b, 0x87, 0x72, 0xc9, 0x55, 0x92, 0xa5, 0xf1, 0xfd, 0x07, 0xae, 0xdf, 0x4a, + 0xc9, 0xd5, 0xf7, 0x1f, 0xdf, 0xfd, 0xe0, 0x20, 0xa2, 0x63, 0xbc, 0xfc, 0xf3, 0x2e, 0x16, 0xf2, + 0x26, 0x18, 0x5b, 0xbc, 0x8c, 0x5d, 0x1f, 0x79, 0x6e, 0x88, 0x57, 0x8c, 0xcb, 0x89, 0x7c, 0xa5, + 0xb5, 0x12, 0xb3, 0x4c, 0xf3, 0xbf, 0x7e, 0x40, 0xde, 0x4a, 0xeb, 0x4d, 0x70, 0x7e, 0x7e, 0x7b, + 0x7b, 0x4b, 0x6f, 0x2f, 0x68, 0xa2, 0x96, 0xe7, 0xfd, 0xeb, 0xeb, 0xeb, 0x73, 0x03, 0xc9, 0x23, + 0x87, 0x48, 0x0d, 0x0c, 0xcf, 0xfc, 0xeb, 0x00, 0x80, 0xc7, 0x9e, 0x1c, 0x16, 0x18, 0xa5, 0xfb, + 0xfd, 0x18, 0xe6, 0x6c, 0x48, 0xd5, 0x6d, 0x53, 0xc7, 0x07, 0xdd, 0x5d, 0x4f, 0xac, 0x98, 0x98, + 0xa2, 0x35, 0xe5, 0xb0, 0x4e, 0x63, 0xf7, 0x0b, 0xfa, 0x71, 0xd3, 0xdc, 0xbb, 0x41, 0x07, 0x9a, + 0x1f, 0xb0, 0xa5, 0xe5, 0xd2, 0x3d, 0x65, 0x88, 0x6c, 0xc8, 0x3d, 0xb9, 0x25, 0x37, 0x40, 0x56, + 0xf7, 0x1b, 0x0e, 0xe4, 0x5c, 0x29, 0x66, 0x92, 0x36, 0xac, 0x93, 0x3d, 0xc9, 0x88, 0x42, 0xb6, + 0xa6, 0x1c, 0xf6, 0x4d, 0x24, 0xf1, 0x48, 0xab, 0x7b, 0xbb, 0x29, 0x9f, 0xd8, 0xe9, 0x6e, 0x8c, + 0xc8, 0xdd, 0x80, 0x6e, 0x68, 0xc4, 0xda, 0x3d, 0x43, 0x21, 0xbb, 0x29, 0x6d, 0x90, 0x8f, 0xb0, + 0x87, 0xb5, 0xdb, 0x6a, 0x12, 0xd2, 0x68, 0x4a, 0x6e, 0x59, 0x38, 0xbe, 0x1f, 0xdf, 0xdb, 0x9a, + 0x76, 0x43, 0x0d, 0x42, 0x2a, 0x02, 0x45, 0x04, 0x8d, 0xc6, 0x33, 0x50, 0x9d, 0x24, 0x8d, 0x98, + 0xa0, 0x11, 0xa6, 0x02, 0xf4, 0x9c, 0x00, 0x79, 0x1b, 0x95, 0xe8, 0x04, 0xfa, 0xf2, 0x84, 0x6c, + 0xdd, 0xb4, 0xdb, 0x37, 0xb4, 0xca, 0xa1, 0xca, 0xa8, 0xc7, 0x63, 0x68, 0xb3, 0x60, 0x92, 0xdf, + 0xb6, 0x6e, 0xd0, 0x86, 0xdc, 0xe2, 0x00, 0xd5, 0x39, 0x6f, 0x4d, 0x0e, 0x59, 0x34, 0x67, 0xca, + 0x6e, 0xc8, 0xc2, 0xb5, 0x65, 0xbf, 0x63, 0x72, 0xdf, 0x6e, 0xdf, 0xd3, 0x34, 0x9b, 0xa1, 0x05, + 0xd4, 0xb3, 0x96, 0xd4, 0x86, 0x2c, 0x68, 0xaa, 0x43, 0xcd, 0xf3, 0x1c, 0xb9, 0x14, 0x70, 0x92, + 0x85, 0x63, 0x8c, 0xab, 0x5b, 0xb2, 0xa0, 0xb7, 0x4c, 0x91, 0x15, 0x5b, 0x18, 0xc5, 0x9f, 0x2c, + 0xe8, 0xba, 0xb1, 0x19, 0x2e, 0xe8, 0xe7, 0x76, 0x1b, 0x2d, 0xe8, 0x67, 0xe6, 0x9a, 0x97, 0x7a, + 0xd1, 0x0d, 0x5d, 0x72, 0xfd, 0x0d, 0x57, 0x62, 0xcb, 0xe7, 0x1f, 0xa0, 0xe0, 0x4f, 0x2a, 0x59, + 0x1b, 0xcb, 0xb6, 0xdd, 0x7e, 0x67, 0x1a, 0x94, 0x2d, 0xc6, 0xf0, 0x61, 0x14, 0xea, 0x05, 0xfd, + 0x8c, 0x83, 0x05, 0xfd, 0x4c, 0x1e, 0x6e, 0x8c, 0x36, 0xa6, 0x16, 0x26, 0x2b, 0x6c, 0x07, 0xf0, + 0x58, 0x3f, 0x76, 0x24, 0x30, 0x97, 0xf5, 0x26, 0x91, 0x5c, 0xea, 0xbf, 0x8b, 0x38, 0x7e, 0x97, + 0x64, 0x52, 0x83, 0xf6, 0x77, 0x9c, 0x8b, 0xca, 0xc1, 0x37, 0x0a, 0xbf, 0x11, 0x73, 0xd7, 0x22, + 0xb1, 0x1b, 0xf1, 0x02, 0x57, 0xe2, 0xf7, 0x79, 0x23, 0x60, 0x2c, 0x7e, 0x60, 0x28, 0x3f, 0xf1, + 0x88, 0x8b, 0x2d, 0x77, 0x75, 0x1f, 0x29, 0xb4, 0x8b, 0x7b, 0xd6, 0x80, 0x93, 0xae, 0x92, 0x2c, + 0x9e, 0xbf, 0x29, 0x1b, 0xfc, 0x6d, 0x33, 0x0f, 0x35, 0x77, 0xbb, 0xde, 0x03, 0xa5, 0x16, 0x75, + 0xe4, 0x16, 0x5b, 0x43, 0xf2, 0x88, 0x04, 0x60, 0x2d, 0x89, 0x59, 0xe4, 0x3e, 0x41, 0x0b, 0x3a, + 0x67, 0x12, 0xd3, 0xc4, 0x99, 0x10, 0xe9, 0x38, 0x3d, 0x03, 0x2d, 0x31, 0x19, 0xa7, 0x81, 0xa0, + 0xce, 0x01, 0x20, 0xa9, 0x64, 0x60, 0x3b, 0x58, 0x4d, 0x5e, 0xd6, 0x06, 0x28, 0xa8, 0xf4, 0x92, + 0xca, 0x49, 0x68, 0x7c, 0x1b, 0x26, 0x41, 0x05, 0xec, 0x7b, 0x4e, 0x19, 0x2d, 0x4e, 0x62, 0xa3, + 0x9c, 0xc3, 0xc9, 0xec, 0x6a, 0xf0, 0x85, 0x55, 0x80, 0xdd, 0xe8, 0xc9, 0xbc, 0xa4, 0xa2, 0x03, + 0x92, 0x3d, 0x35, 0x37, 0xc3, 0xec, 0xef, 0x1c, 0xb3, 0x97, 0x33, 0x35, 0x13, 0x85, 0x06, 0x4c, + 0x93, 0x90, 0x95, 0xfc, 0x52, 0x62, 0x87, 0x1c, 0x41, 0xc7, 0x66, 0xda, 0x5f, 0x39, 0x0b, 0x29, + 0x6c, 0xb7, 0x43, 0x67, 0x9f, 0xbc, 0x2f, 0x57, 0x3b, 0x04, 0x85, 0x67, 0x1c, 0x3a, 0xee, 0x2f, + 0x77, 0xfe, 0x20, 0xac, 0x89, 0x6b, 0xc9, 0xb5, 0x51, 0xea, 0xdf, 0x58, 0x90, 0xc6, 0x1c, 0x7d, + 0x87, 0x0c, 0x0b, 0x28, 0x18, 0xd8, 0x41, 0x39, 0xc2, 0x40, 0xef, 0xa5, 0x0d, 0x61, 0x8a, 0x3f, + 0xc8, 0x70, 0x93, 0xae, 0x12, 0xa7, 0xcd, 0x5b, 0x14, 0xe5, 0x39, 0x5a, 0x3e, 0x5c, 0x8a, 0xb6, + 0x64, 0x8e, 0x31, 0xf9, 0x70, 0x20, 0x4e, 0x41, 0x98, 0x92, 0x85, 0xf3, 0x26, 0xd0, 0x64, 0x04, + 0x28, 0x58, 0x97, 0xa6, 0x72, 0xc9, 0xdc, 0x15, 0x0a, 0x31, 0x09, 0x69, 0x14, 0xc6, 0x31, 0x30, + 0x41, 0x35, 0xa0, 0x6d, 0x3d, 0xb2, 0x26, 0xd7, 0x94, 0x83, 0x3a, 0x95, 0x8b, 0x8c, 0xd0, 0xc6, + 0x64, 0x66, 0xc0, 0xdf, 0xb0, 0x05, 0x90, 0x07, 0xe8, 0x29, 0x46, 0xb3, 0x30, 0xfa, 0x61, 0xd3, + 0xa8, 0x6f, 0x8a, 0x7f, 0x2b, 0xfa, 0x53, 0xe2, 0x6c, 0x61, 0x26, 0xdc, 0x42, 0x6d, 0x4b, 0xc9, + 0x0c, 0x92, 0x9f, 0x33, 0x2f, 0xdd, 0x2e, 0x8d, 0x4e, 0x62, 0x56, 0x27, 0xcf, 0x79, 0xa9, 0xbc, + 0x97, 0x2c, 0x94, 0x19, 0x5d, 0x27, 0x65, 0xfe, 0x28, 0x7d, 0x5d, 0x99, 0x1d, 0xe9, 0x9e, 0xe7, + 0x24, 0x62, 0xd9, 0x24, 0x05, 0x83, 0xd4, 0x31, 0xba, 0x03, 0x36, 0xbe, 0x60, 0x8c, 0x45, 0x14, + 0x3c, 0x59, 0x20, 0xf6, 0x83, 0x88, 0xc6, 0x49, 0x14, 0xc6, 0xa0, 0x47, 0x54, 0x75, 0x30, 0xde, + 0x69, 0x16, 0x11, 0x68, 0x6f, 0x6d, 0x42, 0x6b, 0xa8, 0xd6, 0x3a, 0x74, 0x43, 0x81, 0x2f, 0x9b, + 0xb8, 0x7d, 0x69, 0x9e, 0x44, 0xc6, 0x1d, 0x49, 0x23, 0xc5, 0x43, 0xcd, 0x41, 0x91, 0x03, 0x93, + 0x10, 0x6d, 0xc1, 0x8b, 0xc1, 0xc7, 0x07, 0xc5, 0xce, 0xd3, 0x70, 0x7a, 0x5f, 0x1f, 0xf8, 0xbe, + 0x7f, 0x0e, 0x88, 0x20, 0xae, 0x8b, 0xe0, 0x74, 0x6b, 0xe4, 0x8a, 0x49, 0x66, 0x0d, 0xe9, 0xc6, + 0x0e, 0x59, 0xcf, 0x7a, 0x75, 0xc6, 0xd8, 0xd6, 0xe1, 0xc2, 0x18, 0x73, 0x28, 0x9b, 0x64, 0x0d, + 0x3f, 0xda, 0xd4, 0x69, 0x9a, 0x9a, 0xce, 0x43, 0x1d, 0xb2, 0x2d, 0x0e, 0x40, 0xdf, 0x11, 0xfb, + 0x2d, 0xd8, 0x67, 0xab, 0xdc, 0x59, 0x2a, 0x72, 0xde, 0x3e, 0x98, 0x5e, 0x8a, 0x31, 0x09, 0x19, + 0xaa, 0xd6, 0x33, 0xcf, 0xef, 0x30, 0x7d, 0x40, 0x9f, 0x22, 0x0b, 0xb6, 0x7d, 0xb0, 0x2c, 0x86, + 0x9d, 0x2c, 0xcf, 0x43, 0x58, 0xb6, 0x45, 0xbb, 0x1d, 0x82, 0x3c, 0x79, 0x0f, 0x4c, 0xf9, 0xde, + 0x28, 0x8a, 0xa2, 0xac, 0xc8, 0x16, 0xa6, 0x24, 0xcf, 0x8d, 0x46, 0x7a, 0x4c, 0x6d, 0xce, 0x10, + 0x30, 0x06, 0x67, 0x66, 0x35, 0x62, 0xf3, 0x23, 0xf3, 0x1c, 0xcc, 0x85, 0xcc, 0xf9, 0x76, 0x26, + 0xd9, 0x94, 0x28, 0x5c, 0xd7, 0x92, 0x98, 0x9f, 0x34, 0x73, 0xe5, 0x24, 0x9b, 0xe6, 0xb9, 0x67, + 0x74, 0x01, 0xa0, 0xcb, 0xcc, 0x18, 0x05, 0x3c, 0xba, 0xe1, 0x73, 0xf7, 0x09, 0xa0, 0x8c, 0x12, + 0x0d, 0x15, 0x5d, 0x17, 0x00, 0xdd, 0x75, 0x51, 0x20, 0x4d, 0xb6, 0x64, 0x45, 0x38, 0x89, 0xad, + 0x08, 0x92, 0x07, 0x02, 0x86, 0x2c, 0xf2, 0xbc, 0xe6, 0x6e, 0x50, 0x65, 0xb9, 0x58, 0xca, 0xf7, + 0xf6, 0x88, 0xe2, 0xac, 0x5c, 0xc6, 0x76, 0xdb, 0xf2, 0xd1, 0x1d, 0x80, 0x01, 0x5c, 0xb9, 0x21, + 0x09, 0xd9, 0xda, 0xb6, 0xdb, 0x95, 0xc6, 0xb4, 0xb5, 0x5a, 0x4b, 0xbb, 0xed, 0x12, 0x67, 0x60, + 0xeb, 0xb9, 0x2c, 0xe4, 0x52, 0x4e, 0x25, 0x76, 0x35, 0x40, 0x31, 0x76, 0x49, 0x4c, 0xaa, 0xa9, + 0x1d, 0x81, 0x75, 0x05, 0x00, 0xd8, 0x25, 0x0d, 0xe8, 0x2a, 0x1b, 0x55, 0xe9, 0xba, 0x32, 0x88, + 0x3f, 0x5d, 0x20, 0x01, 0x16, 0x63, 0x43, 0x14, 0x44, 0xd8, 0xaa, 0x6f, 0x73, 0xb1, 0x58, 0xf0, + 0xb9, 0x13, 0xeb, 0x45, 0x14, 0xea, 0x08, 0x1c, 0x92, 0xbb, 0x35, 0x4d, 0x9c, 0x09, 0x59, 0xd4, + 0x66, 0x67, 0xad, 0x57, 0xce, 0x0f, 0x4e, 0x2b, 0x46, 0xe0, 0x27, 0x2d, 0x5d, 0x84, 0xea, 0x7e, + 0x27, 0x8e, 0x55, 0x00, 0x74, 0x04, 0x5d, 0xd0, 0x39, 0x2e, 0xd6, 0x34, 0x02, 0xd7, 0x76, 0x04, + 0xbd, 0x57, 0xe0, 0xbf, 0x2d, 0xcd, 0x57, 0x80, 0x75, 0xca, 0x1d, 0x3c, 0xd6, 0x56, 0x6b, 0x8e, + 0x32, 0x05, 0x5e, 0x1f, 0x26, 0x8f, 0x60, 0x37, 0x3d, 0xb0, 0x7f, 0x3d, 0x3a, 0xf4, 0x00, 0x31, + 0xb7, 0xa6, 0x99, 0x5c, 0x5b, 0xe5, 0xa4, 0x4a, 0x22, 0x8d, 0x09, 0x52, 0x95, 0x93, 0xeb, 0x5b, + 0xa4, 0x9c, 0x11, 0x8f, 0x89, 0xc8, 0xf3, 0x07, 0x1d, 0xd3, 0x79, 0x8e, 0x84, 0xdb, 0xeb, 0x11, + 0x07, 0xe3, 0x1c, 0x70, 0x6e, 0xcc, 0xf4, 0x98, 0x55, 0x4e, 0x80, 0x33, 0x66, 0x20, 0x47, 0xd8, + 0x48, 0x2f, 0x75, 0xb0, 0x4f, 0xdb, 0xfe, 0x0d, 0xf2, 0x4e, 0x17, 0x1d, 0xe3, 0x4f, 0xe2, 0x42, + 0xd9, 0xbd, 0x47, 0xd1, 0x4d, 0xe5, 0xc4, 0x83, 0x3e, 0xa4, 0x11, 0xcf, 0x19, 0xf3, 0x47, 0xd9, + 0x6b, 0x55, 0x8a, 0xe7, 0xac, 0xdb, 0xc5, 0x6a, 0x92, 0x19, 0x6f, 0x0f, 0xfc, 0x5a, 0x2f, 0x89, + 0x1d, 0x19, 0x07, 0x1f, 0x11, 0x6f, 0xac, 0xc0, 0xef, 0x25, 0xca, 0x9a, 0x66, 0x47, 0x43, 0x3b, + 0xb6, 0x28, 0xae, 0xaa, 0x2f, 0x0f, 0xe8, 0x81, 0xf9, 0x23, 0xf1, 0xba, 0xd2, 0x68, 0x04, 0x38, + 0x29, 0xad, 0xc7, 0xc6, 0x9e, 0x65, 0x51, 0x2e, 0xb3, 0x35, 0x57, 0xe1, 0x2c, 0x86, 0xb1, 0xd7, + 0x1f, 0x60, 0x24, 0x11, 0x98, 0xbe, 0x5c, 0x88, 0x65, 0x66, 0xcb, 0xcf, 0x7c, 0x52, 0x73, 0x19, + 0x58, 0xe8, 0x8a, 0xde, 0x2a, 0xa1, 0x5d, 0x19, 0x26, 0x96, 0x47, 0xa9, 0x3d, 0xe0, 0xab, 0xdc, + 0x1b, 0x9a, 0x28, 0xe3, 0x39, 0x51, 0x4d, 0x3a, 0x98, 0xa1, 0x72, 0x3e, 0x68, 0xc6, 0x5c, 0xbb, + 0x30, 0x4d, 0xc5, 0x52, 0xe6, 0x79, 0xd3, 0x3b, 0x54, 0x4e, 0x43, 0xd6, 0xe7, 0x62, 0x44, 0xb0, + 0xfe, 0x48, 0x1c, 0x9f, 0x76, 0x1d, 0x4e, 0xad, 0x6c, 0xca, 0xad, 0xaf, 0xc0, 0x75, 0x52, 0x1b, + 0x23, 0xab, 0x30, 0x7d, 0x7f, 0x2b, 0xcb, 0x61, 0x5a, 0x41, 0x0e, 0xf2, 0x12, 0x78, 0x17, 0x8e, + 0xb1, 0x14, 0x9c, 0x5c, 0x15, 0xd5, 0x61, 0x1f, 0x06, 0x2f, 0x66, 0x7c, 0x6f, 0xcf, 0x27, 0xab, + 0xce, 0x71, 0xb1, 0x86, 0x43, 0xc8, 0xb7, 0x0d, 0xb8, 0x29, 0xd7, 0x1f, 0xac, 0x9e, 0xb1, 0xe7, + 0xce, 0xb7, 0x8b, 0x61, 0x16, 0xef, 0x33, 0x88, 0x0a, 0x48, 0x18, 0x7d, 0xa4, 0xdd, 0xb6, 0x99, + 0x20, 0xd6, 0x4d, 0xc2, 0xda, 0x10, 0x75, 0x05, 0x8c, 0x47, 0xa7, 0xc8, 0xbd, 0xdd, 0x3e, 0x83, + 0xb3, 0x15, 0x24, 0x48, 0x7d, 0xa2, 0x84, 0x71, 0x9e, 0xbf, 0x83, 0x9c, 0xc6, 0x81, 0x9e, 0x29, + 0x35, 0x12, 0x09, 0x12, 0xe6, 0x18, 0x40, 0xba, 0xdc, 0xb5, 0x73, 0xc9, 0x61, 0xc2, 0x4d, 0x29, + 0xc6, 0xfb, 0x53, 0x59, 0x24, 0x2a, 0x72, 0x6a, 0x17, 0x6b, 0xae, 0x4a, 0x0d, 0x72, 0x1f, 0x90, + 0xc6, 0xa4, 0xec, 0xc4, 0x7f, 0x00, 0xa4, 0x33, 0xe3, 0xde, 0x93, 0x14, 0x8e, 0x21, 0x24, 0x3b, + 0x31, 0xb3, 0x1f, 0x55, 0xb2, 0x16, 0x29, 0x1f, 0xbb, 0xdf, 0x46, 0x63, 0xbd, 0xe2, 0x92, 0xce, + 0x84, 0x9c, 0xa3, 0xb2, 0x4c, 0xf1, 0x34, 0x89, 0xb7, 0xc6, 0x41, 0x90, 0x72, 0xfd, 0x51, 0xac, + 0x79, 0x92, 0x69, 0x72, 0xfa, 0x9c, 0x66, 0x7d, 0xa4, 0x8d, 0xd5, 0xf4, 0xa5, 0x46, 0xb0, 0x11, + 0x89, 0x11, 0x68, 0x4d, 0x48, 0x81, 0x55, 0x8b, 0xdb, 0xed, 0x33, 0x45, 0x85, 0x95, 0xa1, 0x56, + 0x48, 0x54, 0x2c, 0x57, 0xea, 0x5e, 0x7b, 0x99, 0xa7, 0xac, 0xa7, 0x6f, 0x95, 0x02, 0x43, 0xbd, + 0xa2, 0x09, 0xf4, 0xbc, 0x16, 0xe0, 0x4d, 0x3c, 0x30, 0xce, 0xd4, 0x9e, 0x18, 0x7f, 0x03, 0xe2, + 0x07, 0x83, 0x26, 0x2f, 0x64, 0xc6, 0x47, 0x27, 0x0a, 0xe1, 0xe0, 0xcb, 0xd1, 0x2e, 0xf4, 0x7a, + 0xc3, 0x54, 0x29, 0xb4, 0xe0, 0x08, 0x92, 0xc9, 0x42, 0xaf, 0x54, 0x72, 0xdb, 0xd2, 0x05, 0x49, + 0xd8, 0x9d, 0x71, 0x79, 0x6a, 0xe6, 0xa1, 0x71, 0x30, 0xe9, 0x7d, 0xfa, 0xd4, 0x9d, 0x8e, 0x3f, + 0x7d, 0x9a, 0x77, 0x3e, 0x7d, 0xa2, 0x9f, 0x3e, 0xcd, 0xbb, 0x2f, 0xc6, 0x38, 0xdf, 0x2b, 0x80, + 0x1c, 0x8f, 0xc4, 0xcc, 0x9b, 0x7c, 0xfa, 0x94, 0xe6, 0x9f, 0x3e, 0xa1, 0x69, 0x17, 0x79, 0x5d, + 0xdd, 0xf5, 0xf0, 0x84, 0xe4, 0x9f, 0x3e, 0xa5, 0x0f, 0x7d, 0x7e, 0xfa, 0x94, 0x76, 0x3e, 0x7d, + 0xc2, 0x63, 0x8f, 0x6c, 0xfe, 0x70, 0xdb, 0x07, 0x41, 0xfd, 0x6a, 0xbc, 0x06, 0x3f, 0xf1, 0xe5, + 0xb7, 0x77, 0x1b, 0xe4, 0xa9, 0xe5, 0xcc, 0xeb, 0xc6, 0x98, 0x7c, 0x7f, 0x98, 0x1b, 0x7a, 0xdd, + 0x0d, 0x26, 0x3f, 0xee, 0x65, 0xaf, 0xd2, 0xd8, 0x54, 0xfe, 0xf2, 0x30, 0xd7, 0x56, 0xfe, 0xc8, + 0xbc, 0x7f, 0xa2, 0x71, 0xf0, 0xc5, 0x38, 0xf7, 0xef, 0x60, 0xc2, 0x7f, 0x67, 0x1e, 0x9a, 0xf8, + 0xbd, 0xeb, 0xb0, 0xb7, 0xf8, 0xaa, 0xf7, 0xa7, 0xe9, 0xae, 0x5f, 0x60, 0x8f, 0xbc, 0x39, 0xc8, + 0x1c, 0x40, 0xe6, 0x37, 0x4d, 0x80, 0x1f, 0xbb, 0x7f, 0x37, 0xff, 0x79, 0x5f, 0x7a, 0x98, 0xfc, + 0xe9, 0x54, 0x89, 0x2b, 0xfb, 0x61, 0xbf, 0xec, 0x8d, 0xf9, 0xcf, 0x94, 0x7c, 0x7d, 0xaa, 0xc4, + 0x95, 0xfd, 0xc6, 0xde, 0x85, 0x7a, 0x45, 0xe3, 0x64, 0x49, 0xbe, 0xb3, 0x49, 0x95, 0x64, 0x72, + 0x4e, 0x7e, 0xb1, 0x1f, 0x8b, 0x38, 0x49, 0x54, 0x1d, 0xda, 0xf0, 0xe7, 0x83, 0xbd, 0xc4, 0xd4, + 0x59, 0x0b, 0x89, 0x6c, 0x22, 0xbc, 0x33, 0xb2, 0x6a, 0x6f, 0x3f, 0xf9, 0x4b, 0x53, 0x7c, 0xf5, + 0xfa, 0xaf, 0x75, 0xa5, 0x2a, 0x7b, 0x2f, 0x3c, 0x4c, 0x14, 0xdb, 0x84, 0x2a, 0xe5, 0x7f, 0x8a, + 0x93, 0x10, 0xb6, 0xec, 0xca, 0x01, 0x3e, 0x96, 0xe7, 0x7d, 0xdf, 0xef, 0xa8, 0x40, 0xd5, 0xa0, + 0xfe, 0xbb, 0xe1, 0x3c, 0x33, 0xad, 0xde, 0xc2, 0x36, 0x4f, 0xfa, 0x57, 0x8d, 0xee, 0xfe, 0xb6, + 0x77, 0x88, 0xae, 0x93, 0x0f, 0xc6, 0x31, 0x8c, 0xfa, 0x57, 0x98, 0x6e, 0x42, 0x60, 0x13, 0xa5, + 0xd1, 0x80, 0x78, 0xbe, 0x87, 0x0b, 0x18, 0xd2, 0xcf, 0xac, 0x11, 0x2a, 0x52, 0x01, 0x89, 0x1b, + 0x27, 0xed, 0x5f, 0xb2, 0xdd, 0x2a, 0xf0, 0x49, 0x1a, 0xf8, 0x64, 0x1b, 0xf8, 0x24, 0x0c, 0xfa, + 0x05, 0x29, 0xc5, 0x56, 0xca, 0x75, 0x25, 0xb3, 0x12, 0xf9, 0x66, 0x05, 0xaa, 0x38, 0x93, 0xf6, + 0x5b, 0x48, 0xa1, 0x45, 0x18, 0xff, 0x6c, 0x34, 0xc4, 0x59, 0x25, 0x91, 0xbf, 0xb4, 0xfd, 0x6a, + 0x16, 0xd7, 0x72, 0xa9, 0x8a, 0x08, 0x01, 0x80, 0xec, 0xe0, 0x70, 0xe3, 0xf8, 0xc0, 0x02, 0x9f, + 0x37, 0xe8, 0xaa, 0x49, 0x3c, 0x17, 0xe4, 0x55, 0xf1, 0xe5, 0xb9, 0x75, 0xf5, 0x6b, 0x3c, 0x36, + 0xfd, 0xad, 0xf8, 0x9d, 0x45, 0x01, 0xd3, 0xc1, 0xf9, 0x3f, 0x81, 0x92, 0xc7, 0x07, 0x35, 0xd4, + 0x72, 0xd6, 0xa8, 0x01, 0xe4, 0x5b, 0xd7, 0x28, 0xa5, 0xfe, 0x2a, 0x8d, 0xcb, 0x3a, 0xb5, 0x60, + 0x29, 0xc3, 0x7e, 0xce, 0xea, 0x91, 0x59, 0xa1, 0x00, 0xd4, 0x66, 0xe5, 0x90, 0xf7, 0x56, 0x6e, + 0xc3, 0x58, 0xcc, 0x5b, 0x51, 0x12, 0x27, 0xaa, 0x65, 0x37, 0x7f, 0x3c, 0xd2, 0x2d, 0x21, 0x53, + 0x1d, 0xca, 0x08, 0x5a, 0xc5, 0x6e, 0xa0, 0xe9, 0x16, 0x8e, 0x24, 0xe1, 0x27, 0xf0, 0x14, 0x68, + 0x08, 0xba, 0xdd, 0xf6, 0x96, 0x65, 0x62, 0xe6, 0x59, 0x17, 0xb4, 0x1b, 0x31, 0xd3, 0x81, 0xb7, + 0x2a, 0xcb, 0xd2, 0x32, 0xb1, 0x6d, 0x54, 0x5a, 0xa5, 0xdb, 0xd3, 0x95, 0xe2, 0xbd, 0x4a, 0x31, + 0x54, 0xba, 0xe1, 0xf1, 0x56, 0x48, 0x57, 0xc1, 0xce, 0xd8, 0x66, 0x31, 0xed, 0x12, 0xb8, 0x28, + 0x88, 0x59, 0x1e, 0x58, 0x64, 0xc9, 0xe3, 0xa3, 0x8d, 0xc0, 0x6e, 0x02, 0xd0, 0x74, 0xa2, 0xa7, + 0xf5, 0x7a, 0x4f, 0xf4, 0x94, 0xc0, 0x86, 0xb0, 0x2b, 0xac, 0x53, 0x9e, 0x28, 0xd8, 0xd0, 0x40, + 0x29, 0xdd, 0x5b, 0x6a, 0x47, 0x6b, 0x16, 0x07, 0x87, 0xd4, 0x03, 0xf5, 0xa3, 0x38, 0x91, 0xbc, + 0x59, 0xbf, 0x54, 0xe5, 0xf9, 0x2d, 0x50, 0x2b, 0x6c, 0x94, 0x50, 0x2d, 0x93, 0xb0, 0xbb, 0x1d, + 0xc1, 0xad, 0x68, 0xd3, 0x05, 0xa4, 0x90, 0x18, 0xba, 0xfa, 0x98, 0xfc, 0xb4, 0x9c, 0xed, 0x91, + 0x5b, 0x19, 0x82, 0xb4, 0x3a, 0xbf, 0xf2, 0x09, 0xd8, 0x01, 0x29, 0x30, 0x21, 0x01, 0x3d, 0x74, + 0x6b, 0x52, 0x9c, 0xfd, 0x02, 0xbb, 0x7d, 0xc6, 0x64, 0x8f, 0x93, 0x84, 0xa9, 0x0e, 0xea, 0xf7, + 0x04, 0x26, 0xb1, 0x4d, 0x65, 0x1d, 0x81, 0x49, 0x6a, 0xd3, 0xf0, 0x89, 0xe1, 0x3b, 0x62, 0xfc, + 0xc5, 0x15, 0x09, 0xd9, 0x24, 0x25, 0x8a, 0x28, 0x12, 0x93, 0x84, 0x24, 0x53, 0x38, 0x78, 0x5c, + 0xb0, 0x49, 0x62, 0x1c, 0x2f, 0x26, 0x1b, 0xb2, 0x1c, 0x1f, 0xec, 0x54, 0xf0, 0x67, 0x34, 0x18, + 0x0e, 0x3b, 0x13, 0x57, 0x1d, 0xea, 0x98, 0x26, 0x3e, 0x19, 0x0c, 0x87, 0x98, 0x2c, 0x5d, 0x79, + 0x58, 0x66, 0xcc, 0x5c, 0xc6, 0xc2, 0x65, 0x14, 0x30, 0x43, 0xb5, 0x9c, 0x7d, 0x4c, 0xbe, 0x4f, + 0xb7, 0x27, 0x67, 0xa8, 0xce, 0x07, 0xc3, 0xa1, 0x99, 0xe2, 0xd2, 0xa4, 0x60, 0x8a, 0x33, 0x93, + 0xe2, 0xac, 0x92, 0x65, 0x36, 0x34, 0x88, 0x64, 0xac, 0x12, 0x73, 0x2e, 0x27, 0x61, 0xbc, 0x97, + 0x91, 0x98, 0xf9, 0x24, 0x65, 0x1c, 0x02, 0x08, 0x18, 0x63, 0x7c, 0xec, 0x07, 0xc9, 0x39, 0x1f, + 0xa5, 0xb7, 0x02, 0x76, 0x49, 0x8e, 0x77, 0x51, 0x98, 0xf2, 0x56, 0x16, 0xc4, 0xcc, 0x1d, 0xcf, + 0x8f, 0x4c, 0x86, 0x0c, 0x62, 0x86, 0x44, 0x4f, 0xe1, 0xf3, 0xa4, 0x8b, 0xc4, 0x6b, 0x35, 0xbe, + 0x0a, 0x7c, 0xdc, 0xac, 0x20, 0xa0, 0x82, 0xea, 0x49, 0xa8, 0x30, 0x68, 0x16, 0x28, 0x28, 0x90, + 0x3d, 0x01, 0x05, 0x97, 0x6e, 0x73, 0xde, 0xad, 0x82, 0x2b, 0xbf, 0x13, 0xbf, 0xb8, 0xb8, 0x02, + 0x11, 0xf5, 0x67, 0x04, 0x32, 0x33, 0x22, 0x3e, 0xe9, 0xfb, 0x3e, 0x26, 0x5b, 0x97, 0x91, 0xba, + 0x8c, 0xa2, 0x5a, 0xf8, 0xef, 0xd3, 0xf8, 0x24, 0x5a, 0xec, 0x7a, 0x8b, 0x6a, 0xbd, 0x15, 0x43, + 0x83, 0x9e, 0xc4, 0x1d, 0x41, 0x38, 0x53, 0xaf, 0x59, 0x7f, 0xac, 0x82, 0x41, 0x4f, 0x91, 0x8c, + 0xf1, 0xd7, 0x7d, 0xde, 0xbb, 0x1e, 0xfb, 0x81, 0xec, 0x88, 0xf3, 0x52, 0x80, 0xed, 0x56, 0x81, + 0xa6, 0xab, 0x6a, 0x1c, 0x59, 0x39, 0x8e, 0x38, 0xf8, 0x33, 0x1a, 0xfa, 0x1d, 0xb5, 0x3f, 0x8c, + 0xf8, 0xa1, 0xd5, 0x19, 0x74, 0x20, 0x8a, 0xc0, 0x90, 0x5f, 0x07, 0xc9, 0xd7, 0xac, 0xef, 0xfb, + 0x63, 0x19, 0x0c, 0x7c, 0x1f, 0x70, 0x62, 0x47, 0x25, 0xbb, 0xa2, 0x1c, 0xc0, 0xa0, 0x23, 0xce, + 0x91, 0xec, 0x0a, 0x7c, 0x7a, 0x10, 0xaa, 0x89, 0x0c, 0x53, 0xef, 0x7c, 0xd0, 0x1c, 0x87, 0x65, + 0xef, 0x07, 0x58, 0xc1, 0x1a, 0xd9, 0x4c, 0x43, 0xaf, 0xa5, 0x90, 0x56, 0x8c, 0xbf, 0xbe, 0xba, + 0x1a, 0x23, 0xc9, 0x2c, 0x05, 0xf5, 0xfa, 0xc3, 0x21, 0x1d, 0x0c, 0x2f, 0x5f, 0x0d, 0x87, 0x57, + 0x83, 0x97, 0xfe, 0x75, 0xff, 0xe5, 0x75, 0x8f, 0x5e, 0x5e, 0x0e, 0xaf, 0xaf, 0xae, 0x87, 0xfe, + 0xe5, 0xd5, 0xf5, 0xf0, 0xe5, 0x75, 0xff, 0xe2, 0xa2, 0x83, 0x04, 0xe3, 0xbd, 0x01, 0xee, 0xf6, + 0xfd, 0x4b, 0x7a, 0x79, 0x3d, 0xe8, 0x5f, 0xf5, 0xaf, 0xaf, 0x2f, 0xae, 0x2f, 0x5e, 0xbd, 0x7a, + 0xd5, 0xf9, 0x0d, 0x09, 0x4c, 0xf8, 0xeb, 0x81, 0x3f, 0xf6, 0x03, 0xfa, 0x6a, 0xf0, 0xf2, 0xd2, + 0xbf, 0xbe, 0xf2, 0xaf, 0x2e, 0x7d, 0xff, 0xe5, 0xc5, 0xf5, 0xb0, 0x83, 0x14, 0xe3, 0xbd, 0xbe, + 0x8f, 0x7b, 0x83, 0xe1, 0x25, 0x7d, 0x79, 0x75, 0x7d, 0x31, 0xec, 0xbf, 0xba, 0xec, 0x0f, 0xfc, + 0x6b, 0x7f, 0xd0, 0xed, 0xf7, 0x87, 0xf4, 0xea, 0xe5, 0xf5, 0xf5, 0xe5, 0xa5, 0xdf, 0xf7, 0xaf, + 0xae, 0xfa, 0x97, 0x2f, 0x3b, 0xbf, 0x21, 0x85, 0xe1, 0x40, 0x84, 0x5d, 0x0c, 0xfb, 0xf4, 0xfa, + 0xe5, 0xd5, 0xb5, 0x3f, 0xbc, 0xba, 0x7a, 0xe5, 0x0f, 0xaf, 0xae, 0x2f, 0xba, 0xb4, 0xdf, 0xbf, + 0x1c, 0xf8, 0x57, 0x97, 0xc3, 0x8b, 0x97, 0xaf, 0x2e, 0xfb, 0x57, 0xc3, 0x0e, 0x9c, 0xe4, 0xf6, + 0x86, 0x43, 0xdc, 0xbb, 0xf4, 0xe9, 0x60, 0x78, 0x71, 0x75, 0x75, 0xe1, 0x5f, 0x5f, 0x5c, 0x0c, + 0xfa, 0x03, 0x80, 0x03, 0xfb, 0x38, 0xbb, 0x18, 0x0c, 0xe9, 0xe5, 0xe5, 0xf5, 0x65, 0x7f, 0x30, + 0x7c, 0xd9, 0xef, 0x5f, 0xbf, 0xbc, 0xec, 0x52, 0xff, 0xe5, 0xf5, 0xe5, 0xc5, 0xe5, 0xf0, 0x0a, + 0xea, 0x5f, 0x0d, 0x2e, 0x2e, 0x07, 0x76, 0x6e, 0x43, 0x18, 0xe2, 0x2b, 0xea, 0xbf, 0x1a, 0x0e, + 0xae, 0xaf, 0x2e, 0x86, 0xfe, 0xcb, 0xeb, 0xe1, 0x4b, 0x3b, 0x35, 0xc3, 0xa2, 0x86, 0xa9, 0x8d, + 0xf0, 0x68, 0x30, 0xf1, 0x2f, 0x50, 0xdc, 0xe0, 0xe1, 0x5f, 0x90, 0xc2, 0x47, 0x2c, 0xfc, 0x17, + 0x2b, 0x89, 0x4f, 0x5a, 0x75, 0x86, 0x4a, 0x94, 0xe5, 0x5e, 0xc2, 0xd9, 0x80, 0x5f, 0x90, 0x8c, + 0x5d, 0xf2, 0xcb, 0x11, 0xbd, 0x7c, 0x9d, 0xf5, 0xf8, 0xc8, 0xae, 0x63, 0xc2, 0xf6, 0xd6, 0x19, + 0x49, 0x46, 0x87, 0x1d, 0x94, 0x75, 0xc1, 0x32, 0x4a, 0xe8, 0xec, 0x3c, 0xa1, 0xea, 0xbf, 0x98, + 0x3a, 0x17, 0xe3, 0x8c, 0xc9, 0x80, 0x33, 0x59, 0xf9, 0x49, 0x0a, 0x72, 0x28, 0xe6, 0x65, 0xbb, + 0xbd, 0x44, 0x0d, 0x2b, 0xd0, 0x68, 0x3b, 0x26, 0x0f, 0x94, 0x1e, 0x14, 0x93, 0xc9, 0x0e, 0x62, + 0x06, 0xbd, 0x55, 0xba, 0xf5, 0xc8, 0x92, 0xeb, 0xa0, 0x21, 0x8a, 0xed, 0xbe, 0x6f, 0xb5, 0x80, + 0x43, 0x62, 0xd5, 0x34, 0x25, 0xdb, 0x40, 0xd3, 0x6d, 0x51, 0x90, 0xb4, 0xd9, 0xac, 0x66, 0x4e, + 0xdb, 0x0e, 0xe2, 0x90, 0xec, 0xf6, 0x22, 0xc9, 0xa1, 0xea, 0x51, 0x6a, 0x59, 0xbb, 0x55, 0x70, + 0xd6, 0x27, 0x5b, 0xf8, 0x27, 0x85, 0x7f, 0xc2, 0xe0, 0xac, 0x5f, 0x54, 0xf6, 0xac, 0xb2, 0xee, + 0x39, 0x31, 0x51, 0x53, 0x06, 0x21, 0x16, 0x67, 0x4c, 0x4e, 0xd4, 0x74, 0xe4, 0x94, 0x1d, 0x4d, + 0x90, 0xa0, 0xab, 0x3c, 0x17, 0x34, 0x85, 0x7f, 0xb6, 0xf0, 0x0f, 0xf8, 0x10, 0xf7, 0x3a, 0xb2, + 0x46, 0xac, 0x70, 0x5e, 0xea, 0xb2, 0x65, 0x51, 0x90, 0x6a, 0xf2, 0xe1, 0xd1, 0xec, 0x1d, 0x4e, + 0xf7, 0x34, 0xa1, 0xa3, 0xa9, 0xd6, 0x3b, 0x77, 0x0d, 0x2c, 0xe3, 0x0f, 0xc1, 0xb2, 0x60, 0xe8, + 0xea, 0x11, 0x38, 0x80, 0xe0, 0xa2, 0x82, 0x95, 0x86, 0x3a, 0x53, 0xa1, 0xb1, 0x16, 0x1f, 0x07, + 0x99, 0x3e, 0x06, 0x32, 0x6d, 0x82, 0xb4, 0x2a, 0xcd, 0xe3, 0xd0, 0xb6, 0x8f, 0x41, 0xdb, 0x36, + 0xa1, 0x85, 0xf1, 0x66, 0x15, 0x3e, 0x01, 0x2d, 0x7c, 0x04, 0x5a, 0x8d, 0xdd, 0x55, 0xba, 0x25, + 0xbb, 0x30, 0xd0, 0x05, 0xae, 0x80, 0x3b, 0x2d, 0xe7, 0x01, 0xe8, 0x7b, 0xdc, 0x86, 0x4a, 0x3d, + 0xeb, 0xa1, 0x35, 0x02, 0x15, 0x6c, 0x9f, 0xa9, 0x74, 0xdd, 0x91, 0xe2, 0xf3, 0x47, 0xe7, 0xa0, + 0x96, 0x33, 0xaa, 0x1e, 0x01, 0x5c, 0xcf, 0x42, 0x2d, 0x67, 0x20, 0x39, 0x9a, 0xb3, 0x58, 0x2a, + 0xce, 0xe5, 0x93, 0xe0, 0x97, 0xcf, 0x07, 0xbf, 0xdc, 0x03, 0x3f, 0x7b, 0x6a, 0x39, 0x01, 0xfa, + 0xec, 0xf9, 0xd0, 0x67, 0x7b, 0xd0, 0xc1, 0xbe, 0x3c, 0x2d, 0x16, 0x6a, 0x8d, 0x0c, 0x39, 0xee, + 0x20, 0x46, 0x4f, 0xb1, 0x3a, 0x8a, 0x95, 0x70, 0xb5, 0x7e, 0xf4, 0x1d, 0x08, 0xd2, 0x65, 0xf0, + 0x1d, 0xc8, 0xcf, 0x59, 0xf0, 0x1d, 0x02, 0xdf, 0xd8, 0x13, 0x54, 0x51, 0x2b, 0x44, 0x60, 0xb2, + 0xec, 0xc2, 0xa0, 0x8a, 0xbd, 0xd6, 0x34, 0x1c, 0xf7, 0x03, 0x4d, 0xc3, 0xfd, 0x81, 0x3e, 0x83, + 0x85, 0xcd, 0x0c, 0xc3, 0xc0, 0xa4, 0x0d, 0xe9, 0x16, 0x8f, 0xd1, 0x4b, 0x53, 0x40, 0xc4, 0x8f, + 0xa3, 0xe1, 0xfb, 0x34, 0xde, 0x43, 0xc3, 0xca, 0xaa, 0x03, 0x06, 0x0d, 0x71, 0x2d, 0x3b, 0x0d, + 0x1a, 0x52, 0x8b, 0x86, 0xf8, 0xb9, 0x68, 0x28, 0x35, 0x8f, 0xe7, 0xa0, 0xc1, 0x18, 0xf3, 0x4f, + 0xa2, 0x61, 0x95, 0xc6, 0xcf, 0x44, 0x83, 0xb1, 0x37, 0x9a, 0x48, 0xb6, 0x56, 0xd5, 0x63, 0x5b, + 0x85, 0x5a, 0x96, 0xeb, 0x0e, 0xf5, 0xc1, 0x7f, 0x41, 0x55, 0xd7, 0x23, 0x2d, 0x48, 0x2c, 0xcb, + 0xc4, 0xac, 0xeb, 0x61, 0xef, 0x81, 0x9d, 0xa3, 0x3c, 0x29, 0x00, 0xaf, 0xe7, 0x02, 0x21, 0xc9, + 0x7e, 0xa5, 0xfc, 0x8e, 0x47, 0xe0, 0x23, 0x1a, 0x23, 0xc1, 0xfe, 0x82, 0x20, 0x5c, 0xc9, 0xee, + 0xc1, 0xca, 0x7c, 0x0d, 0xdc, 0x17, 0x37, 0x5f, 0x17, 0xf6, 0xcb, 0xe8, 0x14, 0xdf, 0x57, 0x2d, + 0x4d, 0x28, 0xe0, 0x33, 0x9b, 0x92, 0xcc, 0x7c, 0x5d, 0x4e, 0x49, 0x1f, 0x63, 0x72, 0x26, 0x1f, + 0xb6, 0x12, 0xd5, 0x72, 0xd6, 0x72, 0xe6, 0x2e, 0x1e, 0x55, 0x74, 0xb3, 0x53, 0x81, 0x20, 0xcb, + 0x40, 0x91, 0x59, 0xc0, 0x49, 0x18, 0x64, 0xc5, 0x1e, 0x95, 0x3e, 0x0b, 0x83, 0x61, 0x03, 0x85, + 0xe1, 0xc3, 0x38, 0xb4, 0x89, 0xf0, 0x34, 0x32, 0x0f, 0x8d, 0xe5, 0x9a, 0x48, 0x4a, 0x13, 0xfb, + 0x99, 0xcb, 0xf8, 0x85, 0xd7, 0xfd, 0x1b, 0xd2, 0x54, 0x61, 0xf3, 0xb3, 0xb4, 0x3f, 0xa7, 0x24, + 0xed, 0xfe, 0xf2, 0x0d, 0x86, 0x43, 0xb7, 0x80, 0xdf, 0xec, 0x2d, 0x60, 0xff, 0x65, 0xe7, 0xbf, + 0xcd, 0x42, 0xc0, 0x22, 0xb8, 0x8f, 0xc1, 0x14, 0xd6, 0xc0, 0x7d, 0x5c, 0x4c, 0xed, 0xea, 0xfd, + 0xe9, 0x0f, 0x37, 0x03, 0xa2, 0xb1, 0x1f, 0x97, 0x0e, 0xc6, 0x0f, 0x7b, 0x30, 0x6a, 0x00, 0x75, + 0xeb, 0x66, 0x8f, 0x5f, 0xef, 0xd3, 0xcb, 0x23, 0xd5, 0x49, 0xc6, 0xca, 0x6e, 0x1e, 0xa5, 0x90, + 0x15, 0xbf, 0x7b, 0x06, 0x85, 0x80, 0x21, 0x57, 0x34, 0xd7, 0xe7, 0xd5, 0x1f, 0xa3, 0x92, 0x07, + 0x56, 0xa8, 0xfb, 0x37, 0xf4, 0x8b, 0xb1, 0x33, 0x35, 0x0d, 0xf1, 0x83, 0x2c, 0x5e, 0x3b, 0x5c, + 0x9a, 0x62, 0xe4, 0xe9, 0x01, 0xac, 0xd2, 0x52, 0xb2, 0x41, 0x7d, 0x43, 0xa4, 0xab, 0x92, 0x24, + 0xd3, 0xae, 0xf7, 0xc2, 0xa6, 0xe2, 0xae, 0xf7, 0xe2, 0xf9, 0xac, 0xfe, 0xe3, 0x29, 0x56, 0xbf, + 0xb8, 0xf2, 0x1b, 0xfc, 0x6a, 0x8c, 0xa3, 0x8a, 0x5f, 0xe1, 0xcb, 0x2c, 0xdd, 0x97, 0x27, 0x59, + 0xfd, 0xa9, 0xa6, 0xcf, 0x67, 0xf5, 0x55, 0x1a, 0x1f, 0x2c, 0x24, 0xc8, 0xc6, 0xdd, 0x2a, 0x10, + 0x24, 0x0d, 0x14, 0x89, 0x0f, 0x59, 0x1d, 0x24, 0xf1, 0xb3, 0x70, 0x18, 0x36, 0x90, 0x18, 0x3e, + 0x86, 0xc5, 0xa7, 0x99, 0xbd, 0xe1, 0xf5, 0x2a, 0x8a, 0x29, 0x26, 0x71, 0x81, 0x70, 0xed, 0x0b, + 0xfd, 0x47, 0x03, 0xed, 0x4c, 0xd3, 0x5b, 0x31, 0xd7, 0x2b, 0xb3, 0x41, 0xa5, 0xb1, 0x98, 0x73, + 0xf5, 0x41, 0xfc, 0xce, 0xc1, 0x76, 0xa4, 0xb3, 0x44, 0xcd, 0xb9, 0xfa, 0xbb, 0x29, 0x86, 0xc0, + 0xd6, 0x55, 0x28, 0xe7, 0x31, 0xff, 0x29, 0x9c, 0x8b, 0x2c, 0x25, 0x89, 0xb9, 0xf0, 0x35, 0x9f, + 0xc3, 0xf9, 0x42, 0x5c, 0xb7, 0x5d, 0x85, 0x1b, 0x4e, 0x52, 0xe6, 0xad, 0x12, 0x25, 0x7e, 0x4f, + 0xa4, 0x0e, 0x63, 0xcf, 0xec, 0x50, 0x71, 0x78, 0x9f, 0x64, 0xfa, 0x1b, 0xa1, 0xb8, 0x19, 0x43, + 0x6d, 0x93, 0xba, 0x53, 0x49, 0xc9, 0x14, 0x06, 0x5b, 0xb9, 0x93, 0x74, 0x07, 0x9d, 0x8c, 0x78, + 0x91, 0x50, 0x91, 0x8d, 0xab, 0x8d, 0xc7, 0x3b, 0xdb, 0xb1, 0x71, 0x81, 0x06, 0x55, 0xaf, 0xdd, + 0x83, 0x01, 0x95, 0x1f, 0x72, 0xc9, 0x03, 0xd1, 0x1b, 0x74, 0x92, 0x1e, 0x00, 0x32, 0x93, 0x0b, + 0x04, 0x59, 0x71, 0xb1, 0x5c, 0xe9, 0x40, 0x90, 0xe8, 0x2e, 0x10, 0xe7, 0x03, 0x12, 0xdd, 0x9b, + 0x1f, 0x65, 0xda, 0x42, 0xb2, 0xc7, 0xcf, 0x07, 0x45, 0xb0, 0xd7, 0x95, 0x3a, 0x1f, 0x1c, 0x40, + 0x55, 0x65, 0x03, 0x28, 0xba, 0x0b, 0x7c, 0x72, 0x1f, 0xf8, 0xae, 0x8b, 0x74, 0xac, 0xea, 0x6e, + 0xd2, 0xb1, 0x08, 0x54, 0xe3, 0xd0, 0xef, 0x7f, 0x9a, 0xae, 0x64, 0x40, 0x3f, 0x51, 0x4c, 0x38, + 0xc4, 0xc3, 0xa5, 0x0c, 0xdb, 0x8c, 0x64, 0x90, 0xac, 0x3b, 0x24, 0x49, 0xf5, 0x6d, 0x06, 0x04, + 0x27, 0x0f, 0x4f, 0xe0, 0x95, 0xa4, 0x2c, 0x1e, 0xab, 0xf3, 0x41, 0xc0, 0x61, 0x8e, 0x2c, 0xe9, + 0x9e, 0x3a, 0x8a, 0x93, 0xc6, 0x5d, 0x07, 0xce, 0x08, 0x23, 0xdc, 0x9d, 0xdf, 0xa7, 0x5c, 0x42, + 0x13, 0x08, 0x69, 0x5c, 0x40, 0x46, 0x1d, 0x0e, 0xca, 0x95, 0x02, 0xcf, 0x13, 0x1d, 0x0e, 0x8d, + 0x47, 0xc7, 0xe9, 0xb2, 0x75, 0xd9, 0xb2, 0x51, 0x66, 0x14, 0xd1, 0xba, 0x68, 0xd6, 0x28, 0xb2, + 0x56, 0x42, 0x59, 0x06, 0x5e, 0x0e, 0x41, 0x43, 0x5b, 0xe4, 0x74, 0xfc, 0xc0, 0x5e, 0x67, 0xd3, + 0xe0, 0xb4, 0xfa, 0xc8, 0xd7, 0x1b, 0xae, 0xc0, 0xf2, 0xe1, 0x86, 0xfe, 0xd6, 0xe1, 0x5d, 0x23, + 0xcb, 0xf8, 0xf1, 0x90, 0x74, 0xaa, 0x7c, 0x8f, 0xe3, 0xf3, 0xac, 0xd3, 0xf0, 0x76, 0x54, 0xbe, + 0x30, 0x9f, 0x54, 0x4e, 0xb0, 0xc4, 0x4a, 0x09, 0xdb, 0xe1, 0xaa, 0x31, 0x4a, 0x41, 0x57, 0xe7, + 0xec, 0x82, 0x5e, 0xd9, 0x92, 0x86, 0xb1, 0x55, 0x57, 0x48, 0x6d, 0x99, 0xb5, 0x9a, 0x02, 0x77, + 0xcf, 0xaf, 0x2e, 0xde, 0x16, 0x85, 0x41, 0xb1, 0xf1, 0xfd, 0x67, 0xe5, 0x20, 0xe2, 0x76, 0x1b, + 0x45, 0xac, 0xd7, 0xef, 0x44, 0xdd, 0xac, 0x3b, 0xe8, 0x24, 0x98, 0xec, 0xee, 0x82, 0x78, 0x9c, + 0x06, 0x11, 0xb9, 0x0f, 0xe2, 0x71, 0x14, 0xa4, 0x85, 0x75, 0xaa, 0x6b, 0x22, 0x35, 0x1b, 0x74, + 0xcc, 0x48, 0x7f, 0x7c, 0x4b, 0x84, 0x66, 0xa7, 0xae, 0x44, 0x21, 0xfd, 0x42, 0x76, 0x25, 0x7e, + 0x21, 0x0b, 0xa2, 0x4e, 0xd7, 0xb0, 0xd3, 0x4e, 0x7f, 0x53, 0x1a, 0xe9, 0x8e, 0xee, 0xca, 0x8e, + 0xc4, 0x45, 0xcd, 0xf8, 0xc6, 0xe9, 0x5f, 0x9f, 0x31, 0x18, 0xfa, 0x3b, 0x1f, 0xf4, 0x2a, 0x96, + 0xea, 0xed, 0xb3, 0x54, 0x6f, 0x4f, 0x04, 0x34, 0x2e, 0x74, 0xe9, 0xa6, 0xe7, 0xcd, 0x41, 0x29, + 0x75, 0x5b, 0xcb, 0x0d, 0x95, 0x54, 0xb1, 0xec, 0x22, 0xf7, 0x41, 0x01, 0x13, 0x4a, 0x60, 0x41, + 0xd9, 0xe0, 0x91, 0x44, 0xef, 0x79, 0xa4, 0x01, 0xf2, 0x8a, 0xf3, 0xf8, 0x2b, 0xb9, 0x8c, 0xad, + 0x38, 0x32, 0x9f, 0x47, 0x12, 0x04, 0xa2, 0xb6, 0xa3, 0x38, 0x89, 0x6e, 0x6e, 0x45, 0x6a, 0xe4, + 0x05, 0x1f, 0x4b, 0xa6, 0xba, 0x26, 0x9a, 0xff, 0x20, 0xf7, 0xe2, 0xca, 0xef, 0x41, 0x09, 0x34, + 0x09, 0xa5, 0x16, 0x27, 0x9a, 0xf5, 0x5f, 0xf9, 0x3d, 0x19, 0x9c, 0x28, 0x85, 0x20, 0x28, 0x06, + 0xbe, 0x4d, 0x22, 0x34, 0x92, 0x66, 0x3b, 0xa9, 0x87, 0x1e, 0x1f, 0x0c, 0xdd, 0x20, 0x08, 0x1c, + 0x8f, 0x34, 0xba, 0x23, 0x19, 0xfc, 0xdc, 0x83, 0xe7, 0xd5, 0x1c, 0x13, 0x81, 0xe3, 0x0a, 0xe4, + 0x6e, 0xd6, 0x13, 0xe6, 0xf0, 0x31, 0x66, 0x66, 0xe2, 0x66, 0xdd, 0x42, 0x1d, 0xca, 0x01, 0xea, + 0x09, 0x02, 0x9e, 0x4b, 0x74, 0x71, 0xe5, 0x9f, 0x4b, 0x8d, 0xc1, 0x31, 0x5d, 0x51, 0xb0, 0xd2, + 0x26, 0x2e, 0x9b, 0x24, 0x0d, 0xa7, 0x61, 0x7d, 0xf6, 0x85, 0x62, 0x30, 0x2a, 0x1a, 0xdf, 0x7d, + 0xdf, 0x3f, 0x4f, 0x3a, 0x69, 0x33, 0xfe, 0x20, 0x3d, 0x5e, 0x3c, 0xb3, 0x0b, 0xcc, 0x92, 0xbb, + 0xef, 0x8d, 0x14, 0xda, 0x5f, 0x48, 0x59, 0x8a, 0x34, 0x77, 0xf3, 0x70, 0x2c, 0x02, 0x59, 0xae, + 0xea, 0x43, 0xa2, 0xb8, 0xd1, 0x5b, 0x74, 0x80, 0x99, 0xb4, 0xc2, 0xcc, 0xad, 0xdb, 0x56, 0x54, + 0x29, 0xfc, 0x12, 0xa6, 0xa8, 0x05, 0x4c, 0x8c, 0xb7, 0x38, 0xc1, 0xe7, 0x88, 0x83, 0x1c, 0xc7, + 0xc0, 0xd6, 0x24, 0x05, 0xdf, 0x33, 0xe4, 0x65, 0x55, 0x5e, 0x39, 0x52, 0x37, 0xe5, 0x03, 0x66, + 0x8f, 0x2d, 0xb3, 0x93, 0xed, 0xc9, 0xd2, 0xbe, 0xef, 0xf7, 0x52, 0x5b, 0xa3, 0x31, 0xdc, 0x50, + 0xd7, 0xf7, 0x6a, 0xab, 0x28, 0x0a, 0xe6, 0x8f, 0x78, 0x1d, 0xc1, 0xc2, 0xcb, 0x60, 0x8b, 0xcc, + 0xc4, 0x49, 0xd0, 0xbb, 0x9e, 0x84, 0xa1, 0x43, 0xf2, 0xbe, 0x67, 0x6e, 0x7f, 0xd6, 0x2c, 0x98, + 0x75, 0xb2, 0x6e, 0xd2, 0x49, 0xf0, 0xeb, 0x7d, 0xfc, 0x94, 0x71, 0x82, 0xbc, 0x19, 0xa6, 0xd7, + 0xb8, 0x37, 0xd3, 0xe0, 0xd2, 0xdd, 0x2c, 0xb9, 0xfb, 0x20, 0x7e, 0x17, 0x72, 0x19, 0x78, 0x96, + 0x81, 0x7a, 0xb3, 0xe4, 0xce, 0x23, 0x36, 0x1d, 0xec, 0x71, 0x15, 0xdc, 0x8f, 0x69, 0xa5, 0x09, + 0xe8, 0x28, 0xc6, 0x2a, 0x30, 0x25, 0x6f, 0xe0, 0xf8, 0xaa, 0x31, 0xc3, 0x95, 0x3e, 0x0c, 0xaa, + 0xe9, 0x7a, 0xbd, 0x25, 0xe0, 0x1d, 0xa2, 0x08, 0xbd, 0xae, 0xb4, 0x2a, 0x87, 0xa0, 0xeb, 0x70, + 0x83, 0x4e, 0xf8, 0xf2, 0x26, 0x7e, 0x7d, 0x0d, 0x7c, 0xd2, 0x9f, 0x76, 0xbd, 0x96, 0x69, 0xf3, + 0xc2, 0x2b, 0x30, 0xfd, 0x9c, 0x08, 0x89, 0x3c, 0xe2, 0x61, 0xa3, 0x99, 0xd4, 0x37, 0x07, 0x1a, + 0xf3, 0x39, 0x3e, 0x13, 0x1c, 0xeb, 0x40, 0x9b, 0x9b, 0x3d, 0x46, 0x10, 0xce, 0x35, 0x9b, 0x78, + 0xeb, 0x24, 0x4b, 0xcd, 0xf5, 0x04, 0x8f, 0x78, 0x3a, 0xc9, 0xa2, 0x95, 0x4b, 0x9b, 0xfc, 0x6c, + 0x53, 0xe6, 0x72, 0x39, 0xf7, 0xa6, 0x64, 0xd9, 0x10, 0x84, 0xb2, 0x71, 0x12, 0x6a, 0x3a, 0x95, + 0x2e, 0x4a, 0x11, 0x9c, 0x7e, 0x55, 0x88, 0x86, 0x98, 0x33, 0xbb, 0x46, 0x2a, 0x94, 0xf3, 0x64, + 0x8d, 0xe0, 0xfe, 0x70, 0x7d, 0xde, 0x7a, 0x71, 0x85, 0x21, 0xc4, 0xde, 0x8e, 0x13, 0x0d, 0xeb, + 0xd0, 0x33, 0x13, 0xd9, 0xf6, 0xeb, 0xaf, 0xc6, 0x81, 0xfa, 0xeb, 0xaf, 0x4c, 0x62, 0x82, 0x9a, + 0xfe, 0xd4, 0x32, 0x96, 0xc7, 0x86, 0x65, 0x22, 0x73, 0x01, 0xbe, 0x2a, 0xc4, 0x78, 0x2f, 0xaa, + 0x5f, 0xe3, 0xe3, 0xb8, 0x90, 0x07, 0xfc, 0xa6, 0x96, 0x6e, 0xcc, 0x3d, 0x18, 0x1b, 0xff, 0x61, + 0x4e, 0xcc, 0x88, 0x60, 0xbb, 0x44, 0xbe, 0x03, 0x7c, 0x7c, 0x93, 0xdc, 0xca, 0x40, 0x92, 0x44, + 0x1a, 0x9c, 0xa4, 0x46, 0x71, 0x81, 0xfd, 0xe1, 0x69, 0x55, 0xa1, 0x0c, 0xff, 0x33, 0x3b, 0xab, + 0x5a, 0x0a, 0x39, 0x2e, 0x55, 0x80, 0x77, 0xe6, 0x33, 0x28, 0xf3, 0x49, 0xc6, 0x76, 0xc9, 0x96, + 0xab, 0x45, 0x9c, 0xdc, 0x06, 0xde, 0x56, 0xa4, 0x62, 0x16, 0x73, 0x8f, 0xcc, 0x45, 0xba, 0x89, + 0xc3, 0xfb, 0x40, 0x8d, 0x3d, 0x21, 0x63, 0x21, 0x79, 0x6f, 0x06, 0x22, 0xd3, 0x03, 0x57, 0x14, + 0xfc, 0x56, 0xb7, 0xc7, 0xfd, 0xf2, 0xe8, 0xdc, 0x44, 0x9c, 0xaa, 0xb1, 0x67, 0xc1, 0xfe, 0xc0, + 0x17, 0xda, 0x0b, 0xdc, 0xc7, 0xc7, 0x64, 0xe3, 0x4d, 0x19, 0xc7, 0x64, 0x85, 0xde, 0xdb, 0x70, + 0xb7, 0xfa, 0x22, 0x39, 0x2a, 0xd7, 0x8d, 0x08, 0x92, 0xd9, 0x53, 0xc8, 0x66, 0x30, 0x53, 0x85, + 0xa0, 0x07, 0x70, 0x48, 0x5c, 0xf0, 0x91, 0x0d, 0xb6, 0x4c, 0xe4, 0x5b, 0xb9, 0xc9, 0x34, 0xe8, + 0xbb, 0x90, 0x09, 0xa1, 0x6b, 0x10, 0x8d, 0xf2, 0x35, 0xc8, 0x4b, 0x21, 0x97, 0x6f, 0x62, 0x60, + 0x84, 0x9f, 0x78, 0xa4, 0x11, 0x1e, 0x41, 0x3f, 0xe6, 0x4a, 0xcb, 0x37, 0x76, 0xbb, 0x47, 0x78, + 0x54, 0xaa, 0x27, 0x96, 0x02, 0xd3, 0x31, 0x8c, 0x12, 0x74, 0xb5, 0xf9, 0x47, 0x9b, 0x01, 0xd7, + 0x74, 0x41, 0x91, 0xe3, 0x34, 0x32, 0x90, 0xfe, 0xd1, 0x03, 0xc9, 0xb1, 0x00, 0xf1, 0x56, 0x66, + 0xfd, 0x4f, 0x4f, 0x51, 0x9d, 0x6c, 0x6a, 0xbd, 0x4b, 0xd7, 0x1a, 0x97, 0x21, 0xf1, 0x79, 0x72, + 0x2b, 0xbd, 0xc0, 0x7c, 0xd7, 0x8b, 0xea, 0x05, 0x67, 0x7d, 0x88, 0xc6, 0x35, 0xb7, 0xbd, 0xe0, + 0xf2, 0xf6, 0x5c, 0x43, 0x94, 0xd2, 0xb7, 0x61, 0xb4, 0xda, 0xe3, 0xd3, 0x2a, 0x3e, 0xf8, 0xe8, + 0xc6, 0x15, 0xf0, 0xfd, 0x6e, 0x03, 0xb1, 0x66, 0x5b, 0x0e, 0x7e, 0x75, 0x5c, 0x34, 0x4f, 0xef, + 0x1a, 0x5c, 0xd7, 0xe8, 0xda, 0x7e, 0xdb, 0x4e, 0xfb, 0xc7, 0xd5, 0xb3, 0x4d, 0xb3, 0x32, 0x30, + 0xa4, 0xab, 0x3b, 0xc0, 0xe4, 0xa9, 0xf1, 0x9d, 0xba, 0xe1, 0x75, 0x62, 0x88, 0x70, 0x7c, 0x5d, + 0xa0, 0xb7, 0x0d, 0xf3, 0x65, 0xb6, 0xb7, 0x83, 0x59, 0x3f, 0x63, 0xa6, 0x62, 0xd0, 0x67, 0x81, + 0xa6, 0x4b, 0xaa, 0x5b, 0x21, 0x13, 0x41, 0x4e, 0x76, 0xd5, 0x4d, 0xbe, 0xc0, 0x7b, 0xab, 0x92, + 0xef, 0x0d, 0xbd, 0xb4, 0xaa, 0x54, 0xaf, 0x07, 0xc2, 0xd2, 0x90, 0xa7, 0x11, 0x66, 0x10, 0x61, + 0x9c, 0x7e, 0x15, 0x69, 0xb1, 0xe5, 0x63, 0xaf, 0x51, 0xab, 0xcc, 0xf4, 0x02, 0xcf, 0xc3, 0xc4, + 0xdc, 0xf1, 0x0b, 0x76, 0x5e, 0xef, 0x96, 0xcf, 0x6e, 0x84, 0xee, 0xe9, 0x70, 0xd3, 0x5b, 0x89, + 0xe5, 0x2a, 0x86, 0xad, 0xac, 0x67, 0x62, 0x06, 0x3c, 0xeb, 0xe5, 0x41, 0x3e, 0x69, 0xb9, 0xff, + 0xf1, 0xc8, 0x23, 0x5a, 0x85, 0x32, 0x85, 0x4b, 0x6a, 0x81, 0x67, 0x92, 0x31, 0x08, 0x0b, 0xaf, + 0x0b, 0x32, 0x92, 0xde, 0x61, 0x2b, 0x7c, 0xcd, 0xc7, 0xbd, 0x91, 0xa2, 0xe4, 0x56, 0xc4, 0xb1, + 0x3d, 0xba, 0x70, 0x0d, 0xcc, 0x05, 0x37, 0xa2, 0x93, 0x4d, 0xb0, 0xd5, 0x08, 0xf4, 0x11, 0xa0, + 0xae, 0x32, 0x6d, 0xf7, 0xec, 0xad, 0x46, 0x83, 0x8e, 0xc4, 0xe5, 0xce, 0x5d, 0x7e, 0x6e, 0x92, + 0x54, 0x00, 0x02, 0x03, 0x2f, 0x9c, 0xa5, 0x49, 0x9c, 0x69, 0xee, 0x91, 0x63, 0xb6, 0x2e, 0x0a, + 0x38, 0x16, 0x5a, 0x21, 0x78, 0x7d, 0xc4, 0x23, 0x7b, 0x01, 0x8a, 0x68, 0x67, 0x6e, 0x98, 0x7d, + 0x0f, 0xaf, 0x4a, 0xec, 0x99, 0x9c, 0x9a, 0x69, 0x9d, 0xe7, 0xd5, 0xca, 0x2e, 0xb9, 0x76, 0x61, + 0xe9, 0xe9, 0xd7, 0xf7, 0x1f, 0xed, 0xfd, 0x3c, 0xe4, 0x01, 0xa7, 0x79, 0xd8, 0xdd, 0x31, 0xbd, + 0x15, 0x72, 0x9e, 0xdc, 0x52, 0x19, 0x6e, 0xc5, 0x32, 0x84, 0x80, 0xb0, 0x2c, 0xe5, 0xea, 0xab, + 0x25, 0xdc, 0xb8, 0x17, 0xec, 0xfc, 0x9f, 0x08, 0x8d, 0xcf, 0xa2, 0x95, 0x4a, 0xd6, 0x3c, 0x0f, + 0xe5, 0x5c, 0x25, 0x62, 0x8e, 0x29, 0xee, 0xa4, 0xe1, 0x22, 0x54, 0xe2, 0x5c, 0x94, 0x57, 0x42, + 0xe1, 0xf1, 0x13, 0xf1, 0xe3, 0x2a, 0x91, 0x3c, 0x17, 0x3f, 0x26, 0xf3, 0x5c, 0xfc, 0x18, 0xce, + 0x1b, 0xa5, 0xbc, 0xec, 0x05, 0x82, 0xfb, 0x1b, 0x2a, 0x23, 0x12, 0x79, 0xae, 0x70, 0xbb, 0xed, + 0xbf, 0xd6, 0xe5, 0xbd, 0xdd, 0xb1, 0x8b, 0xb5, 0x8b, 0x92, 0xb8, 0xeb, 0x9d, 0x9f, 0x7b, 0x5d, + 0x4e, 0x57, 0x49, 0xaa, 0xbb, 0x9c, 0x6e, 0x42, 0xbd, 0x92, 0xe1, 0x9a, 0x77, 0x21, 0xbc, 0x31, + 0x54, 0xd1, 0xaa, 0xab, 0x03, 0x88, 0x64, 0x2e, 0xa5, 0x10, 0x44, 0x1e, 0x92, 0x33, 0x8b, 0x31, + 0x67, 0xb3, 0x92, 0x5d, 0x74, 0x17, 0x28, 0x50, 0x6f, 0x39, 0x51, 0x81, 0x24, 0x0b, 0x11, 0xc7, + 0x81, 0x27, 0x13, 0x09, 0x7b, 0x58, 0xaa, 0x55, 0x72, 0xc3, 0x7b, 0x66, 0xa1, 0xbc, 0x60, 0x40, + 0xec, 0x77, 0xe0, 0x7d, 0xe1, 0xfb, 0xbe, 0x57, 0x3c, 0x01, 0xaa, 0x37, 0xb0, 0xc0, 0x34, 0x85, + 0x9f, 0x47, 0x80, 0x2d, 0x16, 0x0b, 0xaf, 0x68, 0xde, 0xde, 0xdb, 0x68, 0xc4, 0x4b, 0x7f, 0x03, + 0xa7, 0xa1, 0xa1, 0xe4, 0xb7, 0x40, 0xf3, 0x24, 0x63, 0x55, 0x6c, 0x37, 0x04, 0x07, 0xbd, 0xe6, + 0xd4, 0x50, 0x6f, 0x5a, 0x63, 0xc6, 0x7e, 0x4f, 0xf4, 0x34, 0x70, 0x69, 0x22, 0xd9, 0x3f, 0xe0, + 0x05, 0x10, 0x30, 0x20, 0xad, 0x1a, 0x07, 0x0f, 0x2e, 0x38, 0x35, 0x0e, 0x5e, 0x5d, 0x70, 0x6a, + 0x5c, 0xca, 0xfe, 0x07, 0x71, 0x92, 0x41, 0x24, 0xc5, 0x83, 0x66, 0xe8, 0x1f, 0xb3, 0x42, 0x27, + 0x13, 0x9f, 0x18, 0x67, 0xb2, 0x4f, 0xbc, 0xae, 0x32, 0x1e, 0x50, 0xf8, 0x35, 0x4e, 0xe4, 0x29, + 0x99, 0x80, 0x9e, 0x68, 0x8a, 0xe1, 0x80, 0xfb, 0xb8, 0xc2, 0xf4, 0x84, 0xed, 0x5a, 0x41, 0x84, + 0x6a, 0xe0, 0x5a, 0xf5, 0x4f, 0x43, 0x2c, 0x8b, 0x4b, 0xc8, 0x7b, 0x10, 0x9b, 0x16, 0xef, 0x11, + 0xc0, 0x72, 0x1c, 0xfe, 0x69, 0x80, 0x65, 0x31, 0xf8, 0x9b, 0x2b, 0x80, 0x7b, 0x76, 0x72, 0x09, + 0x31, 0x3c, 0x6a, 0xe3, 0x06, 0xf2, 0x14, 0xe8, 0xa3, 0xf1, 0x96, 0xb6, 0x76, 0xad, 0xe2, 0x4e, + 0xa6, 0xd6, 0xc2, 0xde, 0x37, 0xba, 0x93, 0x23, 0xa3, 0x9b, 0xc4, 0x2c, 0xe9, 0x65, 0x24, 0x65, + 0x19, 0x04, 0x8f, 0x8c, 0xd2, 0xd7, 0xc9, 0x28, 0xed, 0xb2, 0xf8, 0xfc, 0x15, 0x89, 0xba, 0xac, + 0xef, 0x2e, 0x75, 0xb2, 0x9f, 0xf7, 0xce, 0xd7, 0x52, 0x4c, 0x16, 0x2c, 0xa4, 0x70, 0x01, 0x31, + 0xa4, 0x4b, 0xb2, 0x65, 0x21, 0x9d, 0x8d, 0xb8, 0x8d, 0x9b, 0x9d, 0xf4, 0x07, 0x74, 0xd8, 0x89, + 0xca, 0x71, 0x2f, 0xcc, 0x68, 0x57, 0xe6, 0xdf, 0xad, 0x19, 0x71, 0x1d, 0xcb, 0x79, 0x64, 0xb5, + 0x1b, 0xbc, 0x7c, 0xb1, 0xf0, 0x7d, 0x33, 0xf9, 0x2b, 0x7a, 0x75, 0x75, 0x05, 0xdf, 0x0b, 0xf3, + 0x7d, 0x71, 0x41, 0x2f, 0x2e, 0x2e, 0x88, 0xf7, 0x85, 0x6f, 0xbf, 0x87, 0xbe, 0x49, 0x2f, 0x20, + 0x7d, 0x55, 0xd6, 0xf5, 0x7d, 0xf3, 0xfd, 0xaa, 0xac, 0xbb, 0xb0, 0xdf, 0x06, 0x91, 0x16, 0xf0, + 0xf4, 0xd8, 0x23, 0x60, 0xd4, 0x54, 0xf6, 0x73, 0x7d, 0xdc, 0x03, 0xae, 0x3f, 0x73, 0x14, 0x0e, + 0x71, 0x76, 0xe0, 0x09, 0xc0, 0x64, 0x79, 0xb2, 0x1c, 0xe0, 0xba, 0x1a, 0xa3, 0xc6, 0x14, 0xac, + 0xcf, 0x74, 0x4e, 0xed, 0xb4, 0xe7, 0xd6, 0xd9, 0x07, 0x09, 0xeb, 0x31, 0x2d, 0x07, 0x64, 0xab, + 0x2d, 0x5d, 0xb5, 0x65, 0x59, 0x6d, 0x59, 0x56, 0x9b, 0x9e, 0x74, 0x50, 0xc0, 0x68, 0x67, 0x27, + 0x47, 0x23, 0xcc, 0xc1, 0x7d, 0xdf, 0xf7, 0xf7, 0x47, 0x63, 0x04, 0xd1, 0x7e, 0x9f, 0x33, 0xd7, + 0xe7, 0xac, 0xec, 0x73, 0x56, 0xf5, 0x59, 0x14, 0x86, 0xc3, 0xeb, 0x3d, 0x77, 0xa9, 0x0f, 0xf7, + 0x8c, 0x82, 0x70, 0xb2, 0x73, 0x5a, 0x57, 0x70, 0x32, 0xf8, 0xec, 0x74, 0x48, 0x1a, 0xe1, 0xd6, + 0x53, 0x06, 0xba, 0xd4, 0xbe, 0x53, 0x8c, 0x37, 0x9d, 0x62, 0xa3, 0xa7, 0x35, 0xdd, 0x71, 0xaf, + 0xdf, 0x11, 0xdd, 0xac, 0x9b, 0x04, 0xb2, 0x97, 0x10, 0x55, 0x87, 0x49, 0xd5, 0x66, 0x35, 0x48, + 0x29, 0x1f, 0x3b, 0x83, 0xfc, 0xc0, 0x80, 0xce, 0x3a, 0x0a, 0x3f, 0x22, 0x9e, 0x9a, 0x8e, 0xab, + 0xf4, 0x88, 0x87, 0x4a, 0xcc, 0xa4, 0xdd, 0xd8, 0xb8, 0x86, 0xd0, 0x91, 0x1b, 0x2b, 0xc5, 0xa7, + 0x7c, 0x63, 0xa6, 0xf6, 0xb1, 0xa3, 0xea, 0x82, 0x5e, 0x75, 0xe2, 0x51, 0x2d, 0x16, 0x1b, 0xe2, + 0xe7, 0x84, 0x5f, 0x0e, 0x1c, 0x6f, 0x9d, 0x78, 0x74, 0xe0, 0xac, 0x8a, 0xcd, 0xa2, 0x81, 0x20, + 0x06, 0x2e, 0x34, 0x2f, 0x96, 0x50, 0x01, 0xab, 0x63, 0x15, 0x1c, 0x88, 0x60, 0xcf, 0x26, 0xbc, + 0x31, 0xd1, 0x29, 0x53, 0x84, 0x97, 0x8a, 0x33, 0x3c, 0x7b, 0x43, 0xc5, 0x1c, 0x17, 0x05, 0x3e, + 0x8a, 0x31, 0xa9, 0x35, 0xaf, 0xb9, 0xd8, 0x7a, 0xc7, 0x84, 0x20, 0x0f, 0x95, 0xb1, 0x0f, 0xa6, + 0x0f, 0xcf, 0xa9, 0x53, 0x47, 0xf5, 0x77, 0xb5, 0xca, 0xa2, 0x78, 0x1c, 0x1a, 0xf5, 0xab, 0xd6, + 0x71, 0x54, 0x53, 0xc3, 0x49, 0xb0, 0xb3, 0x89, 0xad, 0x89, 0x0d, 0x59, 0x31, 0x26, 0xb3, 0x30, + 0xba, 0x59, 0x9a, 0x95, 0x0c, 0xbc, 0x28, 0x91, 0x22, 0xaa, 0x4d, 0xdd, 0x2f, 0xa2, 0x28, 0x6a, + 0x0d, 0x86, 0x2f, 0x48, 0x0b, 0x76, 0xcb, 0x96, 0xdf, 0x1a, 0xfa, 0x90, 0x86, 0x5c, 0xbf, 0xf5, + 0xb2, 0xce, 0xc7, 0x5e, 0x03, 0x0a, 0xb8, 0xc6, 0x03, 0xef, 0xd5, 0xe6, 0xae, 0xf5, 0x0a, 0x2c, + 0x56, 0x08, 0x24, 0x01, 0x8b, 0xc5, 0x4e, 0xf7, 0xe4, 0xdc, 0xbe, 0x73, 0xfd, 0x3d, 0x67, 0x8e, + 0xb5, 0x5a, 0x06, 0xea, 0x9d, 0x6f, 0x35, 0xbb, 0xd2, 0xbf, 0xec, 0xf5, 0x7d, 0xff, 0x85, 0x57, + 0xce, 0xd8, 0x7d, 0x3d, 0x3e, 0xe5, 0x95, 0x46, 0x1e, 0xd8, 0x65, 0xa1, 0xf2, 0xc8, 0x01, 0x8f, + 0xf0, 0x23, 0x1e, 0xf1, 0x74, 0xd2, 0xd2, 0xc9, 0xc6, 0x0b, 0x20, 0xa1, 0xa0, 0x13, 0x8f, 0x44, + 0xb8, 0x20, 0x0b, 0xd0, 0x1c, 0xec, 0x34, 0x67, 0x9a, 0xec, 0x4a, 0x35, 0x38, 0x38, 0xf3, 0x89, + 0x51, 0x9a, 0x03, 0x77, 0x83, 0x90, 0xa8, 0x80, 0xef, 0x3b, 0xe4, 0x33, 0x15, 0x57, 0x59, 0x1f, + 0xb6, 0x4b, 0xf7, 0x34, 0x59, 0x99, 0x63, 0x6e, 0x45, 0x93, 0xbb, 0x20, 0xa5, 0x77, 0xe4, 0x3e, + 0x48, 0xe9, 0x3d, 0xe8, 0x2b, 0x0d, 0x8d, 0xe5, 0xbe, 0xa1, 0xb1, 0xa4, 0xda, 0xaa, 0x1b, 0xba, + 0xf2, 0x1a, 0xe9, 0xda, 0x6b, 0xa4, 0x6b, 0xaf, 0x51, 0xa9, 0xab, 0x40, 0x70, 0xa3, 0xa3, 0x6c, + 0x22, 0x0f, 0xf4, 0x9d, 0xa8, 0xd6, 0x77, 0xc0, 0x46, 0x7f, 0x58, 0xdf, 0x91, 0xb5, 0xbe, 0x13, + 0x9e, 0xf4, 0xb5, 0x4e, 0xdc, 0x06, 0xb4, 0x58, 0x1c, 0xc8, 0x4b, 0x49, 0x57, 0x19, 0xef, 0x7a, + 0xe0, 0x5d, 0x7a, 0x41, 0x86, 0xbe, 0x11, 0x94, 0xa4, 0xde, 0xc5, 0x7d, 0x62, 0xfe, 0xab, 0x25, + 0xbb, 0x15, 0xb9, 0xd3, 0x69, 0x81, 0x7c, 0x73, 0x97, 0x97, 0xc5, 0x47, 0xde, 0x17, 0xc7, 0x59, + 0xa7, 0xd4, 0x28, 0xeb, 0x52, 0x7b, 0xf0, 0x3c, 0x41, 0x95, 0xc7, 0x35, 0x56, 0xdd, 0x8a, 0x19, + 0xec, 0xde, 0xca, 0x9c, 0x8d, 0x44, 0x10, 0xd8, 0x57, 0x39, 0xab, 0xc1, 0x33, 0xdd, 0x4d, 0x6c, + 0x20, 0x65, 0x27, 0x05, 0xff, 0x74, 0x17, 0x45, 0xbd, 0xc4, 0xc6, 0x53, 0x76, 0x22, 0x6c, 0xa5, + 0x06, 0x2e, 0xfe, 0x75, 0x61, 0x2f, 0x16, 0x08, 0xc2, 0x15, 0x2a, 0xb1, 0x1f, 0x6a, 0x2b, 0x87, + 0xc8, 0xa2, 0xbc, 0x02, 0xc6, 0xd4, 0xd8, 0xc4, 0xa6, 0x5b, 0x1a, 0x33, 0xbe, 0x2b, 0xa4, 0x70, + 0x80, 0xd2, 0x43, 0x01, 0x15, 0x99, 0xb8, 0x88, 0xc8, 0x01, 0xc0, 0xc7, 0xe2, 0xc9, 0x45, 0x78, + 0x99, 0xe7, 0x2e, 0xda, 0xed, 0x27, 0x01, 0x80, 0x28, 0xfc, 0x77, 0x08, 0xb8, 0xaf, 0xc1, 0x59, + 0xf7, 0x10, 0xe7, 0x9f, 0x96, 0x64, 0x59, 0xd3, 0x52, 0xab, 0xc4, 0xde, 0xe3, 0xb2, 0xe6, 0x19, + 0xdd, 0x3c, 0x4b, 0x7e, 0x24, 0x25, 0xc3, 0x93, 0xdd, 0x43, 0x72, 0x44, 0x27, 0xad, 0x59, 0xa2, + 0x75, 0xb2, 0xf6, 0x48, 0x08, 0x67, 0xe2, 0x56, 0x63, 0x3b, 0xa8, 0xe1, 0xe4, 0x47, 0x38, 0xf1, + 0xa7, 0xc0, 0xd4, 0x98, 0xc4, 0x60, 0xb1, 0x68, 0xb8, 0x95, 0x7f, 0x4c, 0xd4, 0xfa, 0x8c, 0xb1, + 0xa8, 0xc0, 0x0f, 0xd1, 0xfc, 0xa1, 0xdc, 0xe9, 0x3b, 0xb9, 0xe3, 0x8c, 0xf6, 0xd2, 0x22, 0xaa, + 0x8e, 0x3c, 0xff, 0x55, 0x41, 0xb4, 0x98, 0x38, 0x88, 0x53, 0x23, 0x91, 0x1a, 0x9f, 0xf7, 0xc5, + 0x63, 0xf2, 0x2f, 0x6a, 0x8e, 0x23, 0x3a, 0x18, 0x87, 0x95, 0x3c, 0xdf, 0x37, 0x46, 0x93, 0xe7, + 0xff, 0xe2, 0xe8, 0xa2, 0xfd, 0xd1, 0x45, 0x8d, 0xd1, 0x01, 0x8e, 0x67, 0xfb, 0x8f, 0x4f, 0xb2, + 0x5d, 0xd3, 0xec, 0x2c, 0x8f, 0x2a, 0x55, 0xf0, 0xca, 0xf4, 0x67, 0x1c, 0x6c, 0xb6, 0xa3, 0x9d, + 0x2b, 0x2b, 0x0a, 0xb2, 0x39, 0x00, 0x71, 0x4c, 0x4a, 0x8d, 0x43, 0xde, 0xc0, 0x9b, 0xc1, 0x6a, + 0xd7, 0x5a, 0x42, 0x15, 0x51, 0xb8, 0xaf, 0x00, 0x05, 0x83, 0x81, 0xef, 0x93, 0x7d, 0x9d, 0x27, + 0xe8, 0xf7, 0xf9, 0x45, 0xd1, 0x70, 0xe9, 0xdc, 0x56, 0xc2, 0xde, 0x1c, 0x9e, 0x70, 0x5c, 0x09, + 0xfa, 0x4a, 0xa4, 0x27, 0x0c, 0xf1, 0xbd, 0x33, 0xa4, 0x52, 0xc2, 0xe3, 0x5a, 0xf0, 0xc3, 0xe9, + 0xa7, 0x91, 0x72, 0xf0, 0x0e, 0xd9, 0x1f, 0x10, 0xa1, 0xa5, 0x25, 0xfa, 0xc8, 0xb9, 0x0d, 0x9c, + 0x49, 0xf4, 0x5f, 0xf9, 0x5d, 0x73, 0x58, 0x03, 0x0a, 0xf4, 0x99, 0x8f, 0x71, 0x07, 0x49, 0x7d, + 0x6e, 0x22, 0x0b, 0x52, 0x26, 0x9c, 0xe4, 0x4c, 0x48, 0xc4, 0xf6, 0x8f, 0x9e, 0x0e, 0x4f, 0xae, + 0xc6, 0xbd, 0x7e, 0xd0, 0xaf, 0x85, 0x2e, 0xef, 0xa6, 0xf6, 0xdc, 0x2f, 0x4a, 0x52, 0x14, 0xe3, + 0x0e, 0x1c, 0x0d, 0x66, 0x65, 0x5e, 0x0a, 0x07, 0x19, 0xb8, 0x13, 0x55, 0xc2, 0x97, 0x84, 0xec, + 0x7f, 0x4f, 0x63, 0xf0, 0x86, 0x36, 0xeb, 0xd4, 0x11, 0x43, 0xf1, 0xbf, 0x20, 0xe6, 0xc5, 0x02, + 0x9d, 0x9d, 0xd6, 0xf8, 0x4f, 0x63, 0xfa, 0xf0, 0x30, 0xb1, 0xa5, 0x34, 0x82, 0xd3, 0xb2, 0xac, + 0x27, 0xf0, 0xeb, 0xa4, 0xd4, 0x5a, 0xdd, 0xf1, 0xc9, 0x59, 0x7f, 0x74, 0xb0, 0x8d, 0x44, 0x8d, + 0x6d, 0x24, 0x39, 0xb5, 0x8d, 0x24, 0x87, 0xbb, 0x80, 0xa1, 0x16, 0x16, 0x3f, 0x7b, 0x1b, 0x79, + 0x0a, 0xc0, 0xbf, 0x6b, 0x1b, 0xf9, 0x3b, 0x10, 0xd0, 0x1f, 0xdd, 0x48, 0xd4, 0xbf, 0xb0, 0x91, + 0x98, 0x8e, 0xbe, 0x07, 0x2e, 0x7e, 0xa0, 0xaf, 0x90, 0xec, 0x1a, 0x2e, 0x4c, 0x95, 0xc0, 0xed, + 0xcd, 0x5f, 0x90, 0xd7, 0x45, 0xbc, 0x71, 0x5c, 0xdb, 0xbd, 0xf6, 0x71, 0xd7, 0x9b, 0xf3, 0xe5, + 0x9e, 0xf2, 0x7c, 0x78, 0x22, 0x7b, 0xc8, 0x16, 0x87, 0x1a, 0xba, 0xe2, 0x73, 0xd2, 0xba, 0xe7, + 0x71, 0x9c, 0xdc, 0x92, 0x56, 0x2c, 0xd6, 0x9c, 0xb4, 0xc2, 0xdf, 0xb2, 0x90, 0xb4, 0xc0, 0xde, + 0x21, 0xad, 0x75, 0x08, 0xee, 0xc2, 0x90, 0xb4, 0x14, 0x9f, 0x63, 0x2f, 0x38, 0xd9, 0xba, 0xaa, + 0x63, 0x9b, 0xd8, 0xe6, 0x16, 0x54, 0x09, 0xd8, 0xb4, 0x2e, 0x9e, 0x42, 0xc9, 0x87, 0x46, 0xfc, + 0xf5, 0xc3, 0x98, 0x69, 0xce, 0x15, 0xc6, 0x11, 0xc6, 0xf5, 0x78, 0xac, 0x47, 0xaf, 0x15, 0xc5, + 0x49, 0xca, 0x53, 0xdd, 0x4b, 0xc5, 0x9c, 0x5b, 0x13, 0x83, 0xb4, 0x0c, 0x42, 0x9d, 0x30, 0xb3, + 0x43, 0x71, 0xb8, 0xf9, 0x01, 0x96, 0x52, 0xf2, 0x34, 0x35, 0x3e, 0xc1, 0x07, 0x07, 0x57, 0x55, + 0x7b, 0xee, 0xd8, 0x8c, 0xc2, 0x49, 0x92, 0x0d, 0xbc, 0x1c, 0x7d, 0x1f, 0xf4, 0x7b, 0xa9, 0x55, + 0xf2, 0x9e, 0xc4, 0xc2, 0xd7, 0x46, 0x3c, 0x3c, 0xd2, 0x4b, 0x65, 0x2c, 0x64, 0x4f, 0xec, 0xf7, + 0xf1, 0xff, 0xff, 0xfd, 0x3e, 0xda, 0xdf, 0xef, 0xa3, 0xe7, 0xee, 0xf7, 0x71, 0x73, 0x1c, 0xf1, + 0xbf, 0x6b, 0xbf, 0x8f, 0x26, 0xf1, 0xfe, 0xe8, 0xe2, 0x83, 0xfd, 0x1e, 0x64, 0xdf, 0x4d, 0xe3, + 0xc8, 0x4b, 0x1c, 0x9e, 0x7d, 0xd6, 0x07, 0x60, 0x23, 0x71, 0xe2, 0x18, 0xd4, 0x6e, 0xab, 0xe0, + 0xf8, 0x73, 0xd7, 0xf6, 0x1a, 0x72, 0xad, 0x6f, 0xf3, 0xcc, 0xd1, 0x57, 0xca, 0xca, 0x60, 0x61, + 0x3b, 0xb7, 0x6f, 0xf7, 0x33, 0xe7, 0x7c, 0xc1, 0x95, 0xe2, 0xf3, 0x83, 0x6c, 0x31, 0x87, 0xb7, + 0x8f, 0xe7, 0x04, 0xc1, 0xd9, 0xdf, 0xbe, 0x81, 0x55, 0x7e, 0x07, 0x13, 0x97, 0x9a, 0xe2, 0x93, + 0x07, 0x45, 0xd5, 0x93, 0x1f, 0xe1, 0x7c, 0x6e, 0x45, 0xb8, 0xd9, 0xff, 0xca, 0xeb, 0xab, 0x4d, + 0xd9, 0xee, 0xe3, 0xc6, 0xa3, 0x01, 0xc7, 0x6a, 0x8b, 0x26, 0x3b, 0xd3, 0x51, 0x50, 0x4f, 0x9c, + 0xb8, 0x31, 0x34, 0x50, 0x41, 0xac, 0x45, 0x1c, 0x94, 0xee, 0xa3, 0xa2, 0x72, 0x3d, 0x8a, 0x83, + 0xa3, 0x5f, 0xf1, 0xf8, 0xd1, 0xaf, 0x30, 0xcf, 0x88, 0x3e, 0xe7, 0xe8, 0xb7, 0x9c, 0xd9, 0xa1, + 0x7b, 0xbc, 0x7a, 0x5b, 0xdc, 0x44, 0x9d, 0x34, 0xc6, 0x58, 0x3e, 0x80, 0x3e, 0x72, 0x7a, 0x0b, + 0xbf, 0x6d, 0xfd, 0x8c, 0x74, 0x75, 0x97, 0x06, 0xea, 0xd8, 0xc3, 0xa2, 0xc6, 0x59, 0xb1, 0x0b, + 0x52, 0x74, 0x00, 0xd2, 0x8d, 0x79, 0x99, 0x51, 0x12, 0x1f, 0xc2, 0x48, 0x9a, 0x25, 0xc7, 0x6b, + 0xd0, 0x08, 0x2b, 0x72, 0xbc, 0xc1, 0x64, 0xd1, 0x44, 0x75, 0x7d, 0x99, 0xd8, 0xde, 0xf1, 0xdf, + 0x1d, 0x63, 0xb5, 0xac, 0x5f, 0xd1, 0xc9, 0x5a, 0x68, 0xe4, 0xd9, 0xe5, 0x80, 0xab, 0xa2, 0x1e, + 0x11, 0x07, 0x07, 0xba, 0xee, 0xd5, 0xf8, 0x03, 0xbc, 0xec, 0x1d, 0x8a, 0xef, 0xcf, 0x45, 0x93, + 0x3e, 0x86, 0xb0, 0x04, 0xe9, 0x2e, 0x91, 0xa2, 0xff, 0x47, 0xf3, 0x72, 0x4f, 0x90, 0x33, 0xd6, + 0x18, 0x54, 0x79, 0xce, 0xfd, 0x18, 0x95, 0xf2, 0x06, 0x0a, 0x94, 0x8b, 0x73, 0x90, 0x07, 0x48, + 0xd8, 0x6f, 0x7a, 0xfc, 0x04, 0x85, 0x69, 0xdd, 0x44, 0x06, 0x5c, 0xd7, 0x7d, 0x6a, 0x02, 0x8d, + 0xf1, 0x17, 0x27, 0xc6, 0x52, 0xf5, 0xe9, 0x35, 0xf0, 0x77, 0x3c, 0xae, 0x37, 0x56, 0x70, 0x3c, + 0xf4, 0xc6, 0xc7, 0xe8, 0x80, 0x7a, 0xfd, 0xa7, 0x56, 0xa3, 0xb1, 0x16, 0xe5, 0xfa, 0x15, 0xc7, + 0x32, 0xea, 0xb1, 0x86, 0xe2, 0x19, 0x22, 0x42, 0x3e, 0x30, 0xe1, 0x38, 0x6e, 0xce, 0x36, 0x3d, + 0x98, 0x6e, 0x22, 0x1f, 0x9c, 0x67, 0x19, 0x40, 0x60, 0x85, 0xe4, 0xe8, 0xe8, 0x85, 0xe0, 0xb1, + 0x0e, 0x26, 0xfa, 0x01, 0xd1, 0x86, 0xd4, 0x44, 0x4f, 0xf3, 0xdc, 0xfc, 0xc0, 0x4b, 0x8c, 0xb8, + 0x7a, 0x8d, 0x44, 0x1c, 0x08, 0xd4, 0x89, 0x9e, 0x9a, 0xa7, 0x99, 0x8f, 0x72, 0x4f, 0xc2, 0x95, + 0xee, 0xa5, 0x16, 0x1b, 0x3f, 0x81, 0x8b, 0x93, 0xf0, 0xa0, 0xc3, 0xe2, 0x70, 0x9e, 0x8b, 0x45, + 0x73, 0xa2, 0x75, 0x0c, 0x1e, 0x2c, 0xe8, 0x1f, 0x9a, 0x9a, 0xe5, 0x52, 0xe5, 0xd0, 0x32, 0xd1, + 0x53, 0xfb, 0xa7, 0x20, 0x4a, 0xb1, 0x53, 0x3d, 0xa1, 0x20, 0x30, 0xe9, 0x1f, 0x0d, 0x03, 0x16, + 0xe7, 0xf4, 0x75, 0xc9, 0x32, 0x90, 0xc3, 0x3c, 0xe0, 0x7e, 0xf8, 0x00, 0x4e, 0xaf, 0x3f, 0xf2, + 0x5f, 0x2b, 0x78, 0x4e, 0xda, 0x5c, 0x0d, 0xac, 0x8a, 0x27, 0xaa, 0xdb, 0x9f, 0x96, 0x11, 0x1b, + 0x87, 0x3b, 0xd8, 0xe8, 0xec, 0xec, 0xf0, 0x25, 0x1c, 0xf3, 0x6c, 0x00, 0xb7, 0x4b, 0x03, 0x3f, + 0xa0, 0xe0, 0xa3, 0xc6, 0x22, 0x9b, 0x92, 0xc9, 0xe3, 0xdb, 0x95, 0x2e, 0x97, 0xc0, 0xaa, 0xd9, + 0x16, 0x4c, 0xff, 0x60, 0x9e, 0x4d, 0x61, 0xf8, 0xe0, 0xf5, 0xd0, 0xff, 0xbb, 0xa9, 0xee, 0x2f, + 0xfb, 0xc8, 0x6d, 0x23, 0xd8, 0xa0, 0xb8, 0x1a, 0x22, 0xd0, 0x51, 0x94, 0xc8, 0x28, 0xd4, 0x48, + 0x60, 0x4c, 0x50, 0x73, 0xee, 0x35, 0x59, 0x8a, 0x63, 0x19, 0xf0, 0xde, 0xbc, 0x3a, 0x9e, 0x1e, + 0xcb, 0xa5, 0x4a, 0xe2, 0xe8, 0x23, 0xa9, 0x9e, 0x8a, 0xdf, 0xf9, 0xc9, 0x06, 0x0e, 0x18, 0x2a, + 0x43, 0x47, 0x8b, 0xe3, 0xa6, 0x27, 0x5e, 0x1c, 0x78, 0x8e, 0x3c, 0x31, 0x2d, 0x51, 0x53, 0x24, + 0x3c, 0x22, 0xcf, 0x0f, 0x39, 0xdf, 0x3c, 0x18, 0x76, 0x3c, 0x60, 0x1e, 0x97, 0x7f, 0xbc, 0x62, + 0x7f, 0x4b, 0x33, 0xef, 0x63, 0x59, 0x59, 0x72, 0x04, 0xaa, 0xb1, 0x31, 0x1f, 0xc8, 0x93, 0x67, + 0x8a, 0xe9, 0x86, 0x76, 0x56, 0x3e, 0xf3, 0x70, 0x52, 0x61, 0x33, 0x23, 0x31, 0x25, 0x81, 0x0d, + 0x4b, 0xf2, 0xac, 0x59, 0x7a, 0x2c, 0xfb, 0x9a, 0xc5, 0xc7, 0x4c, 0x68, 0x2c, 0xd8, 0xc3, 0x98, + 0x2a, 0x18, 0xaf, 0xb9, 0x6e, 0x36, 0x3e, 0xea, 0xcc, 0x46, 0x28, 0x35, 0x04, 0x29, 0x91, 0x38, + 0xe8, 0x9f, 0xae, 0x6b, 0x37, 0xbc, 0xfd, 0xaa, 0x03, 0xc6, 0xaa, 0x07, 0x9b, 0x9a, 0x75, 0x21, + 0xa2, 0x68, 0xbf, 0xea, 0x21, 0x69, 0x1c, 0xc4, 0xce, 0x55, 0xaf, 0xfe, 0x59, 0x81, 0x21, 0xab, + 0x83, 0x8e, 0xd2, 0xa3, 0xb0, 0x2f, 0xcb, 0xe0, 0x65, 0x6f, 0x24, 0xd9, 0x64, 0x57, 0xbd, 0x22, + 0x14, 0xdc, 0xea, 0x82, 0x34, 0x3e, 0x37, 0xba, 0x98, 0x12, 0x4e, 0x6b, 0x13, 0x2d, 0xba, 0x37, + 0x91, 0x7d, 0xc0, 0x18, 0x7b, 0xd5, 0x88, 0x7d, 0x89, 0x3f, 0x0d, 0x76, 0x4d, 0x97, 0x98, 0x3d, + 0xd4, 0x2b, 0x0a, 0x7c, 0x68, 0x58, 0x19, 0xa3, 0xca, 0xd0, 0xc5, 0x8f, 0x22, 0xba, 0x01, 0xa3, + 0x4a, 0xcc, 0x03, 0x70, 0x18, 0x94, 0xa1, 0x45, 0x65, 0x74, 0x1d, 0xa7, 0x2e, 0x55, 0x14, 0x44, + 0x1e, 0xd8, 0x4c, 0x8d, 0x1d, 0xa9, 0x7e, 0x07, 0xc9, 0x9c, 0x93, 0xb8, 0xc1, 0xd4, 0x3e, 0x1c, + 0x71, 0xca, 0x85, 0xa3, 0xc8, 0x0e, 0x62, 0x79, 0xdc, 0x5f, 0xd1, 0x28, 0xfd, 0x39, 0xd9, 0x01, + 0x09, 0x58, 0x5d, 0x12, 0xdc, 0xd2, 0xc6, 0x44, 0x0d, 0x32, 0x67, 0x08, 0x49, 0x63, 0x88, 0xe0, + 0x32, 0x3e, 0xeb, 0xe6, 0x49, 0xef, 0xa1, 0xe5, 0xef, 0x0b, 0xdf, 0x2f, 0x9d, 0x14, 0x90, 0xb4, + 0xe4, 0x68, 0x8f, 0x51, 0x4a, 0xad, 0x7c, 0x32, 0x25, 0x2e, 0xb2, 0x38, 0xb8, 0x22, 0x07, 0x07, + 0x55, 0x81, 0xb7, 0xe5, 0x4a, 0x8b, 0x28, 0x8c, 0x4b, 0x67, 0xd6, 0x9b, 0x26, 0x84, 0x86, 0x87, + 0x30, 0xf0, 0x49, 0xd3, 0xf0, 0x0a, 0x5e, 0x91, 0x63, 0xeb, 0xcc, 0x7a, 0x42, 0x2b, 0x63, 0xac, + 0xf9, 0xf9, 0xe3, 0xbe, 0x7b, 0x94, 0xec, 0x5b, 0xe4, 0x60, 0x0f, 0xd6, 0x0e, 0x8f, 0xc0, 0x7d, + 0x34, 0x46, 0xb9, 0x1f, 0x42, 0x4e, 0xea, 0xab, 0x35, 0xb6, 0x8f, 0xbd, 0x40, 0xcb, 0xfe, 0x80, + 0x54, 0xe1, 0xd7, 0xf6, 0x4f, 0xcb, 0x90, 0x5d, 0x8d, 0x8a, 0x92, 0x12, 0x5c, 0x58, 0x25, 0x10, + 0x8a, 0xfd, 0x6b, 0x18, 0xd6, 0x5e, 0xf1, 0x1c, 0xd6, 0x3d, 0x62, 0x23, 0x29, 0x2d, 0x00, 0x6b, + 0x22, 0xac, 0x35, 0xb9, 0xd3, 0xe4, 0xb3, 0x26, 0xef, 0x34, 0x79, 0xaf, 0x19, 0x7a, 0xdb, 0xb4, + 0x5a, 0xd0, 0x5a, 0xb3, 0x9b, 0xa6, 0x3d, 0x72, 0x40, 0x22, 0x6f, 0x35, 0x59, 0x6b, 0x4c, 0xde, + 0x6a, 0xfa, 0x96, 0xad, 0x35, 0x79, 0xab, 0x1b, 0x0e, 0xdb, 0xb7, 0x10, 0x87, 0xee, 0xb6, 0x7b, + 0x41, 0x14, 0x7b, 0xe0, 0x55, 0x52, 0x43, 0xf1, 0x8d, 0x66, 0xbc, 0xba, 0xf4, 0x24, 0x9b, 0x0f, + 0xe9, 0xb8, 0xea, 0x63, 0x59, 0xbf, 0x6e, 0xfa, 0x5b, 0xc6, 0xd5, 0xfd, 0x07, 0x1e, 0x73, 0xf3, + 0xd0, 0x1f, 0xbc, 0xec, 0xbd, 0xf7, 0xb7, 0x34, 0x84, 0x09, 0xdc, 0x34, 0x7f, 0x9d, 0xc2, 0x0a, + 0xea, 0xc6, 0xab, 0x5f, 0x0f, 0x44, 0x13, 0x90, 0x6c, 0xb4, 0xa6, 0x02, 0xde, 0x59, 0x14, 0xf6, + 0xad, 0x27, 0xce, 0xcc, 0xdf, 0x90, 0x61, 0x2c, 0xb1, 0xef, 0xb6, 0x07, 0xc6, 0x70, 0x93, 0x79, + 0x2e, 0xa9, 0x24, 0x9a, 0x55, 0x81, 0xa8, 0xa0, 0xeb, 0x90, 0x0c, 0xf6, 0x63, 0xf8, 0xcb, 0x24, + 0xf0, 0x88, 0xb7, 0x64, 0x3a, 0x80, 0x28, 0x32, 0x89, 0x21, 0x49, 0x38, 0xfc, 0xa1, 0x92, 0x3b, + 0xd2, 0x78, 0xac, 0xfc, 0xf0, 0xaf, 0xc8, 0xc0, 0x5f, 0x33, 0x50, 0xe3, 0x89, 0x98, 0x06, 0xdc, + 0x76, 0xb5, 0xf7, 0x88, 0xaa, 0x6c, 0x3e, 0xa2, 0x4a, 0x32, 0x02, 0x2e, 0x0f, 0xf3, 0xb7, 0x4f, + 0xe0, 0xaf, 0xcc, 0x64, 0xa0, 0xc8, 0xa1, 0x15, 0x5a, 0x9f, 0x70, 0xc2, 0xee, 0x0e, 0x23, 0xf1, + 0x4a, 0x85, 0xd8, 0xdc, 0xe0, 0x83, 0xab, 0x03, 0x0a, 0x13, 0x2f, 0x4e, 0x42, 0x60, 0x25, 0x78, + 0x41, 0xb4, 0x11, 0x7c, 0x19, 0xce, 0xef, 0xcd, 0xa6, 0x33, 0xe6, 0xa8, 0xf1, 0xa8, 0xec, 0x51, + 0xd0, 0xa8, 0xf7, 0xcd, 0xfb, 0x77, 0xe6, 0x11, 0x63, 0xa9, 0x7f, 0x48, 0xc2, 0x39, 0xdc, 0x8f, + 0x07, 0xb4, 0x3b, 0x5c, 0xa3, 0xcf, 0x9a, 0xdd, 0xc1, 0xff, 0x79, 0xbe, 0x2b, 0x30, 0xdd, 0x72, + 0x95, 0x8a, 0x44, 0x32, 0xcf, 0xfc, 0x15, 0x37, 0x8f, 0x7c, 0xd6, 0xd4, 0xda, 0x2f, 0x3f, 0x57, + 0x49, 0x2b, 0xed, 0xd8, 0x7b, 0x4d, 0xd0, 0x3b, 0xcd, 0x3e, 0x6b, 0x9a, 0x89, 0x3c, 0x47, 0xe6, + 0xd7, 0xfc, 0x01, 0x00, 0xba, 0x62, 0x2b, 0xf2, 0x0e, 0x2a, 0x3b, 0x71, 0xf6, 0x35, 0xbc, 0x2d, + 0xb9, 0x04, 0x0a, 0xa6, 0x96, 0x71, 0xd9, 0xcc, 0x7c, 0xd8, 0xe3, 0x75, 0xb6, 0x31, 0x1f, 0xc6, + 0x3b, 0xc5, 0x6e, 0x4d, 0xfa, 0xeb, 0xe4, 0x8e, 0xdd, 0x03, 0xdd, 0x17, 0x78, 0xf4, 0x7f, 0x00, + 0x6f, 0x95, 0xe5, 0xa4, 0x5e, 0x6e, 0x00, 0x00 +}; + + +// Autogenerated from wled00/data/rangetouch.js, do not edit!! +const uint16_t rangetouchJs_length = 1826; +const uint8_t rangetouchJs[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xb5, 0x58, 0xdf, 0x6f, 0xdb, 0x38, + 0x12, 0x7e, 0x5f, 0x60, 0xff, 0x07, 0x59, 0x77, 0xa7, 0x25, 0x13, 0x86, 0x96, 0x03, 0xf4, 0x45, + 0x2e, 0x6b, 0xf4, 0xba, 0x5d, 0x60, 0x71, 0xcd, 0xf5, 0xd0, 0x64, 0x71, 0x07, 0x78, 0xfd, 0x40, + 0x4b, 0x63, 0x9b, 0x1b, 0x99, 0xd4, 0x91, 0xa3, 0xa4, 0x86, 0xad, 0xff, 0xfd, 0x40, 0xca, 0xb2, + 0xe5, 0xc6, 0x6e, 0xfb, 0xb0, 0x17, 0x04, 0x96, 0x45, 0x7e, 0x9c, 0x9f, 0xdf, 0xcc, 0x10, 0x1e, + 0x0e, 0x23, 0xf1, 0xa7, 0xfd, 0xfd, 0xf8, 0xc3, 0x70, 0x18, 0x59, 0xa9, 0x97, 0x80, 0xa6, 0xce, + 0x57, 0xfc, 0x0f, 0x17, 0x3d, 0xdd, 0xf2, 0x94, 0x8f, 0xc2, 0xc6, 0x9d, 0x7c, 0x54, 0x7a, 0x19, + 0xbd, 0x56, 0xba, 0xaa, 0x31, 0xc2, 0x4d, 0x05, 0x22, 0x0e, 0xe0, 0xf8, 0x4d, 0xf4, 0x6c, 0xec, + 0x63, 0x64, 0x74, 0x14, 0xce, 0x45, 0x05, 0x3c, 0xa9, 0x1c, 0x5c, 0x38, 0xb5, 0x42, 0xac, 0x5c, + 0x36, 0x1c, 0x2e, 0x15, 0xae, 0xea, 0x39, 0xcf, 0xcd, 0x7a, 0xe8, 0xe4, 0xba, 0x32, 0x88, 0x6e, + 0x78, 0x54, 0x15, 0xa0, 0x1f, 0x54, 0x0e, 0xda, 0x41, 0x16, 0x3d, 0xac, 0x20, 0xba, 0xfb, 0xf5, + 0xa1, 0x5b, 0x88, 0xc8, 0xdd, 0xaf, 0x0f, 0x34, 0x40, 0xfe, 0x4c, 0x5f, 0x07, 0x8b, 0x5a, 0xe7, + 0xa8, 0x8c, 0x26, 0xc0, 0x90, 0x6e, 0x63, 0x33, 0xff, 0x03, 0x72, 0x8c, 0x85, 0xf0, 0xae, 0x99, + 0x45, 0x04, 0x9f, 0x2b, 0x63, 0xd1, 0x25, 0x49, 0x5c, 0xeb, 0x02, 0x16, 0x4a, 0x43, 0x11, 0x0f, + 0xba, 0xcd, 0xb5, 0x29, 0xea, 0x12, 0x26, 0xed, 0x83, 0xef, 0xa1, 0x02, 0x09, 0xcd, 0xe2, 0x4e, + 0xec, 0x51, 0x52, 0x7b, 0x3a, 0x49, 0xda, 0x27, 0x97, 0xeb, 0x62, 0xd2, 0x7e, 0x25, 0xf1, 0x27, + 0x1f, 0x82, 0x07, 0x1f, 0x82, 0x98, 0x21, 0xcd, 0x08, 0x08, 0xd8, 0xed, 0x1c, 0x94, 0x0b, 0xca, + 0x8f, 0x5b, 0x5e, 0x6e, 0x43, 0x70, 0xa5, 0x1c, 0x23, 0x07, 0xa3, 0xe9, 0x36, 0xae, 0x1d, 0x44, + 0x0e, 0xad, 0xca, 0x31, 0x1e, 0x77, 0xeb, 0x11, 0xb4, 0xee, 0x2c, 0x8c, 0x25, 0x4f, 0xd2, 0x46, + 0x5a, 0xa4, 0x63, 0xfd, 0x1a, 0x79, 0x09, 0x7a, 0x89, 0xab, 0xb1, 0xbe, 0xbe, 0xa6, 0x5b, 0xbf, + 0x6e, 0x05, 0x4e, 0xf5, 0x6c, 0x6c, 0x39, 0xe8, 0x7a, 0x0d, 0x56, 0xce, 0x4b, 0x10, 0xfd, 0x97, + 0xdd, 0x6e, 0x30, 0x62, 0x96, 0xe7, 0x46, 0x2f, 0xd4, 0xb2, 0x6e, 0xf7, 0x07, 0x29, 0x8b, 0x9f, + 0x64, 0x59, 0x43, 0xac, 0x74, 0x64, 0x93, 0x84, 0x58, 0xfe, 0x6c, 0x15, 0xee, 0xf7, 0x28, 0xfb, + 0x18, 0x22, 0xc8, 0x5b, 0xdf, 0xfe, 0x65, 0x4d, 0x05, 0x16, 0x37, 0x04, 0x98, 0xe5, 0x8f, 0xb0, + 0x61, 0x96, 0x36, 0xcd, 0xc1, 0x4a, 0xf4, 0x56, 0x32, 0x4d, 0xb7, 0x16, 0xb0, 0xb6, 0x3a, 0xc2, + 0x48, 0xe9, 0x08, 0x26, 0x97, 0x24, 0x20, 0xdb, 0x06, 0xc5, 0x99, 0x66, 0x47, 0x13, 0xb3, 0x41, + 0xca, 0xfa, 0xf6, 0xf9, 0xf7, 0xce, 0x9e, 0x6c, 0x90, 0x36, 0x34, 0x83, 0x29, 0xce, 0x84, 0x66, + 0x70, 0xd4, 0xbb, 0x4f, 0x76, 0x1b, 0x99, 0xbd, 0xb6, 0x47, 0xd8, 0x38, 0x02, 0x74, 0xac, 0x16, + 0x64, 0xbf, 0xb2, 0x04, 0xfc, 0xf8, 0xac, 0x3b, 0xfd, 0xf7, 0x9b, 0xf5, 0xdc, 0x94, 0xae, 0x8b, + 0xdb, 0xd7, 0x30, 0x5e, 0x0c, 0xfa, 0xc0, 0x08, 0xcb, 0x17, 0xaa, 0x44, 0xb0, 0xe4, 0x98, 0x31, + 0x3c, 0x78, 0x7b, 0x56, 0xc4, 0xcf, 0xe0, 0x72, 0xab, 0x2a, 0x34, 0x36, 0xd8, 0xd8, 0xcb, 0x45, + 0x43, 0x29, 0x65, 0x9a, 0x57, 0xb5, 0x5b, 0x71, 0x59, 0x55, 0xe5, 0x86, 0x68, 0x1f, 0xcd, 0xbd, + 0x30, 0x7d, 0xf4, 0xce, 0x12, 0x38, 0x66, 0xde, 0x8a, 0xd1, 0xd8, 0xbe, 0x96, 0x76, 0x59, 0xaf, + 0x41, 0xa3, 0xeb, 0x18, 0x60, 0x3b, 0x06, 0x28, 0xa1, 0xeb, 0xb2, 0x1c, 0x88, 0x03, 0x62, 0x6a, + 0x67, 0x93, 0xfe, 0x4b, 0xb6, 0x6d, 0xc6, 0xf6, 0x6f, 0xb7, 0x13, 0xbd, 0x0f, 0x0b, 0x51, 0x94, + 0x0d, 0x52, 0xca, 0x17, 0xc6, 0xbe, 0x97, 0xf9, 0xaa, 0xe7, 0x99, 0xa6, 0x5b, 0x9f, 0x50, 0xcd, + 0xd4, 0x54, 0xcf, 0x68, 0x43, 0x69, 0xf6, 0x0d, 0x0f, 0xdd, 0xd9, 0x4c, 0x2b, 0x70, 0x04, 0xd8, + 0xb7, 0x8e, 0x12, 0x45, 0x69, 0xd6, 0xb3, 0xe9, 0x9c, 0x41, 0x48, 0xb7, 0x97, 0xa9, 0xf4, 0xad, + 0xe8, 0x2b, 0x86, 0xd4, 0x3b, 0xd1, 0x05, 0x18, 0x9a, 0x36, 0x5c, 0x5b, 0x59, 0x14, 0xef, 0xee, + 0xef, 0x3d, 0xcd, 0x70, 0x55, 0xaf, 0xe7, 0xff, 0x56, 0x05, 0xae, 0xb2, 0xd1, 0x2b, 0xf6, 0x2c, + 0x31, 0x5f, 0x79, 0xc6, 0x1d, 0xab, 0xb0, 0x6e, 0x79, 0xb6, 0x97, 0xd0, 0x2b, 0xda, 0xfd, 0xca, + 0x5b, 0x6b, 0xe5, 0x86, 0x2f, 0xac, 0x59, 0x93, 0xc2, 0xe4, 0x21, 0xe4, 0xfc, 0xbf, 0x35, 0xd8, + 0xcd, 0x3d, 0x94, 0x90, 0xa3, 0xb1, 0x6f, 0xcb, 0x92, 0x20, 0xa5, 0x5c, 0xe9, 0xbc, 0xac, 0x0b, + 0x70, 0xa1, 0xfc, 0x69, 0xc3, 0x73, 0x59, 0x96, 0x41, 0x76, 0x30, 0xca, 0x88, 0x63, 0x13, 0x3b, + 0xc8, 0x6e, 0xd3, 0x0a, 0x13, 0xf0, 0xd5, 0xeb, 0xd0, 0xd6, 0x5e, 0x5e, 0xe6, 0x57, 0x1b, 0x96, + 0x8b, 0xd3, 0xae, 0xd7, 0x1e, 0x19, 0x0c, 0x08, 0x24, 0x09, 0x26, 0x09, 0x44, 0x4a, 0x3b, 0x94, + 0x3a, 0xf7, 0x6d, 0x0b, 0x69, 0xc3, 0xca, 0x4b, 0x0a, 0x84, 0x80, 0x86, 0xc9, 0x73, 0xbb, 0x86, + 0x00, 0x15, 0x62, 0x5f, 0x26, 0x0d, 0x73, 0x5f, 0xc1, 0xdc, 0xa3, 0x55, 0x7a, 0xd9, 0xb0, 0xc5, + 0x39, 0x4c, 0x1b, 0x22, 0xe5, 0xc2, 0x93, 0x00, 0x6d, 0xd8, 0xea, 0x1c, 0x2c, 0x27, 0xc0, 0xfe, + 0x69, 0x0a, 0xf8, 0xa0, 0x9c, 0x37, 0xb8, 0x10, 0x8e, 0x6d, 0xc4, 0x82, 0xcd, 0xc5, 0x8a, 0xad, + 0x2f, 0xe1, 0xdf, 0x97, 0xe0, 0x23, 0x4e, 0x1b, 0xb6, 0xbc, 0x08, 0x79, 0x6a, 0x01, 0xd5, 0x39, + 0x40, 0x49, 0x80, 0xee, 0x76, 0xc4, 0x85, 0xc7, 0x22, 0x7c, 0xae, 0x08, 0x50, 0x9a, 0x24, 0x03, + 0xd8, 0xd7, 0xd9, 0x6e, 0x27, 0x09, 0xf8, 0x85, 0xd3, 0x26, 0xb3, 0xdf, 0xed, 0x31, 0xe5, 0xa9, + 0x4d, 0x84, 0x5a, 0x90, 0xd1, 0x9b, 0x43, 0x67, 0xea, 0xeb, 0xf4, 0x2b, 0x28, 0xe2, 0xd8, 0x67, + 0x33, 0x97, 0xe8, 0x85, 0xac, 0x3d, 0xe1, 0xc8, 0x90, 0x4c, 0xb2, 0xdf, 0x39, 0xf9, 0xbd, 0xb8, + 0xa6, 0x74, 0x42, 0x26, 0xd9, 0x14, 0xde, 0xcf, 0xc8, 0xf4, 0xfa, 0x66, 0x36, 0x69, 0x97, 0xfe, + 0x3a, 0xa4, 0xe3, 0xae, 0xbd, 0x4e, 0xee, 0x24, 0xae, 0xf8, 0x5a, 0x7e, 0x26, 0x29, 0x23, 0x38, + 0x1d, 0xcd, 0x26, 0xfe, 0x63, 0x6f, 0x4d, 0x96, 0xd2, 0x1b, 0x82, 0xd3, 0xdb, 0xd9, 0xe4, 0xda, + 0x7f, 0x66, 0x29, 0xa5, 0x59, 0xda, 0x10, 0x3c, 0x1c, 0xaf, 0xa4, 0x75, 0xf0, 0x4b, 0x69, 0xbc, + 0x76, 0x8e, 0xe6, 0x17, 0xf5, 0x19, 0x0a, 0xa2, 0x8f, 0x15, 0x12, 0x84, 0x5b, 0x53, 0xeb, 0x82, + 0xc0, 0x10, 0xe9, 0x15, 0x36, 0x2f, 0x89, 0x7f, 0xd2, 0xfa, 0x35, 0xdd, 0x92, 0x53, 0x22, 0xaa, + 0x05, 0x19, 0x90, 0x2f, 0xe8, 0x47, 0x71, 0x65, 0xcd, 0x73, 0xa4, 0xe1, 0x39, 0x7a, 0xd8, 0x54, + 0xf0, 0xde, 0x5a, 0x63, 0x49, 0xfc, 0x4e, 0x6a, 0x6d, 0x30, 0xf2, 0x85, 0x10, 0xc9, 0x28, 0x2f, + 0xa5, 0x73, 0x91, 0x74, 0x91, 0x3c, 0x28, 0x8b, 0x69, 0x43, 0xdb, 0x69, 0x89, 0x94, 0xad, 0x09, + 0xd0, 0x89, 0x7f, 0xe1, 0xd0, 0x26, 0x5d, 0x40, 0x56, 0x84, 0xcc, 0x90, 0x93, 0xd5, 0xf3, 0x55, + 0xe8, 0x93, 0xca, 0xd6, 0x27, 0x48, 0x9a, 0x24, 0xd5, 0xc9, 0x02, 0xb7, 0x87, 0x41, 0x7d, 0x90, + 0xda, 0x8e, 0x25, 0x61, 0xc9, 0xb6, 0x61, 0x8a, 0x6d, 0x1b, 0xa6, 0x29, 0x0b, 0x1b, 0x4a, 0x2b, + 0x24, 0xc7, 0xc0, 0x69, 0x81, 0x2c, 0x17, 0xd3, 0xed, 0x23, 0x6c, 0xb2, 0xd8, 0x01, 0xd6, 0x55, + 0xcc, 0xda, 0x41, 0xf7, 0x25, 0x03, 0xb4, 0x18, 0xbd, 0xe8, 0xe5, 0x49, 0xf2, 0x64, 0x54, 0x11, + 0xa5, 0x03, 0xd1, 0x6b, 0xe2, 0xa3, 0x7e, 0x13, 0x1f, 0xf9, 0x26, 0xce, 0x4c, 0xe8, 0xf4, 0x7e, + 0xbe, 0x55, 0x81, 0xab, 0xde, 0xff, 0x89, 0x11, 0xdf, 0xd5, 0x83, 0x02, 0x16, 0xb2, 0x9f, 0xc2, + 0xad, 0x6f, 0xda, 0xbf, 0xf5, 0xcd, 0x7e, 0xa2, 0x34, 0x5b, 0xb7, 0xa2, 0xa6, 0x30, 0xcb, 0xe6, + 0x2f, 0xa4, 0x02, 0xcd, 0x36, 0x6d, 0xa8, 0x8d, 0x80, 0x6e, 0x1e, 0xae, 0x29, 0x65, 0x15, 0x31, + 0x94, 0xf6, 0x9a, 0xc9, 0xd8, 0x7b, 0x98, 0x9f, 0x84, 0xcb, 0x5b, 0xdb, 0xe6, 0x29, 0xe7, 0xa1, + 0xbf, 0xb6, 0x61, 0x28, 0x85, 0x67, 0xc3, 0x5d, 0x8d, 0xd2, 0x07, 0xe7, 0xe3, 0xdc, 0x81, 0x7d, + 0x3a, 0x19, 0xb2, 0x9a, 0x6e, 0x7b, 0x16, 0xe8, 0x0b, 0xd3, 0xaa, 0x0f, 0xe1, 0xb2, 0x28, 0xa0, + 0xf0, 0x2d, 0xc4, 0x5d, 0x40, 0x7b, 0x39, 0x49, 0x52, 0x13, 0xcd, 0xbc, 0x3d, 0xde, 0x00, 0x24, + 0x9a, 0xe5, 0x61, 0x46, 0x84, 0xff, 0x71, 0xc9, 0x4d, 0x6b, 0xca, 0x31, 0x92, 0x73, 0x53, 0x6c, + 0xd8, 0x36, 0x5f, 0xa9, 0xb2, 0xf0, 0xad, 0xc9, 0x4f, 0x0d, 0x57, 0xcf, 0xd1, 0x42, 0x7b, 0x37, + 0xe9, 0x18, 0x60, 0xf8, 0x5a, 0x56, 0x3d, 0x75, 0xbd, 0x2e, 0x1b, 0xf4, 0xf8, 0x5a, 0xf1, 0x4a, + 0x1a, 0xd6, 0x92, 0x04, 0xb4, 0xbf, 0x11, 0x14, 0x31, 0x5b, 0x02, 0x66, 0x2f, 0xe6, 0x4a, 0x6c, + 0x74, 0xb8, 0x4f, 0x3b, 0x94, 0x16, 0xfd, 0x2d, 0xed, 0x60, 0x4e, 0xf7, 0x65, 0xdf, 0xfa, 0x9a, + 0x66, 0xc6, 0x88, 0xe9, 0x98, 0xe7, 0x59, 0xf9, 0x82, 0x78, 0x74, 0x8b, 0x7c, 0xaf, 0xed, 0x94, + 0xd6, 0xbc, 0x9d, 0x83, 0x5f, 0x54, 0x10, 0x77, 0xb8, 0x29, 0x81, 0xd7, 0x0e, 0x6c, 0xcb, 0x1f, + 0x11, 0x6b, 0xa3, 0x21, 0x66, 0x67, 0x40, 0xcf, 0x30, 0xff, 0x87, 0xc2, 0xdf, 0xbe, 0x07, 0x1a, + 0xdc, 0x79, 0x1b, 0x2c, 0x12, 0xf1, 0x5a, 0x6a, 0x55, 0xd5, 0xa5, 0x6c, 0xab, 0xbc, 0x85, 0x97, + 0xca, 0x21, 0x68, 0xb0, 0x8e, 0xf8, 0x2b, 0xe8, 0x85, 0xc2, 0x14, 0xed, 0xf4, 0xec, 0x82, 0x58, + 0x80, 0x43, 0x6b, 0x36, 0xff, 0x07, 0x97, 0xbf, 0xd3, 0xdd, 0x6f, 0xbb, 0x7a, 0xc6, 0xbd, 0xd1, + 0x65, 0xf7, 0x7c, 0x11, 0x1d, 0xdd, 0x3b, 0x9c, 0xb9, 0xd4, 0x4c, 0x30, 0xc4, 0x83, 0x69, 0x01, + 0x93, 0x58, 0x16, 0x45, 0x98, 0x75, 0x1f, 0xf6, 0x87, 0xe2, 0x2c, 0xb6, 0xb0, 0x36, 0x4f, 0x70, + 0xba, 0x3a, 0x9e, 0xc6, 0x3d, 0x66, 0xb1, 0xf6, 0xc5, 0xc3, 0xba, 0xef, 0xa0, 0x8b, 0x78, 0x76, + 0xa6, 0x7e, 0x20, 0x04, 0xb5, 0x35, 0x79, 0xaa, 0x67, 0x04, 0xd8, 0x39, 0xb2, 0x23, 0x77, 0xe0, + 0x47, 0x5b, 0x43, 0xd9, 0x60, 0x74, 0x42, 0xf8, 0x25, 0xe0, 0x39, 0x37, 0xfc, 0xbc, 0x38, 0x24, + 0x6b, 0xb7, 0x1b, 0x2c, 0x7d, 0xab, 0xfe, 0xb2, 0xa5, 0x68, 0x66, 0x05, 0x70, 0x94, 0x76, 0x09, + 0xc8, 0x94, 0x00, 0x9e, 0xaf, 0x7c, 0xcc, 0x8a, 0x10, 0x34, 0x70, 0xd3, 0x74, 0xc6, 0x6a, 0xd1, + 0x1b, 0x6e, 0xd6, 0x5f, 0x0a, 0xdf, 0x22, 0x5a, 0x35, 0xaf, 0x11, 0x48, 0xbc, 0x56, 0x3a, 0xa6, + 0x74, 0xb7, 0x4b, 0x99, 0xf9, 0x2a, 0x4c, 0x7e, 0x0e, 0xb0, 0x51, 0x9a, 0xb2, 0xfc, 0x6b, 0x40, + 0x87, 0x50, 0xb5, 0x48, 0x56, 0x8a, 0xb0, 0xf9, 0x77, 0x3f, 0x34, 0x95, 0x5e, 0xbe, 0x2b, 0x15, + 0x68, 0xfc, 0xe4, 0x6f, 0xb4, 0x94, 0x49, 0x31, 0x4a, 0xd3, 0x61, 0xc9, 0x9f, 0xfd, 0x05, 0xf3, + 0xea, 0x84, 0x87, 0xc7, 0x8b, 0xe7, 0xf0, 0x96, 0x0e, 0x47, 0x69, 0xda, 0x8d, 0xe8, 0xf4, 0x0d, + 0xd1, 0xa7, 0xc7, 0x14, 0xcf, 0x83, 0xd0, 0xff, 0xdc, 0x94, 0xbc, 0x84, 0x05, 0x52, 0x3a, 0xd1, + 0x22, 0xcd, 0x46, 0x69, 0xfa, 0x5a, 0x27, 0x49, 0x8b, 0xa6, 0xec, 0x55, 0xfa, 0x46, 0x4f, 0xf4, + 0x8d, 0x20, 0xa3, 0x34, 0xbd, 0xb9, 0xbd, 0xd2, 0xf4, 0x4a, 0x66, 0xaf, 0xf6, 0x88, 0x6b, 0x71, + 0x7b, 0x45, 0xf4, 0xcd, 0xab, 0x94, 0x5e, 0x49, 0xca, 0xea, 0xeb, 0x27, 0xa2, 0xbd, 0xca, 0x2b, + 0x62, 0x6e, 0x6a, 0xea, 0xfb, 0x5f, 0x97, 0x23, 0x77, 0x3e, 0x47, 0xbd, 0x62, 0x5a, 0xb6, 0x97, + 0xa1, 0x2e, 0x19, 0xbc, 0x50, 0xae, 0x2b, 0x33, 0xe0, 0x95, 0x05, 0xcf, 0xb5, 0x9f, 0x61, 0x21, + 0xeb, 0xd2, 0x07, 0xe0, 0x00, 0x0b, 0x32, 0x03, 0x59, 0x7d, 0xb0, 0x08, 0x50, 0xf6, 0xe2, 0xd6, + 0xe0, 0x2f, 0xae, 0xdd, 0x88, 0xf4, 0x2d, 0x33, 0xd0, 0x96, 0x20, 0xdb, 0xce, 0xeb, 0xf9, 0xbc, + 0x04, 0x17, 0xba, 0xed, 0x18, 0xbc, 0xc6, 0xca, 0xcf, 0x91, 0x76, 0x5f, 0xd3, 0xa6, 0x21, 0x07, + 0x6a, 0x1c, 0x19, 0x2c, 0x84, 0x27, 0xcc, 0xa6, 0x82, 0x49, 0xdc, 0x72, 0x25, 0xf6, 0xdd, 0xb1, + 0xaa, 0x31, 0xf6, 0x94, 0x9c, 0xd1, 0x24, 0x01, 0xa2, 0x79, 0x65, 0x0d, 0x1a, 0x8f, 0x62, 0x86, + 0xb2, 0x3c, 0xac, 0xb1, 0x9c, 0x32, 0xdc, 0x93, 0xce, 0xb0, 0xbc, 0x21, 0x61, 0x2e, 0xf8, 0x1f, + 0x2d, 0xfe, 0x12, 0x39, 0x53, 0xdb, 0x1c, 0xee, 0x64, 0x55, 0x29, 0xbd, 0xfc, 0xed, 0xd3, 0x07, + 0x71, 0xf2, 0x1b, 0x8b, 0x1f, 0x00, 0x3f, 0xfe, 0xf0, 0x3f, 0x24, 0x8e, 0x90, 0x87, 0xc9, 0x11, + 0x00, 0x00 +}; + diff --git a/wled00/html_pixart.h b/wled00/html_pixart.h new file mode 100644 index 0000000000..790d8ea405 --- /dev/null +++ b/wled00/html_pixart.h @@ -0,0 +1,535 @@ +/* + * Binary array for the Web UI. + * gzip is used for smaller size and improved speeds. + * + * Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui + * to find out how to easily modify the web UI source! + */ + +// Autogenerated from wled00/data/pixart/pixart.htm, do not edit!! +const uint16_t PAGE_pixart_L = 8364; +const uint8_t PAGE_pixart[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x13, 0xd5, 0x3c, 0x6b, 0x7b, 0xda, 0x38, + 0xb3, 0xdf, 0xf3, 0x2b, 0x54, 0x77, 0xb7, 0xc5, 0x8b, 0x31, 0xb6, 0xb9, 0x06, 0xe2, 0xf4, 0x21, + 0xe4, 0x42, 0xb6, 0xb9, 0x93, 0xa4, 0x4d, 0xf3, 0xe6, 0xe9, 0x1a, 0x2c, 0xb0, 0x13, 0x63, 0x53, + 0xdb, 0x40, 0x08, 0xe5, 0xbf, 0x9f, 0x19, 0xc9, 0x06, 0x73, 0x49, 0x93, 0xf6, 0x74, 0xdf, 0xf3, + 0x9c, 0xed, 0x06, 0xdb, 0xd2, 0x48, 0x1a, 0xcd, 0x8c, 0xe6, 0x26, 0xd9, 0x5b, 0x6f, 0x76, 0x4f, + 0xeb, 0x97, 0x37, 0x67, 0x7b, 0xc4, 0x0a, 0x7b, 0xce, 0x36, 0xd9, 0x8a, 0x2f, 0xd4, 0x30, 0xe1, + 0xd2, 0xa3, 0xa1, 0x01, 0x35, 0x61, 0x3f, 0x43, 0xbf, 0x0d, 0xec, 0xa1, 0x2e, 0xd4, 0x8d, 0xb6, + 0x45, 0x33, 0x75, 0xcf, 0x0d, 0x7d, 0xcf, 0x11, 0xc8, 0x46, 0x1b, 0xee, 0xa8, 0x1b, 0xea, 0x82, + 0xeb, 0x65, 0xda, 0x58, 0x27, 0x11, 0xb8, 0x0b, 0x42, 0xcf, 0x87, 0xbb, 0xde, 0x20, 0x08, 0x33, + 0x3e, 0x1d, 0x1a, 0x8e, 0x6d, 0x1a, 0x21, 0x15, 0xd6, 0x75, 0x78, 0xe6, 0x1b, 0xdd, 0x9e, 0xb1, + 0xae, 0xa7, 0xb5, 0xe0, 0x7b, 0x8f, 0x7d, 0xdb, 0xa7, 0x81, 0x40, 0x66, 0xe0, 0x0a, 0xc2, 0x85, + 0x76, 0xe8, 0xd0, 0xed, 0x8d, 0x4f, 0x47, 0x7b, 0xbb, 0xe4, 0xcc, 0x7e, 0xa4, 0x0e, 0xa9, 0xf9, + 0x21, 0x01, 0x34, 0x87, 0xd4, 0x0f, 0xa9, 0xbf, 0x95, 0xe5, 0x00, 0x64, 0x2b, 0x08, 0xc7, 0x08, + 0x28, 0xb7, 0xbc, 0xc7, 0x49, 0xcb, 0xf3, 0x4d, 0xea, 0x57, 0xb4, 0xfe, 0x23, 0x09, 0x3c, 0x40, + 0x91, 0xbc, 0xed, 0x74, 0x3a, 0xd3, 0x96, 0x67, 0x8e, 0x27, 0x1d, 0xe8, 0x3d, 0xd3, 0x31, 0x7a, + 0xb6, 0x33, 0xae, 0xd4, 0x7c, 0xdb, 0x70, 0xa4, 0xc0, 0x70, 0x83, 0x4c, 0x40, 0x7d, 0xbb, 0x53, + 0x6d, 0x19, 0xed, 0x87, 0xae, 0xef, 0x0d, 0x5c, 0x33, 0xd3, 0xf6, 0x1c, 0xcf, 0xaf, 0xbc, 0x55, + 0x55, 0x75, 0x2a, 0x87, 0x5e, 0x3f, 0xd3, 0x37, 0xfc, 0x70, 0x32, 0xb2, 0xcd, 0xd0, 0xaa, 0x14, + 0x15, 0xa5, 0xff, 0x58, 0xed, 0x19, 0x7e, 0xd7, 0x76, 0x2b, 0x0a, 0x31, 0x06, 0xa1, 0x37, 0x95, + 0x11, 0x6b, 0xc3, 0x76, 0xa9, 0x3f, 0xe9, 0x19, 0x8f, 0x19, 0x0e, 0xa8, 0x2a, 0xca, 0x9f, 0x24, + 0x93, 0x47, 0x68, 0x8e, 0x51, 0xc6, 0x37, 0x4c, 0x7b, 0x10, 0x54, 0x94, 0x6a, 0xdf, 0x30, 0x4d, + 0xdb, 0xed, 0x56, 0x34, 0xac, 0x0c, 0xe9, 0x63, 0x98, 0x01, 0x52, 0x76, 0xdd, 0x4a, 0x1b, 0x66, + 0x4e, 0xfd, 0xa9, 0xa5, 0x72, 0x44, 0x03, 0xfb, 0x89, 0x56, 0x34, 0x39, 0x47, 0x7b, 0xd5, 0x08, + 0x21, 0xd3, 0x34, 0xe3, 0xa1, 0x55, 0x98, 0x9e, 0x52, 0xfd, 0xe1, 0x84, 0x1c, 0xc0, 0x28, 0x63, + 0x51, 0xbb, 0x6b, 0x85, 0x15, 0xb9, 0x30, 0xb5, 0xb4, 0x44, 0xb7, 0xaa, 0xac, 0xce, 0xba, 0xf5, + 0xbb, 0x2d, 0x23, 0xa5, 0x69, 0xaa, 0x14, 0xff, 0xc9, 0x45, 0x55, 0xfc, 0xe5, 0x71, 0xd6, 0x4d, + 0x28, 0x97, 0x18, 0x59, 0x2e, 0xfd, 0x2b, 0x03, 0xab, 0x72, 0x7e, 0x75, 0xe4, 0x2a, 0x7b, 0xc8, + 0xd8, 0x21, 0xed, 0x05, 0x71, 0xd1, 0x3d, 0x48, 0xaf, 0xdd, 0x19, 0x67, 0x22, 0x49, 0x8b, 0x8b, + 0x4d, 0x3b, 0xe8, 0x3b, 0xc6, 0xb8, 0xd2, 0x71, 0xe8, 0xe3, 0xb4, 0x9f, 0xa4, 0xd4, 0x9c, 0xfc, + 0xa5, 0x52, 0x69, 0x69, 0xcc, 0xc2, 0x0f, 0x11, 0x9c, 0xbe, 0xed, 0xd8, 0xd4, 0x31, 0x2f, 0x8d, + 0x96, 0x43, 0x93, 0x3d, 0x92, 0x1f, 0x74, 0xf9, 0x42, 0x87, 0x41, 0xdb, 0x70, 0xe8, 0xef, 0xec, + 0xd0, 0xf4, 0x41, 0xbe, 0x9f, 0x3c, 0x97, 0x4e, 0x62, 0x0a, 0xb4, 0x1c, 0xaf, 0xfd, 0x50, 0x9d, + 0x0b, 0x71, 0x52, 0x86, 0x2b, 0x39, 0xe0, 0x8a, 0x69, 0x04, 0x16, 0x85, 0x65, 0x85, 0xd2, 0xb8, + 0x2c, 0xda, 0xab, 0x2c, 0x58, 0x10, 0xf6, 0x78, 0xdd, 0x54, 0xdb, 0x03, 0x3f, 0x00, 0x74, 0xfb, + 0x9e, 0xcd, 0x80, 0x7e, 0xc8, 0xe6, 0xc4, 0x44, 0x0b, 0xd0, 0xc7, 0x7c, 0xa2, 0x48, 0x5f, 0x87, + 0x66, 0xfa, 0x76, 0xfb, 0x01, 0x96, 0x5e, 0x8c, 0xbf, 0x0b, 0x93, 0x99, 0xca, 0x86, 0x69, 0xf4, + 0x43, 0x7b, 0x48, 0x2f, 0x77, 0x27, 0x49, 0xd6, 0x56, 0xf1, 0x27, 0x63, 0x82, 0xb2, 0x69, 0x87, + 0xb6, 0xe7, 0x56, 0x7c, 0x6f, 0xc4, 0x8b, 0x46, 0xbe, 0xd1, 0x87, 0xa6, 0x78, 0x59, 0x23, 0x35, + 0x53, 0xb9, 0x07, 0xeb, 0xbb, 0x49, 0x1d, 0x68, 0xe6, 0xf9, 0x93, 0x55, 0x55, 0xa1, 0x69, 0x5a, + 0x72, 0x91, 0x46, 0xd4, 0x52, 0xe7, 0x3a, 0x28, 0x97, 0xcb, 0x45, 0xb3, 0xcf, 0x80, 0x46, 0xa9, + 0xe4, 0x67, 0xc4, 0xc8, 0xb4, 0xbc, 0x30, 0xf4, 0x7a, 0xac, 0x24, 0xa6, 0x95, 0x42, 0xca, 0xf0, + 0x14, 0x71, 0x50, 0xc3, 0xfb, 0x25, 0x1a, 0x2c, 0x92, 0xbd, 0xd4, 0x8f, 0xe6, 0x05, 0x38, 0x8d, + 0x80, 0xe3, 0x0b, 0x13, 0x7e, 0xf5, 0x12, 0x98, 0xd3, 0xec, 0xff, 0xdb, 0x3c, 0x39, 0xc7, 0x03, + 0xda, 0xed, 0xc1, 0x44, 0x82, 0x5d, 0x7b, 0x18, 0xe9, 0xea, 0x5c, 0x71, 0x3e, 0x56, 0xc6, 0xa1, + 0x9d, 0xb0, 0x02, 0x7d, 0x4e, 0xff, 0x22, 0xb6, 0xdb, 0x1f, 0x84, 0xb7, 0xe1, 0xb8, 0x4f, 0x75, + 0xdf, 0x70, 0xbb, 0xf4, 0x6e, 0x62, 0xf4, 0xfb, 0xd4, 0x80, 0xfb, 0x36, 0x65, 0x9d, 0x55, 0x33, + 0x3d, 0xef, 0x29, 0xb3, 0x52, 0x38, 0xa2, 0xad, 0x07, 0x3b, 0x5c, 0x29, 0x4f, 0xa2, 0x34, 0x9b, + 0x59, 0x2c, 0xea, 0x30, 0x5d, 0x9c, 0x24, 0xc9, 0x33, 0x75, 0xb6, 0x42, 0xd0, 0x10, 0xfa, 0x09, + 0xc0, 0xbc, 0x00, 0xe2, 0xcb, 0x6b, 0x62, 0x0e, 0x5b, 0xc1, 0x05, 0x6d, 0xf8, 0x30, 0x06, 0xd0, + 0x01, 0x20, 0x53, 0xa1, 0x47, 0x7c, 0xa4, 0x99, 0xf4, 0xb6, 0xd5, 0x6a, 0x91, 0x82, 0xf2, 0xa7, + 0x84, 0x74, 0xc7, 0x1b, 0x71, 0x95, 0x64, 0xd3, 0x95, 0xe9, 0x56, 0x3a, 0x5e, 0x7b, 0x10, 0x4c, + 0xbc, 0x41, 0x88, 0x1d, 0x57, 0x94, 0x35, 0x10, 0x95, 0x78, 0xb2, 0x01, 0x70, 0x15, 0xbb, 0x1b, + 0xb8, 0x2e, 0x6a, 0x9d, 0x0c, 0x20, 0xdc, 0x7e, 0x98, 0x24, 0x59, 0xf6, 0x3c, 0xda, 0x0a, 0xce, + 0xf8, 0x15, 0xe8, 0x2c, 0x0f, 0x16, 0x5a, 0x83, 0x5e, 0x2b, 0x1e, 0x43, 0x45, 0x26, 0x46, 0xda, + 0xa8, 0xb8, 0x22, 0x12, 0x30, 0xe5, 0xe4, 0x88, 0x68, 0xea, 0x97, 0x11, 0x7a, 0x8e, 0x6d, 0x4b, + 0x52, 0xfa, 0x3a, 0x44, 0x51, 0x2e, 0xd8, 0xc3, 0x1a, 0x42, 0xac, 0x30, 0x97, 0xd9, 0x37, 0x45, + 0x62, 0xff, 0xc4, 0x9f, 0x1f, 0x80, 0x51, 0x21, 0x5a, 0x61, 0x4a, 0xb4, 0xbe, 0x92, 0xf2, 0xf2, + 0x2a, 0x02, 0x95, 0x16, 0x10, 0x63, 0x04, 0x9a, 0xca, 0x6c, 0x80, 0x13, 0xe8, 0x1e, 0xd4, 0x26, + 0x6f, 0xc9, 0xb4, 0x33, 0x7a, 0x55, 0x36, 0x18, 0x98, 0x48, 0x81, 0xf7, 0x6c, 0xd3, 0x74, 0x60, + 0x61, 0x75, 0x06, 0x8e, 0x73, 0x09, 0x7a, 0x7d, 0x1f, 0x8d, 0x19, 0x47, 0x17, 0xd5, 0xfc, 0xdd, + 0x33, 0xea, 0x61, 0xbd, 0x4e, 0x88, 0x97, 0xa1, 0xed, 0x32, 0xeb, 0x14, 0x84, 0xe0, 0x52, 0xe1, + 0x72, 0x7c, 0x59, 0x59, 0xc4, 0x24, 0xce, 0xaf, 0xf1, 0xa4, 0x7e, 0xc1, 0x6e, 0xac, 0x5a, 0xad, + 0xdf, 0xa5, 0x3e, 0x3b, 0xce, 0xe3, 0xe5, 0xbe, 0x63, 0xfe, 0x1e, 0xb2, 0xfc, 0x7f, 0x99, 0xf5, + 0x82, 0x36, 0x0d, 0x06, 0xad, 0x9e, 0xfd, 0x6b, 0x82, 0x01, 0x9e, 0x23, 0x38, 0x31, 0x73, 0xaf, + 0x63, 0x09, 0x5b, 0x46, 0x87, 0xd7, 0xce, 0x7b, 0xc9, 0x65, 0x06, 0x1c, 0x5b, 0x03, 0x10, 0x27, + 0xf7, 0x7f, 0xc1, 0x19, 0xc6, 0x93, 0xdf, 0x83, 0xdd, 0xa2, 0x3b, 0xff, 0x6b, 0x64, 0x5f, 0xd2, + 0x71, 0x91, 0x5f, 0x88, 0x96, 0xef, 0xa5, 0xfe, 0xd6, 0xaf, 0x71, 0x5c, 0xcf, 0xa0, 0x54, 0x8c, + 0x49, 0xd7, 0xb7, 0xcd, 0x0c, 0x13, 0x88, 0xac, 0x96, 0x9c, 0x6f, 0x2c, 0x8e, 0x2c, 0xfa, 0xf9, + 0x19, 0x32, 0x26, 0xd8, 0x20, 0x5b, 0xa0, 0xdd, 0x97, 0xdc, 0xb4, 0x60, 0x08, 0x14, 0x86, 0xb9, + 0x4d, 0xd6, 0xe3, 0xf5, 0xd6, 0xee, 0x19, 0xa0, 0x0a, 0xe7, 0xf1, 0x55, 0xdc, 0x1a, 0xf1, 0xac, + 0x32, 0x64, 0x61, 0x72, 0x50, 0x12, 0x52, 0xc4, 0x3a, 0xa8, 0xa8, 0x1d, 0x9f, 0xc0, 0xdf, 0xf4, + 0x2d, 0xe7, 0xf8, 0x9a, 0x96, 0x8c, 0x30, 0x31, 0x73, 0x23, 0x35, 0xa3, 0x2a, 0x09, 0x47, 0x01, + 0x35, 0x11, 0x16, 0x4c, 0x65, 0xde, 0x47, 0xdb, 0x31, 0x82, 0x60, 0x82, 0xcd, 0xe6, 0xf6, 0x9d, + 0x01, 0x15, 0xfa, 0x2b, 0x1d, 0xa1, 0x7f, 0x21, 0x77, 0x8d, 0xfe, 0x24, 0x26, 0x1d, 0x3c, 0xbf, + 0xe5, 0xab, 0x22, 0x8a, 0x5a, 0x2b, 0x95, 0x16, 0xed, 0x40, 0x18, 0x3d, 0x89, 0x19, 0x2a, 0x08, + 0x33, 0x19, 0x88, 0x94, 0x00, 0x77, 0xc0, 0x13, 0x44, 0x66, 0x44, 0xa8, 0x0c, 0x7c, 0x27, 0xf5, + 0x1e, 0x02, 0x6e, 0xa3, 0xc2, 0x9e, 0xb3, 0x40, 0xba, 0xf4, 0x63, 0xcf, 0xa9, 0x0e, 0xc2, 0x4e, + 0x59, 0x82, 0x10, 0x78, 0xd8, 0x25, 0x2c, 0x0c, 0xd6, 0x85, 0x48, 0xa5, 0x2f, 0x29, 0x4f, 0x81, + 0x0c, 0x6d, 0x3a, 0xda, 0xf1, 0x1e, 0x21, 0xb8, 0x26, 0x0a, 0xd1, 0xf2, 0xf0, 0xbf, 0x40, 0xb6, + 0xfa, 0x46, 0x68, 0x11, 0xf0, 0xa3, 0x1d, 0x5d, 0x00, 0xa1, 0x42, 0xc3, 0x52, 0x47, 0x96, 0x09, + 0xc4, 0xd4, 0x85, 0x63, 0x55, 0x93, 0x8a, 0xd7, 0x9b, 0x47, 0x6a, 0x51, 0x2a, 0x1c, 0xc1, 0xbd, + 0x7a, 0x9d, 0xaf, 0x95, 0xa5, 0x32, 0xb4, 0x06, 0x8b, 0x46, 0xf2, 0x92, 0xaa, 0xd5, 0xe1, 0x27, + 0x27, 0x17, 0x4a, 0x24, 0x2f, 0xe7, 0x8b, 0x92, 0x5a, 0x90, 0x15, 0x70, 0x43, 0x64, 0x0d, 0x4a, + 0x8b, 0xb2, 0x56, 0x3c, 0x2a, 0xca, 0x25, 0x49, 0xcd, 0xcb, 0xe5, 0x3a, 0x3c, 0x15, 0x10, 0x72, + 0xb3, 0x44, 0x00, 0x2c, 0x87, 0x3f, 0x5a, 0xad, 0x28, 0x15, 0x59, 0x57, 0x2a, 0xc1, 0x71, 0x8e, + 0xd5, 0xb2, 0x5c, 0x2a, 0x4a, 0x25, 0xb9, 0x94, 0x3f, 0x52, 0x4b, 0x72, 0x4e, 0xda, 0x94, 0xb5, + 0xba, 0x8a, 0x8f, 0x92, 0xaa, 0xc8, 0x4a, 0x9e, 0xa8, 0x65, 0x49, 0x55, 0xd9, 0xef, 0x52, 0x53, + 0xb5, 0x7c, 0xad, 0x16, 0x8e, 0xa0, 0x7c, 0x13, 0x91, 0xd4, 0x72, 0xd7, 0x9a, 0x92, 0x40, 0x53, + 0x53, 0x10, 0x4f, 0xfc, 0x55, 0xe4, 0x7c, 0x8e, 0xa8, 0x9b, 0x72, 0x21, 0x2f, 0x95, 0x11, 0x93, + 0xf9, 0x80, 0x5f, 0x04, 0x92, 0xdd, 0xde, 0x42, 0x92, 0x6e, 0xbf, 0x17, 0xab, 0x09, 0xef, 0x31, + 0xa2, 0x1f, 0xde, 0x03, 0x23, 0x61, 0xfd, 0xc2, 0x1a, 0x23, 0x7f, 0x4d, 0xd6, 0x71, 0x0c, 0x84, + 0x19, 0xea, 0x0f, 0x51, 0x1b, 0x32, 0x4b, 0x19, 0x44, 0x42, 0x50, 0xf8, 0xd9, 0x25, 0xf3, 0x1a, + 0x53, 0x99, 0xf9, 0xb1, 0x8d, 0x28, 0xfd, 0x84, 0x3e, 0x5a, 0xb4, 0x12, 0x53, 0x03, 0xbd, 0xcd, + 0x87, 0xc9, 0x8f, 0x22, 0xf4, 0x1f, 0x7a, 0xb1, 0x2c, 0x02, 0x34, 0x69, 0xdb, 0xf3, 0x0d, 0x16, + 0x61, 0xb1, 0x75, 0x6e, 0x54, 0x86, 0x76, 0x00, 0x7a, 0xc8, 0xfc, 0xed, 0xfd, 0x5a, 0x1e, 0xac, + 0xaa, 0x49, 0xd2, 0xca, 0xfd, 0x42, 0x27, 0x46, 0x1b, 0xc3, 0x9e, 0xdf, 0x8c, 0xdb, 0x06, 0xc8, + 0x13, 0x4b, 0x4b, 0x91, 0x2d, 0xa4, 0x28, 0xf1, 0x29, 0x2c, 0xb1, 0xc0, 0xf2, 0xfc, 0xb0, 0x3d, + 0x08, 0x09, 0x2a, 0x3d, 0x81, 0x6c, 0x58, 0x3e, 0xed, 0xe8, 0x42, 0x62, 0x55, 0xf7, 0xdd, 0x2e, + 0x8c, 0x15, 0xd0, 0x62, 0x5e, 0xb2, 0xaf, 0x77, 0x4e, 0x2f, 0x46, 0xca, 0xc7, 0x83, 0xae, 0x57, + 0x83, 0xff, 0x4e, 0x9a, 0x57, 0xd6, 0xde, 0x55, 0x17, 0xee, 0x76, 0xf0, 0xb1, 0x76, 0x5e, 0xaf, + 0xdd, 0xe0, 0xb5, 0x53, 0xce, 0x6e, 0x5a, 0xac, 0xe4, 0xf3, 0x49, 0xf3, 0x42, 0x39, 0xac, 0xf9, + 0x41, 0xbe, 0x5d, 0x3c, 0x87, 0xe7, 0x87, 0x93, 0xbf, 0x2f, 0xf6, 0xf6, 0xaf, 0x4e, 0xf7, 0xd2, + 0xce, 0x55, 0x10, 0x9e, 0x6a, 0x6a, 0xed, 0xca, 0x6d, 0x9c, 0x04, 0xfb, 0xca, 0x75, 0x5a, 0xd9, + 0xfb, 0x7c, 0x6d, 0x0f, 0x6b, 0x9f, 0x3b, 0x35, 0x5a, 0xfa, 0xe6, 0x1c, 0x95, 0xf6, 0xbe, 0xec, + 0xb5, 0xcf, 0x0b, 0xed, 0xf3, 0xb2, 0x5b, 0x3f, 0xac, 0xb7, 0x76, 0xff, 0xde, 0x2f, 0x5d, 0xfa, + 0x43, 0xcb, 0x08, 0x8a, 0x37, 0xad, 0xf1, 0xae, 0xb9, 0x33, 0xd0, 0xac, 0xe6, 0x43, 0xe9, 0xc1, + 0xb6, 0x82, 0xf6, 0x47, 0xb5, 0x73, 0xb5, 0xa9, 0x36, 0x2e, 0x3e, 0x7e, 0x34, 0xf6, 0x3b, 0xea, + 0xa3, 0xe5, 0x9f, 0x95, 0xe9, 0xfd, 0xb1, 0x5b, 0x6f, 0x94, 0x0b, 0xca, 0x59, 0x36, 0x3d, 0xcc, + 0xb6, 0xeb, 0xda, 0xb7, 0xf6, 0xb7, 0x51, 0xbe, 0x1b, 0x1c, 0xec, 0xe6, 0x1a, 0x0f, 0xd9, 0x03, + 0x2d, 0x97, 0x6e, 0x0d, 0x9b, 0xe6, 0xa8, 0xe4, 0x3e, 0xa8, 0x1f, 0xcb, 0xe5, 0xd2, 0x0e, 0xad, + 0x9f, 0xe7, 0x6b, 0x07, 0xc7, 0x35, 0x7b, 0xef, 0xbe, 0x7d, 0x60, 0xec, 0x94, 0xba, 0xae, 0xb9, + 0xd7, 0xb1, 0x2e, 0xbe, 0x99, 0x17, 0xe7, 0xcd, 0xfa, 0xa6, 0xdb, 0x3e, 0xb7, 0x1f, 0x6a, 0xd7, + 0x76, 0x50, 0xfb, 0x74, 0xb0, 0xb3, 0xdf, 0xed, 0x5e, 0x14, 0xce, 0x87, 0xe7, 0xa5, 0xab, 0xf6, + 0xe5, 0x89, 0xb9, 0xd9, 0x3b, 0x1a, 0xee, 0x9a, 0x75, 0xad, 0xaf, 0xf9, 0xd6, 0xe1, 0x89, 0x76, + 0x90, 0xbf, 0xca, 0x0e, 0x2f, 0x5a, 0x2e, 0x1d, 0x8f, 0xdd, 0x27, 0xab, 0x1f, 0x94, 0x14, 0xaf, + 0x76, 0xe6, 0x58, 0x27, 0x67, 0x47, 0xf7, 0x5f, 0x5c, 0x43, 0x1d, 0xe6, 0xb3, 0x8f, 0xd7, 0xbd, + 0xf0, 0xbc, 0x71, 0x55, 0x0e, 0x9f, 0xce, 0x3f, 0x9f, 0xe6, 0xea, 0xf5, 0x87, 0xbc, 0xeb, 0x9f, + 0xed, 0x96, 0x8f, 0x8f, 0x4e, 0xd2, 0xc5, 0x6f, 0x66, 0x99, 0x76, 0xca, 0xd4, 0x1f, 0xed, 0x7c, + 0x1c, 0x36, 0x4a, 0x05, 0xe5, 0xf3, 0x47, 0xf5, 0xf3, 0x38, 0xef, 0xd8, 0x9b, 0xd9, 0xce, 0xf9, + 0xbe, 0x3f, 0xda, 0x3c, 0xab, 0x1d, 0x34, 0x77, 0xbb, 0x65, 0xe3, 0x69, 0x30, 0xfa, 0x7b, 0xf7, + 0xa4, 0x78, 0xdf, 0x1a, 0xd0, 0x7e, 0xc9, 0x48, 0x1f, 0xec, 0xef, 0xe7, 0xe8, 0xd3, 0x89, 0x42, + 0xdd, 0x42, 0x67, 0xf7, 0x5b, 0xf9, 0xbc, 0xe3, 0xa6, 0xaf, 0xbe, 0x5d, 0x77, 0xef, 0xad, 0x4f, + 0x85, 0x16, 0x3d, 0xeb, 0x8f, 0xea, 0x1f, 0x47, 0x57, 0x8d, 0xfb, 0xa2, 0xa1, 0xd5, 0xea, 0x37, + 0xa5, 0x27, 0xbf, 0x6e, 0xd6, 0xeb, 0xb9, 0xfc, 0xd5, 0xbd, 0xff, 0x34, 0x08, 0xef, 0x8f, 0xbe, + 0xd8, 0xe7, 0xf5, 0xec, 0x83, 0xa5, 0x34, 0x9c, 0xf1, 0xd9, 0x78, 0xb0, 0x19, 0x7e, 0x7c, 0x3a, + 0xce, 0xdb, 0x07, 0x67, 0x9d, 0xd2, 0xe0, 0xa0, 0x10, 0xec, 0xee, 0x8d, 0x3e, 0xf5, 0x6f, 0x3e, + 0x0d, 0x7d, 0xab, 0x5c, 0xb8, 0xf8, 0x72, 0x03, 0xdc, 0x3d, 0xec, 0x97, 0xd2, 0x9f, 0x8d, 0xf1, + 0x49, 0xf8, 0x6d, 0x1c, 0x7e, 0xa6, 0x47, 0xdf, 0x3e, 0xb5, 0xee, 0xaf, 0xae, 0x4e, 0xda, 0x47, + 0xf5, 0x74, 0x67, 0x70, 0xa0, 0xf5, 0xfa, 0x47, 0x83, 0x52, 0x78, 0xe6, 0x14, 0x82, 0x2f, 0xbb, + 0x35, 0xb7, 0x7f, 0xf4, 0xa0, 0xf4, 0x9e, 0xf6, 0x77, 0x6d, 0x3f, 0xbd, 0xb3, 0xff, 0xb7, 0xf3, + 0x50, 0xdf, 0xab, 0x1b, 0x1f, 0xf7, 0x07, 0xcd, 0x9d, 0x9b, 0xa6, 0x53, 0xcb, 0xf7, 0x8f, 0x3e, + 0x85, 0xf6, 0xc5, 0xfd, 0xde, 0xb8, 0x7b, 0x38, 0xde, 0xf3, 0xf7, 0x9f, 0xc6, 0x87, 0x1f, 0xef, + 0x7d, 0x7a, 0x73, 0xe9, 0xb4, 0xbe, 0x7c, 0xec, 0x1a, 0x0d, 0xff, 0xc2, 0x79, 0xf2, 0x1a, 0x5e, + 0x38, 0xa2, 0x9f, 0x69, 0xcd, 0xda, 0xb3, 0x0e, 0x1f, 0xcf, 0x76, 0x2e, 0x77, 0x77, 0xac, 0xf3, + 0x9b, 0xee, 0xde, 0x8d, 0x75, 0xd2, 0x1a, 0xb7, 0x0e, 0x4f, 0xac, 0xc7, 0x1b, 0x35, 0x68, 0x35, + 0x47, 0xa3, 0x87, 0x56, 0xe3, 0xf4, 0x8b, 0xff, 0x14, 0xe4, 0xae, 0x2f, 0x7a, 0xf7, 0x3d, 0xf7, + 0xf4, 0xa1, 0xd8, 0x3a, 0x7d, 0xf0, 0xbf, 0x3d, 0x1e, 0x37, 0x2e, 0x37, 0xc7, 0xf5, 0x6f, 0xe3, + 0xc7, 0x93, 0x71, 0xab, 0xb6, 0x7f, 0xdc, 0x55, 0x7b, 0x9f, 0x2e, 0x76, 0x0e, 0x6e, 0x9c, 0xae, + 0x46, 0x5b, 0xde, 0x69, 0x73, 0xff, 0xc0, 0xbc, 0x4c, 0x1f, 0x8e, 0x8e, 0x73, 0x7b, 0xb6, 0x53, + 0x7c, 0xda, 0x19, 0x7d, 0xfe, 0xb2, 0xaf, 0x3e, 0x3e, 0x34, 0x3f, 0x3d, 0x9c, 0x1a, 0xc5, 0x6f, + 0x61, 0xc3, 0xa1, 0x6d, 0x3a, 0x38, 0x1f, 0xd6, 0xd3, 0xdd, 0x7c, 0xff, 0xcb, 0x67, 0xdb, 0x38, + 0x0d, 0x37, 0xef, 0xc7, 0xbb, 0x2d, 0x2d, 0x7f, 0xae, 0x7d, 0xfb, 0x58, 0x2f, 0x7c, 0xbe, 0x6c, + 0x0e, 0x7d, 0xe7, 0x63, 0xee, 0x4b, 0xe3, 0xf8, 0x93, 0x92, 0x77, 0x8f, 0xcc, 0xe3, 0xb3, 0x8f, + 0xe1, 0xf9, 0xe9, 0xf1, 0x93, 0x77, 0x70, 0xf9, 0x74, 0xf6, 0x54, 0xdc, 0xbc, 0x39, 0x39, 0xd3, + 0x86, 0x76, 0x69, 0x57, 0x55, 0x3b, 0xe1, 0xf0, 0xfc, 0xd3, 0x4d, 0xfd, 0x69, 0x1c, 0x96, 0xee, + 0x3b, 0xa3, 0x53, 0x55, 0xb9, 0x3c, 0xef, 0x1e, 0x15, 0xac, 0x4b, 0xb6, 0x26, 0x6a, 0x3b, 0x7f, + 0x5f, 0x5c, 0x15, 0xf6, 0xfc, 0x87, 0xbf, 0xbb, 0xdd, 0xae, 0xae, 0x0b, 0xdb, 0x1b, 0x60, 0x3a, + 0xdb, 0xbe, 0xdd, 0x0f, 0x09, 0x73, 0x61, 0x05, 0x5c, 0xcb, 0xd9, 0x7b, 0x63, 0x68, 0xf0, 0x52, + 0x00, 0xe8, 0x0c, 0x5c, 0x96, 0x2c, 0x22, 0xdd, 0x43, 0x33, 0x45, 0xc5, 0x89, 0x4f, 0xc3, 0x81, + 0xef, 0x12, 0x53, 0xee, 0xd2, 0x70, 0xcf, 0xa1, 0x98, 0x74, 0xd8, 0x19, 0xb3, 0xaa, 0xe9, 0x0c, + 0xb4, 0xbd, 0xb7, 0x00, 0xd9, 0x06, 0xdf, 0x2a, 0xa4, 0x11, 0x30, 0x02, 0x0e, 0x0d, 0x1f, 0x0c, + 0xac, 0x09, 0x71, 0x38, 0x96, 0xa0, 0xaa, 0x60, 0xa3, 0x81, 0xae, 0xc8, 0x46, 0x39, 0x7a, 0x4c, + 0x58, 0xcf, 0x2f, 0x26, 0x98, 0x1d, 0xe6, 0x88, 0x00, 0x82, 0x51, 0x42, 0x5a, 0x88, 0x8a, 0x37, + 0x22, 0xab, 0xbf, 0xe0, 0xe0, 0xac, 0x77, 0x1b, 0xb1, 0x89, 0xa5, 0xae, 0x6f, 0xb1, 0xea, 0x2b, + 0x22, 0x34, 0x7a, 0x15, 0x1b, 0x0b, 0x6e, 0xc5, 0xb2, 0x59, 0x8c, 0xad, 0x12, 0x4b, 0x48, 0x54, + 0x8a, 0x2b, 0x7e, 0x46, 0x4e, 0x83, 0xff, 0x85, 0x6d, 0xee, 0x67, 0x6c, 0x70, 0x47, 0xe3, 0xad, + 0xa2, 0xe4, 0x20, 0x10, 0xe5, 0x3e, 0x46, 0x91, 0x68, 0x9a, 0x55, 0x1e, 0xe6, 0x1b, 0xc5, 0xa7, + 0x63, 0x15, 0xac, 0x7c, 0xde, 0xca, 0x0f, 0xcb, 0x56, 0x26, 0x0f, 0x4f, 0x65, 0xa2, 0x2a, 0xb3, + 0x27, 0x4d, 0x23, 0x45, 0x84, 0xb3, 0x32, 0xe5, 0x27, 0x21, 0xb6, 0xd6, 0x1b, 0xe4, 0xb9, 0xcd, + 0x01, 0xa4, 0xa4, 0x8a, 0xf4, 0x04, 0x1a, 0xe1, 0xb4, 0xb5, 0xed, 0x8d, 0xa8, 0x8e, 0x30, 0x4d, + 0x4b, 0x42, 0x8f, 0xb7, 0xfd, 0xbb, 0x79, 0x7a, 0x42, 0x52, 0x7d, 0xd6, 0x03, 0x90, 0x95, 0x00, + 0xf7, 0x58, 0x79, 0xcf, 0x08, 0x7d, 0xfb, 0x51, 0x84, 0x6e, 0x34, 0x68, 0xdf, 0xc7, 0xed, 0x08, + 0xcc, 0x80, 0x90, 0x0d, 0x1b, 0xb0, 0x9e, 0x27, 0x76, 0x85, 0x45, 0xaf, 0x8b, 0x79, 0xcb, 0x0c, + 0x30, 0x03, 0xa4, 0xf5, 0x06, 0x61, 0xa5, 0x03, 0x3d, 0x9b, 0x11, 0x79, 0x57, 0xd9, 0x11, 0xfa, + 0x28, 0x81, 0xa1, 0x19, 0xf7, 0xb2, 0xd6, 0x0f, 0x46, 0x40, 0xc7, 0x68, 0x01, 0x82, 0xe0, 0x3a, + 0xea, 0x82, 0x43, 0xcd, 0x26, 0x48, 0x56, 0x3f, 0xce, 0xd3, 0x09, 0xdb, 0x47, 0x14, 0x3a, 0xc0, + 0xa2, 0x0a, 0x88, 0x12, 0x83, 0xc4, 0x99, 0x87, 0x28, 0x47, 0xd0, 0x75, 0x24, 0x38, 0xf3, 0x94, + 0x28, 0xe3, 0x2c, 0x6b, 0x4c, 0x70, 0x32, 0x2b, 0xfd, 0x91, 0x8d, 0xa8, 0x49, 0x32, 0xeb, 0x89, + 0x8d, 0xbc, 0x3e, 0x93, 0xee, 0xa1, 0xe1, 0x0c, 0x28, 0xd6, 0x22, 0x85, 0x80, 0x00, 0x0c, 0x82, + 0x42, 0x4f, 0xf1, 0x9d, 0xb0, 0xad, 0xed, 0x92, 0x63, 0x56, 0x0d, 0x08, 0xf1, 0x56, 0x2b, 0xcd, + 0x7d, 0xcd, 0x11, 0xb6, 0x9b, 0xd4, 0xef, 0x03, 0x31, 0xc0, 0xd1, 0x91, 0xc0, 0x0b, 0xf5, 0x83, + 0x90, 0x80, 0x1f, 0xcf, 0xb3, 0x5b, 0xc8, 0x21, 0xcc, 0xda, 0x91, 0x77, 0x4e, 0x58, 0xcd, 0xcc, + 0xba, 0xd9, 0x58, 0xee, 0xc7, 0xd1, 0xfc, 0xe7, 0xfa, 0x61, 0xcd, 0xe3, 0x6c, 0x19, 0xc9, 0xbc, + 0xeb, 0x86, 0xd5, 0x39, 0x3a, 0xb8, 0xea, 0x18, 0xbe, 0x33, 0x5a, 0x65, 0x81, 0x1b, 0x8c, 0x25, + 0x3f, 0xc1, 0x91, 0x0d, 0xc6, 0x12, 0xf8, 0x01, 0x6a, 0xcc, 0x49, 0x75, 0x3a, 0x08, 0xc1, 0x0f, + 0x24, 0xbc, 0xb8, 0xf2, 0x4a, 0x9e, 0x6c, 0x24, 0x99, 0xb2, 0xd4, 0x23, 0x79, 0x15, 0x47, 0x46, + 0xc0, 0x4a, 0xe0, 0xde, 0x3a, 0x86, 0xcc, 0x64, 0xfd, 0x59, 0x7e, 0x80, 0xfb, 0x0f, 0x0c, 0xa9, + 0x5f, 0x5d, 0x1c, 0x25, 0x69, 0xb4, 0x08, 0x63, 0x19, 0xc2, 0x76, 0xc3, 0xeb, 0x51, 0x52, 0x0b, + 0x02, 0x1b, 0x3c, 0x53, 0x37, 0x24, 0x37, 0xb5, 0xe3, 0x44, 0x83, 0x17, 0x68, 0xba, 0xf1, 0x13, + 0x62, 0xce, 0xfc, 0xac, 0xfd, 0x25, 0xc2, 0x6e, 0xb0, 0xe8, 0x84, 0xb4, 0x3d, 0x93, 0xfe, 0x24, + 0x75, 0x63, 0xe2, 0xb2, 0xf5, 0xbb, 0xae, 0xef, 0xd7, 0x91, 0xd8, 0xa2, 0x8f, 0xcf, 0x50, 0xb8, + 0xb1, 0xf7, 0x99, 0xa4, 0xde, 0x7d, 0x1b, 0x78, 0x61, 0xb5, 0x93, 0xc7, 0x7f, 0xfc, 0x5e, 0x7c, + 0x96, 0xe0, 0xe0, 0x31, 0xc2, 0x84, 0x76, 0xf7, 0xea, 0x24, 0xa5, 0xe5, 0xf3, 0x52, 0xf4, 0x27, + 0xfe, 0x2b, 0xc4, 0x84, 0x98, 0xc2, 0xa7, 0xc0, 0x33, 0xb7, 0x3b, 0x9f, 0x5a, 0x6d, 0x56, 0xf6, + 0x4b, 0x6a, 0x63, 0x4d, 0x97, 0xaf, 0x54, 0x1c, 0xd6, 0xb8, 0x05, 0xa1, 0xfb, 0x7a, 0xc5, 0xb1, + 0xd1, 0x60, 0x95, 0x33, 0x4a, 0x2a, 0xf8, 0x8f, 0xdf, 0x43, 0x04, 0x27, 0x11, 0xb5, 0x24, 0x91, + 0x57, 0x11, 0x79, 0x23, 0x56, 0x33, 0x98, 0xec, 0x14, 0xb6, 0x2f, 0xf0, 0x42, 0x52, 0xbf, 0xd4, + 0x05, 0xce, 0x11, 0x29, 0xda, 0x64, 0xd7, 0x97, 0x98, 0xfc, 0x3c, 0xd3, 0x36, 0x7e, 0x52, 0xd1, + 0xb7, 0x98, 0xde, 0x72, 0x81, 0xc8, 0x3c, 0x59, 0x2b, 0x6c, 0xef, 0xcc, 0x4a, 0xd6, 0xb2, 0xec, + 0x87, 0x12, 0x51, 0x7d, 0x85, 0xbd, 0x67, 0xc9, 0x3c, 0xb2, 0xc1, 0x5d, 0x21, 0x4e, 0x39, 0xc6, + 0xeb, 0x15, 0x4c, 0x48, 0xcf, 0x76, 0x75, 0x41, 0x85, 0xab, 0x01, 0x86, 0x5e, 0x2b, 0x14, 0x84, + 0x98, 0xb9, 0xaa, 0x56, 0x66, 0x82, 0xd2, 0x37, 0x5c, 0xbe, 0xd6, 0xe6, 0x6d, 0xaf, 0x11, 0x42, + 0xd8, 0x06, 0x08, 0x20, 0x12, 0xd4, 0xff, 0x2e, 0x25, 0x71, 0x64, 0xf7, 0xec, 0x30, 0x26, 0xd1, + 0xc6, 0xb1, 0xf1, 0x48, 0x5c, 0x8f, 0x78, 0x1d, 0xc2, 0x2a, 0x83, 0x2c, 0x6a, 0xbd, 0xca, 0x7f, + 0x93, 0x5a, 0x2b, 0x48, 0x2d, 0x52, 0xab, 0xa0, 0x6a, 0x33, 0x6a, 0x69, 0x85, 0xe2, 0x22, 0xb5, + 0xe6, 0x6d, 0x23, 0x6a, 0x01, 0xc4, 0x33, 0xd4, 0x8a, 0x57, 0xa8, 0x65, 0x64, 0x30, 0xb5, 0x26, + 0xfc, 0x3c, 0xf5, 0x2c, 0xe3, 0x10, 0x56, 0x75, 0xa3, 0x46, 0x76, 0xe9, 0xd0, 0x6e, 0x53, 0x72, + 0xb8, 0xbb, 0x86, 0x4e, 0x1b, 0xeb, 0x35, 0x01, 0x9f, 0x7c, 0x54, 0xb5, 0xb0, 0x47, 0x20, 0x24, + 0x5c, 0x69, 0x4e, 0x10, 0x36, 0xce, 0x6c, 0x39, 0x31, 0x5f, 0xeb, 0x2b, 0xf8, 0x5a, 0x5f, 0xdb, + 0xfc, 0xc4, 0x89, 0x43, 0xfd, 0xaf, 0x8a, 0xa2, 0x0a, 0xff, 0xca, 0x0c, 0xaf, 0x16, 0xa7, 0x78, + 0xe5, 0xda, 0xdf, 0x06, 0x8b, 0x33, 0xdd, 0x78, 0x41, 0xe7, 0xbd, 0x76, 0xa6, 0x1b, 0x7c, 0xaa, + 0x38, 0x20, 0x79, 0x61, 0xaa, 0xc6, 0x6b, 0xe6, 0xfa, 0xb3, 0xea, 0xc2, 0x32, 0x4e, 0x8c, 0x1e, + 0x4d, 0x4e, 0x16, 0x9f, 0xff, 0xb5, 0x79, 0xb2, 0xc1, 0xe2, 0x89, 0xce, 0x3d, 0xf0, 0x8f, 0x76, + 0xd8, 0xb6, 0xa8, 0x2b, 0xfc, 0x8e, 0xc5, 0x0d, 0x1e, 0xc9, 0x15, 0x3a, 0x25, 0xb1, 0x78, 0x9e, + 0x65, 0x2d, 0x0f, 0x5c, 0x3b, 0x37, 0x39, 0xad, 0x0d, 0xf2, 0x3b, 0xa7, 0x15, 0x0f, 0xc9, 0xe7, + 0xf5, 0x1b, 0x3c, 0xc3, 0x10, 0xc2, 0x23, 0x0a, 0x4e, 0x06, 0xdb, 0x9c, 0x16, 0xb6, 0x2f, 0xd9, + 0x23, 0x89, 0x36, 0xab, 0x61, 0x71, 0xbc, 0xde, 0x39, 0x5c, 0x9c, 0x07, 0xdf, 0x6c, 0x8a, 0x67, + 0xe0, 0x46, 0x7a, 0xc6, 0x66, 0xb6, 0xb4, 0x9b, 0x90, 0x40, 0x25, 0x52, 0x3e, 0x4a, 0xa4, 0x7c, + 0x8a, 0xb9, 0x65, 0x4f, 0x73, 0x11, 0xc3, 0xe5, 0xc1, 0x93, 0x56, 0x7c, 0x6e, 0xd9, 0x58, 0xe0, + 0x89, 0x8d, 0xe7, 0x2d, 0x71, 0xe3, 0x7d, 0xd6, 0x3a, 0xb1, 0x19, 0x0f, 0xb1, 0x5f, 0x14, 0x83, + 0x25, 0x49, 0x99, 0x65, 0x31, 0xd2, 0x3c, 0xa8, 0x8a, 0xdb, 0xcd, 0xce, 0xb7, 0xd4, 0xb1, 0x20, + 0x9a, 0xd0, 0xac, 0x50, 0x58, 0x0a, 0x46, 0x7f, 0x21, 0xda, 0x7a, 0x9d, 0x10, 0xe2, 0xfc, 0x66, + 0x43, 0xb3, 0x49, 0xbc, 0x26, 0x16, 0x5e, 0x9f, 0x62, 0xc7, 0x2d, 0x04, 0xbb, 0xfd, 0x00, 0x9d, + 0x8d, 0x70, 0x69, 0x34, 0xb1, 0xcb, 0x94, 0x88, 0xbe, 0x12, 0xdb, 0xc4, 0x01, 0x25, 0xc1, 0x77, + 0x71, 0xe2, 0x20, 0x79, 0x5d, 0x32, 0x7e, 0x83, 0x65, 0xe3, 0x4b, 0xa4, 0xd4, 0x28, 0x19, 0x05, + 0x52, 0xc0, 0xd4, 0x36, 0x51, 0x32, 0x78, 0x37, 0x7b, 0xc2, 0x3b, 0x4b, 0x55, 0x8c, 0x44, 0x41, + 0x26, 0x51, 0x9d, 0x81, 0xa7, 0xe3, 0x12, 0x51, 0x0b, 0x46, 0x8e, 0xe4, 0x58, 0x91, 0x9a, 0xc9, + 0x65, 0xf0, 0x3e, 0x7a, 0x22, 0x4b, 0x4f, 0x24, 0xf1, 0x84, 0x35, 0x18, 0x74, 0x6f, 0xf0, 0xa8, + 0x9b, 0xbc, 0x73, 0x5b, 0x41, 0xbf, 0xca, 0x66, 0x12, 0x45, 0xd3, 0x8b, 0x3c, 0x7e, 0xc5, 0x1a, + 0x99, 0x49, 0x50, 0x94, 0x55, 0x9f, 0xc5, 0xd1, 0xc9, 0x0d, 0xa2, 0x25, 0x7d, 0x80, 0xa0, 0x9f, + 0x21, 0x94, 0x21, 0x15, 0x32, 0x5b, 0x35, 0x1c, 0x95, 0xd8, 0x14, 0xc7, 0x62, 0xb4, 0x98, 0x89, + 0x5f, 0xbb, 0x4a, 0x58, 0x5f, 0x33, 0xc3, 0x1c, 0x3b, 0x2f, 0x45, 0xcc, 0x08, 0xf1, 0x3e, 0x93, + 0xbf, 0x2b, 0x68, 0xdc, 0x80, 0x9a, 0xfd, 0x5d, 0x68, 0xdc, 0xac, 0x45, 0x23, 0x26, 0xe9, 0xc6, + 0xfa, 0x75, 0x93, 0xed, 0x47, 0x59, 0x89, 0x04, 0x62, 0x89, 0xe3, 0x50, 0x49, 0x21, 0x9e, 0x9d, + 0xf1, 0xc2, 0xb9, 0xed, 0xc2, 0x43, 0xc4, 0x33, 0x8b, 0xfa, 0xc0, 0xb8, 0x96, 0xbf, 0x0d, 0x01, + 0x15, 0x5e, 0x48, 0x1d, 0xe5, 0x14, 0xe3, 0xe5, 0x48, 0x3b, 0x18, 0x28, 0x8b, 0x09, 0xde, 0xce, + 0x14, 0x55, 0x34, 0x76, 0xac, 0x93, 0xf8, 0xb4, 0x10, 0x96, 0x4f, 0x2a, 0x89, 0xc7, 0x73, 0x6c, + 0x85, 0x3e, 0x16, 0xd2, 0x57, 0xc9, 0x05, 0xbd, 0x7c, 0x88, 0x8c, 0x69, 0xf1, 0x5e, 0x97, 0xf5, + 0xdd, 0xf7, 0x29, 0x2e, 0x32, 0x61, 0x25, 0x8b, 0x95, 0x38, 0xd4, 0x11, 0x1d, 0xcc, 0x84, 0xb1, + 0xfd, 0x76, 0x94, 0x5d, 0x67, 0x69, 0x3d, 0x3c, 0x05, 0x1b, 0x67, 0xd7, 0xe7, 0x04, 0x9e, 0x8b, + 0xa2, 0x6f, 0x8c, 0x32, 0x4b, 0x3b, 0x8e, 0xcf, 0xe2, 0x1f, 0xe3, 0xc3, 0xe0, 0x11, 0x9b, 0x1f, + 0x0c, 0x45, 0x0c, 0x27, 0xd4, 0x85, 0x0b, 0x63, 0x74, 0xc8, 0xe8, 0xce, 0x9b, 0xcc, 0xc7, 0x07, + 0x4a, 0x30, 0x8a, 0xce, 0xf0, 0x78, 0x2d, 0x0e, 0x31, 0x83, 0x39, 0xbc, 0xed, 0x76, 0xbc, 0xf5, + 0x64, 0x99, 0xab, 0xe0, 0x78, 0xa3, 0x97, 0x35, 0x43, 0xbf, 0x98, 0x25, 0x0c, 0xa0, 0xc0, 0xf4, + 0x5c, 0x67, 0x0c, 0x14, 0x88, 0xee, 0x04, 0x5c, 0xea, 0x31, 0xf0, 0x22, 0xa5, 0x98, 0x4b, 0xbf, + 0xb4, 0xbb, 0xfa, 0x2c, 0x86, 0x1c, 0x30, 0xf6, 0x6c, 0xfb, 0xe3, 0x68, 0x48, 0x5e, 0x3c, 0xb3, + 0x16, 0x89, 0x8d, 0x56, 0x44, 0x95, 0x3f, 0x26, 0x86, 0xeb, 0x1a, 0x7d, 0x75, 0x06, 0x0c, 0x0f, + 0x0c, 0x3b, 0x8e, 0x51, 0x34, 0x00, 0xb7, 0x7b, 0xae, 0xf9, 0x73, 0xfd, 0xcf, 0x67, 0x85, 0x5e, + 0x84, 0x95, 0xdb, 0x9e, 0x8d, 0x08, 0x6a, 0x2b, 0x80, 0x68, 0x4e, 0xd8, 0xbe, 0xe6, 0x37, 0x44, + 0x95, 0x15, 0xb9, 0xcc, 0x1b, 0xf0, 0xa5, 0x9e, 0x89, 0x15, 0x83, 0x11, 0xef, 0xe2, 0xe0, 0x81, + 0xe6, 0xa0, 0x92, 0xcd, 0x76, 0xed, 0xd0, 0x1a, 0xb4, 0xe4, 0xb6, 0xd7, 0xcb, 0x8e, 0xa8, 0xff, + 0x10, 0x80, 0xdb, 0xd7, 0xcb, 0x62, 0xfa, 0x25, 0xc3, 0x9c, 0x24, 0xf0, 0x91, 0x66, 0x49, 0xca, + 0x6c, 0xcb, 0xf1, 0x5a, 0x59, 0x0c, 0x96, 0xb3, 0x17, 0x7b, 0xb5, 0xdd, 0xe3, 0x3d, 0xb9, 0x87, + 0xf9, 0x1b, 0x6e, 0x91, 0x75, 0xe1, 0x6b, 0xcb, 0x31, 0xdc, 0x07, 0xd0, 0x34, 0xd4, 0xe9, 0x67, + 0x6b, 0x2d, 0xb0, 0x6f, 0x5b, 0x59, 0x03, 0x26, 0x00, 0x98, 0xce, 0x17, 0xe5, 0x12, 0x67, 0xd8, + 0x46, 0x34, 0x4f, 0x10, 0xaf, 0x97, 0x85, 0x19, 0x51, 0x12, 0x90, 0x33, 0xf9, 0x68, 0x1b, 0xee, + 0xd0, 0x08, 0xf8, 0x42, 0x43, 0x64, 0xeb, 0xec, 0x99, 0xd1, 0x9b, 0x57, 0x6d, 0xbf, 0x9c, 0x2e, + 0xf7, 0xfc, 0x14, 0xa6, 0xb7, 0xbb, 0xe0, 0x49, 0xe9, 0x98, 0x32, 0x9f, 0xf9, 0x54, 0xa2, 0x14, + 0x3c, 0x7d, 0xe6, 0x45, 0x5c, 0xfb, 0x62, 0xc1, 0xcd, 0xbc, 0xe0, 0x86, 0x15, 0x80, 0x2d, 0x98, + 0x17, 0xa1, 0x61, 0x10, 0xa5, 0xbe, 0x3f, 0xe2, 0x45, 0xf1, 0xe2, 0x07, 0xb8, 0xc3, 0xdd, 0x08, + 0x8a, 0x79, 0x3b, 0xa2, 0xf4, 0xf7, 0x51, 0x54, 0x10, 0x4b, 0xb5, 0x28, 0x85, 0xcd, 0x2e, 0x2f, + 0x5a, 0xf4, 0x70, 0x44, 0xa9, 0xe5, 0x77, 0x2d, 0x5e, 0xb3, 0x12, 0xd7, 0x42, 0xcf, 0x74, 0x86, + 0xc1, 0x92, 0x73, 0x23, 0x4a, 0xed, 0xfb, 0x56, 0x34, 0xa5, 0x15, 0x59, 0x16, 0xa5, 0x8e, 0xdf, + 0xe3, 0x95, 0x4b, 0xa9, 0x3b, 0x68, 0x76, 0x74, 0x12, 0x37, 0x5b, 0x0a, 0x0c, 0x45, 0x09, 0x43, + 0x23, 0xca, 0x6b, 0x59, 0x94, 0x84, 0x25, 0x57, 0xb3, 0x82, 0xab, 0xa8, 0xe4, 0x64, 0x56, 0xc2, + 0xbc, 0x6e, 0x51, 0x32, 0x9a, 0xbc, 0x60, 0x4d, 0x12, 0x06, 0xc6, 0xdb, 0x6f, 0x26, 0xc6, 0xdb, + 0x5f, 0x46, 0xc7, 0x69, 0x46, 0xd5, 0x2b, 0x89, 0x5f, 0x51, 0xb2, 0xd1, 0x12, 0xb1, 0xca, 0x84, + 0x52, 0xc1, 0xe2, 0xf6, 0x42, 0xf1, 0x7c, 0xe5, 0x03, 0x35, 0xe3, 0xaa, 0x15, 0xad, 0x80, 0xed, + 0x12, 0xad, 0x90, 0xb8, 0xed, 0x39, 0x7b, 0x63, 0xd7, 0x4a, 0x94, 0x46, 0xfa, 0xc8, 0x76, 0x4d, + 0x6f, 0x24, 0x71, 0x19, 0x8b, 0x78, 0x9d, 0x90, 0x3f, 0xc6, 0xb2, 0xeb, 0x65, 0x96, 0xf1, 0x00, + 0x19, 0xc9, 0x7b, 0xbd, 0x4c, 0xde, 0xb8, 0x0a, 0xd7, 0x64, 0xcd, 0xf7, 0x8d, 0xb1, 0x7e, 0x7b, + 0x27, 0xa1, 0x59, 0x42, 0xa6, 0xe9, 0x82, 0x20, 0x61, 0x88, 0x85, 0x96, 0xf4, 0xc2, 0x1b, 0x05, + 0xba, 0x29, 0x43, 0x48, 0xe8, 0x8f, 0x63, 0x32, 0xd4, 0x1c, 0x27, 0x25, 0xc8, 0x71, 0x1c, 0x06, + 0x93, 0xd0, 0x95, 0xaa, 0xbd, 0x95, 0x6c, 0x21, 0x3b, 0xd4, 0xed, 0x86, 0x56, 0xd5, 0x4e, 0xa7, + 0xc5, 0x64, 0xf9, 0xad, 0x7d, 0x27, 0xb3, 0xe5, 0x75, 0x64, 0x07, 0xa1, 0x0c, 0xbc, 0x01, 0x8e, + 0xb1, 0x3e, 0xaa, 0xb8, 0x22, 0x8c, 0x76, 0x3b, 0xf6, 0xe8, 0x74, 0xe1, 0x2d, 0xa5, 0x54, 0x90, + 0x78, 0x11, 0x06, 0x24, 0x71, 0x71, 0xa9, 0x54, 0x12, 0x40, 0xe0, 0x03, 0x78, 0x86, 0xa7, 0x76, + 0xbb, 0x2d, 0x48, 0x5d, 0x9f, 0x52, 0x37, 0xae, 0x57, 0x0a, 0xc5, 0x96, 0x62, 0x08, 0x92, 0x4f, + 0xcd, 0xb8, 0x08, 0x0a, 0x0a, 0x0a, 0xc0, 0x71, 0x3f, 0xd9, 0xeb, 0x76, 0x1d, 0x7a, 0xda, 0xe9, + 0x70, 0x8f, 0x51, 0x02, 0x8f, 0xb1, 0x56, 0x90, 0x0a, 0xf1, 0x61, 0x08, 0x3c, 0x3c, 0x31, 0x7f, + 0x2c, 0x49, 0x6a, 0xa9, 0xa1, 0x2e, 0x00, 0x2c, 0x41, 0x60, 0x0f, 0xc7, 0x00, 0x56, 0xa8, 0xe5, + 0xa4, 0x5c, 0x74, 0xe4, 0x02, 0x0f, 0x7e, 0x24, 0x1e, 0x4b, 0xd2, 0x66, 0xe2, 0x49, 0x55, 0x96, + 0x6b, 0xd5, 0xc2, 0x97, 0x45, 0xdc, 0xdc, 0xdf, 0x88, 0x9a, 0xba, 0x84, 0x9b, 0xba, 0x84, 0x9c, + 0xba, 0x88, 0x9d, 0xa6, 0xac, 0x54, 0x73, 0xf4, 0x62, 0x55, 0x91, 0x58, 0xf2, 0xcd, 0xeb, 0x03, + 0xf4, 0xc7, 0x05, 0x31, 0xb9, 0x3d, 0xf7, 0xea, 0xbd, 0x42, 0x1a, 0x32, 0x6d, 0x7f, 0x71, 0xb0, + 0xc3, 0x64, 0x31, 0xc0, 0xfd, 0xc0, 0xf5, 0xd2, 0xf8, 0xcf, 0x44, 0x00, 0xfd, 0x51, 0x09, 0xfd, + 0x01, 0x95, 0x50, 0xba, 0x85, 0xca, 0x1f, 0x13, 0xd4, 0x50, 0x32, 0x73, 0x06, 0xa7, 0x12, 0xaa, + 0x38, 0xa1, 0x32, 0x11, 0x6c, 0x13, 0x6b, 0x40, 0xa9, 0xcd, 0x2a, 0x00, 0xf4, 0xf6, 0x9f, 0xaa, + 0x03, 0xf1, 0x64, 0x08, 0x22, 0x1a, 0x62, 0x54, 0x48, 0x1f, 0x05, 0x5d, 0x47, 0x18, 0xa6, 0xfa, + 0xe5, 0x48, 0xf3, 0x7f, 0x98, 0xb5, 0xaa, 0x80, 0xe2, 0xe4, 0x77, 0x55, 0x58, 0xa7, 0x10, 0x4b, + 0x3b, 0x3a, 0x58, 0x80, 0x00, 0xdc, 0xd4, 0x30, 0x05, 0x6a, 0x8a, 0x57, 0x89, 0xac, 0x4f, 0x57, + 0x7f, 0xa3, 0x4a, 0x86, 0x9e, 0x51, 0xab, 0x86, 0x0e, 0xba, 0x4d, 0x8e, 0x73, 0xb4, 0x87, 0xae, + 0x49, 0x1f, 0xa3, 0xd6, 0x36, 0xab, 0xe1, 0xa9, 0xcf, 0xe0, 0xd6, 0xb8, 0x8b, 0xba, 0x36, 0x74, + 0xd0, 0x31, 0x6b, 0x1b, 0x78, 0xac, 0x66, 0x4d, 0x03, 0xd0, 0x59, 0x4b, 0x0d, 0x10, 0x87, 0x40, + 0x7f, 0xa3, 0x54, 0x59, 0xa2, 0x5c, 0x67, 0x10, 0xcb, 0x0d, 0xdf, 0xbd, 0x4b, 0x01, 0x88, 0x0a, + 0xfa, 0x50, 0x37, 0xd6, 0xb5, 0xf7, 0x59, 0xfb, 0x28, 0x7f, 0xab, 0x23, 0xcc, 0x72, 0x0f, 0x1f, + 0x00, 0x44, 0xad, 0xc4, 0x29, 0xe9, 0xb5, 0x20, 0x30, 0x08, 0xd0, 0x42, 0xe1, 0x54, 0x19, 0xa0, + 0xf6, 0x30, 0xf1, 0xa7, 0xad, 0xbf, 0x17, 0xde, 0x4b, 0x16, 0xfe, 0x56, 0x83, 0xef, 0xdf, 0x53, + 0xe0, 0xff, 0xdd, 0x82, 0x62, 0xd1, 0x85, 0x3b, 0x81, 0x83, 0xf6, 0x91, 0x80, 0xe0, 0x18, 0x08, + 0x6c, 0xf5, 0x77, 0xf5, 0xf6, 0x1e, 0xa8, 0xa8, 0x58, 0xa1, 0x81, 0x62, 0xc4, 0x0d, 0x64, 0x7c, + 0xe7, 0x0a, 0xa4, 0x28, 0x25, 0x68, 0xa6, 0x20, 0x4d, 0x46, 0x10, 0xfa, 0x5d, 0x80, 0x0b, 0xb6, + 0xef, 0x53, 0xd0, 0x48, 0x6e, 0xe8, 0x8c, 0x2b, 0x6f, 0x94, 0xa9, 0x28, 0x0d, 0x75, 0x97, 0x8e, + 0x08, 0xf3, 0x1e, 0xab, 0x43, 0x19, 0x5d, 0x4d, 0x2a, 0x0d, 0x65, 0x70, 0xd4, 0x3c, 0xc3, 0xd4, + 0x63, 0x99, 0x4b, 0x89, 0x13, 0x1c, 0x95, 0xea, 0x4c, 0xc9, 0xca, 0x6d, 0xcb, 0x76, 0xc0, 0x36, + 0xb8, 0xb7, 0xca, 0xdd, 0xc2, 0x3d, 0x0c, 0x5a, 0x0b, 0x43, 0xdf, 0x06, 0x6d, 0x4d, 0x53, 0xe8, + 0xaa, 0xa3, 0x61, 0x36, 0x74, 0x30, 0xcd, 0x7c, 0xb6, 0xd2, 0x18, 0xee, 0x6f, 0x22, 0xbe, 0xa4, + 0xe8, 0x1b, 0x3d, 0xa1, 0xb6, 0xbe, 0x7f, 0x37, 0xb6, 0xd4, 0xef, 0xdf, 0xc7, 0x5b, 0xaa, 0x08, + 0x44, 0x31, 0xf4, 0xa1, 0xcc, 0xdc, 0x76, 0x68, 0x32, 0x94, 0x79, 0x1c, 0x2c, 0xa5, 0xa2, 0xb2, + 0xed, 0x82, 0xaa, 0x7d, 0xff, 0x1e, 0x17, 0xe3, 0x13, 0x6f, 0xa2, 0x16, 0x01, 0x5a, 0x2d, 0x8a, + 0xa2, 0xd4, 0xe5, 0x80, 0xba, 0x01, 0x77, 0x1c, 0x4a, 0x1f, 0x23, 0xb9, 0xc0, 0x1f, 0xfe, 0xc4, + 0x82, 0x01, 0x22, 0xa4, 0x8d, 0xb4, 0x20, 0x91, 0x06, 0x8f, 0xb0, 0xe1, 0x71, 0x9c, 0x16, 0x48, + 0xaa, 0x67, 0x3c, 0x50, 0x12, 0x0c, 0x20, 0x76, 0x09, 0x2d, 0x3b, 0xc0, 0x7d, 0xda, 0xb6, 0x45, + 0x03, 0x02, 0xe1, 0xbf, 0x4f, 0xc0, 0xa2, 0x45, 0x1b, 0xb7, 0x7c, 0x3f, 0x54, 0x44, 0xcf, 0x5a, + 0x90, 0x7a, 0xb2, 0xe9, 0x47, 0xde, 0x77, 0x6a, 0xc8, 0x0e, 0x64, 0x1b, 0xd2, 0x98, 0x2b, 0xe5, + 0xa6, 0xde, 0x43, 0x92, 0xb0, 0xba, 0x5d, 0x70, 0xdf, 0x53, 0x71, 0xad, 0x8c, 0xce, 0xbc, 0x74, + 0x08, 0x4b, 0x95, 0x31, 0xf3, 0x0f, 0x5d, 0xad, 0xb2, 0xad, 0x46, 0x5d, 0xf7, 0x60, 0x22, 0x7f, + 0xe8, 0x20, 0x0f, 0xb1, 0xab, 0x33, 0x82, 0xd5, 0x37, 0xda, 0x6a, 0xc6, 0x56, 0x61, 0x94, 0xd6, + 0xf3, 0xe2, 0x04, 0x2b, 0x5a, 0x7a, 0xf3, 0x76, 0x74, 0x27, 0x3d, 0xe2, 0x25, 0xad, 0xde, 0x49, + 0x27, 0xec, 0x46, 0xbb, 0x93, 0x2e, 0xd9, 0x4d, 0x8e, 0x77, 0x4d, 0xf5, 0x51, 0x36, 0x2f, 0x85, + 0xfa, 0x31, 0x68, 0x1b, 0xb9, 0xe3, 0x78, 0xd0, 0x2b, 0xcd, 0x1a, 0x60, 0xa7, 0x75, 0x5a, 0xb5, + 0x3b, 0xa9, 0x78, 0x9f, 0x15, 0x06, 0x16, 0xab, 0xd4, 0x09, 0x20, 0x80, 0xe8, 0xa4, 0x52, 0x61, + 0xfa, 0x0f, 0xf1, 0x4f, 0x4d, 0x47, 0x34, 0xb0, 0x6c, 0xe2, 0xe8, 0xe1, 0x5f, 0x46, 0x3a, 0x65, + 0x64, 0xd4, 0x4c, 0xca, 0xc9, 0xc0, 0xbd, 0x28, 0x4e, 0x0f, 0xe5, 0xfe, 0x20, 0xb0, 0x52, 0xb7, + 0x2d, 0xe9, 0x51, 0x3a, 0x91, 0x2e, 0x25, 0x47, 0xa2, 0x52, 0x78, 0x87, 0xe5, 0x81, 0xe7, 0x87, + 0xa9, 0x14, 0x3c, 0x89, 0xfa, 0x36, 0xbd, 0x2d, 0xdc, 0x65, 0x42, 0xf8, 0xe1, 0x52, 0x7b, 0xaa, + 0xdf, 0xca, 0xb2, 0x7c, 0x78, 0x57, 0x3d, 0x5d, 0x82, 0xca, 0x23, 0x54, 0x3e, 0x82, 0xaa, 0xe1, + 0x0a, 0xd8, 0x05, 0xe5, 0x20, 0x5d, 0xe8, 0xa7, 0xd1, 0xc4, 0xa5, 0x7b, 0x5d, 0x91, 0xea, 0x48, + 0x30, 0x24, 0x0c, 0x9f, 0x99, 0x52, 0xa5, 0x5b, 0x17, 0x55, 0x0a, 0x76, 0x72, 0xc2, 0xf5, 0xd4, + 0xe9, 0x2d, 0xbd, 0x03, 0x71, 0x0b, 0x41, 0x14, 0xc1, 0xb0, 0x86, 0xb7, 0x40, 0x15, 0x0f, 0x2e, + 0x40, 0x93, 0x01, 0x5c, 0x72, 0x77, 0x7c, 0x6d, 0x75, 0x50, 0xed, 0xc0, 0x34, 0x7d, 0x11, 0x7e, + 0x76, 0xb7, 0x14, 0xa0, 0xf8, 0xae, 0x4e, 0x45, 0x09, 0x3a, 0xcb, 0xa8, 0x89, 0xae, 0x80, 0xa8, + 0x55, 0xec, 0x0a, 0xd6, 0xed, 0xbb, 0x77, 0xd8, 0x99, 0xae, 0xdb, 0x78, 0xa3, 0xc1, 0x8d, 0x07, + 0x6b, 0xb2, 0xa3, 0x03, 0x08, 0x74, 0xb9, 0xab, 0xeb, 0xb0, 0x8c, 0xdd, 0x0f, 0x02, 0x50, 0xb1, + 0xf6, 0x81, 0x82, 0x54, 0x09, 0x15, 0x41, 0xa8, 0xec, 0xe2, 0x4d, 0xba, 0x83, 0xbf, 0xe2, 0x94, + 0x91, 0x76, 0xd6, 0x20, 0xad, 0xea, 0x7a, 0xe7, 0xc7, 0x4d, 0x18, 0xe1, 0x59, 0x35, 0xa0, 0x57, + 0x43, 0xf4, 0x00, 0x47, 0x40, 0x9d, 0x71, 0x6d, 0xb0, 0xa5, 0x15, 0x0a, 0x50, 0xde, 0x47, 0xcd, + 0x21, 0x75, 0xb6, 0xe7, 0x68, 0x1b, 0xac, 0x07, 0x9b, 0xfd, 0x7a, 0x08, 0x1a, 0x88, 0x13, 0xa6, + 0x1c, 0x6f, 0x29, 0x70, 0xc8, 0xbd, 0xd3, 0x6f, 0x0d, 0xc9, 0x96, 0xbc, 0x3b, 0x54, 0xe8, 0x42, + 0x3a, 0x2a, 0x94, 0x7b, 0x46, 0x3f, 0x45, 0x81, 0x0d, 0x72, 0xe8, 0x35, 0x41, 0x1c, 0xdc, 0x6e, + 0x0a, 0x16, 0x91, 0xdc, 0x37, 0xcc, 0x26, 0x9e, 0x0f, 0x4c, 0x69, 0x92, 0xa0, 0x08, 0xa2, 0x28, + 0xdf, 0x7b, 0xb6, 0x9b, 0x12, 0x60, 0x36, 0xb5, 0xb4, 0x6e, 0xa6, 0xdb, 0xe9, 0x30, 0x6d, 0xcd, + 0x2d, 0x4d, 0x2d, 0x1d, 0x17, 0xdd, 0xa7, 0x75, 0x55, 0xba, 0xff, 0xd3, 0x01, 0xf9, 0xf9, 0xfe, + 0x9d, 0xea, 0x3a, 0x50, 0xf5, 0x43, 0xaa, 0xce, 0xc5, 0xa5, 0x26, 0x4a, 0xc8, 0x5d, 0xb1, 0x02, + 0x5d, 0x00, 0x8e, 0x8c, 0xcb, 0xd3, 0x29, 0x16, 0x2d, 0xf1, 0xb5, 0x1e, 0x0b, 0xfc, 0x8c, 0xbd, + 0xce, 0xcf, 0x1a, 0xb3, 0xc8, 0x88, 0xfd, 0x31, 0xa9, 0x83, 0x58, 0x4c, 0xef, 0xa6, 0xd3, 0x7f, + 0xaa, 0x33, 0x23, 0xc9, 0xb1, 0x71, 0x62, 0x63, 0xf4, 0x0f, 0x86, 0x11, 0x24, 0xf3, 0x99, 0x9c, + 0x9d, 0x36, 0x2f, 0x09, 0x8b, 0xb6, 0x20, 0xd8, 0xfa, 0x63, 0x82, 0x71, 0x46, 0xd4, 0x7b, 0xf6, + 0x3e, 0xf0, 0xdc, 0x6c, 0x10, 0xe2, 0x5b, 0xa9, 0x24, 0x63, 0x92, 0xf7, 0x7f, 0x4c, 0x9c, 0xe9, + 0x7b, 0x92, 0x69, 0x10, 0xa1, 0xce, 0x73, 0x81, 0x99, 0x4b, 0x30, 0xdc, 0x15, 0x62, 0xf4, 0xfb, + 0x8e, 0xdd, 0x66, 0x87, 0xf5, 0x58, 0x1b, 0xe1, 0x9f, 0x2a, 0xdd, 0x46, 0x21, 0xc3, 0x29, 0xff, + 0xc7, 0xdd, 0x3a, 0xd9, 0xfb, 0x7c, 0x49, 0xea, 0xa7, 0xc7, 0xc7, 0xb5, 0x93, 0x5d, 0x50, 0x38, + 0x03, 0x27, 0xb4, 0xfb, 0x0e, 0x25, 0x10, 0xd2, 0xf5, 0x0c, 0xd7, 0x0c, 0x88, 0xeb, 0x81, 0x69, + 0x1a, 0xf4, 0xfb, 0xb0, 0x3e, 0x40, 0xe1, 0xd8, 0x2e, 0xa9, 0x9d, 0x1d, 0x66, 0x21, 0x38, 0x09, + 0x58, 0x6a, 0x17, 0xf5, 0xce, 0xf6, 0x7f, 0x5c, 0x41, 0x1a, 0x40, 0x6f, 0xe4, 0xdd, 0x3b, 0x02, + 0x0a, 0x16, 0x3a, 0x76, 0xf0, 0xd9, 0x9d, 0x9a, 0xfa, 0x3f, 0x6f, 0xaf, 0x5c, 0xec, 0x8a, 0x25, + 0x80, 0x3b, 0xa8, 0xb8, 0x08, 0x04, 0xdd, 0xef, 0x43, 0x62, 0x38, 0x0e, 0x06, 0xe0, 0x63, 0x62, + 0x19, 0x43, 0xd4, 0x6e, 0xd0, 0x1f, 0x31, 0x69, 0x07, 0x7c, 0x69, 0x36, 0x08, 0xd3, 0x70, 0x3c, + 0x89, 0x08, 0xa3, 0x70, 0x3f, 0xc3, 0xeb, 0xf0, 0x62, 0x10, 0xa0, 0x8e, 0x0d, 0xa4, 0x60, 0x73, + 0x92, 0xc7, 0x46, 0xcf, 0xf9, 0x8f, 0xfb, 0x36, 0x43, 0xf0, 0x68, 0x34, 0x86, 0x23, 0x95, 0x18, + 0xf7, 0xaf, 0x78, 0xa6, 0xf4, 0x3f, 0x2e, 0x21, 0x6f, 0x79, 0x47, 0x34, 0xa8, 0xe0, 0x13, 0x21, + 0x7f, 0x4c, 0x58, 0x24, 0x12, 0x51, 0x92, 0x97, 0x11, 0xd2, 0xf1, 0x6d, 0x88, 0xaa, 0x9d, 0xf1, + 0x57, 0x96, 0x72, 0x67, 0x40, 0x27, 0xcb, 0x30, 0x03, 0xb6, 0xa3, 0xf2, 0xd5, 0x36, 0x79, 0xfd, + 0xd5, 0x72, 0x7d, 0x3c, 0xb2, 0xe7, 0x56, 0xc8, 0x76, 0x5c, 0x88, 0x03, 0x0e, 0x56, 0x41, 0x3a, + 0x9d, 0x05, 0x98, 0x5f, 0x60, 0x39, 0x97, 0xbf, 0x8e, 0x01, 0x4b, 0xf3, 0x95, 0xac, 0xc7, 0xd0, + 0x51, 0x4e, 0x1e, 0xa0, 0x00, 0x1d, 0xf2, 0xa1, 0x56, 0xe1, 0xe7, 0x21, 0xf0, 0x7e, 0x50, 0xc1, + 0x73, 0x0f, 0x78, 0x67, 0x56, 0x84, 0xbd, 0x8b, 0x8b, 0xd3, 0x8b, 0x37, 0x59, 0x97, 0xad, 0x60, + 0x02, 0xf6, 0xc7, 0x70, 0x81, 0x02, 0x0f, 0xae, 0x37, 0x72, 0xa3, 0x03, 0x09, 0xb2, 0x30, 0x5b, + 0x71, 0xc0, 0x7f, 0x90, 0x69, 0x81, 0x49, 0xf0, 0x57, 0x1d, 0x63, 0x2b, 0xa9, 0xc1, 0x2e, 0xd5, + 0x3e, 0x08, 0x5b, 0x27, 0xcd, 0x8c, 0xdd, 0x56, 0x6b, 0xfb, 0x53, 0xed, 0xe2, 0xe4, 0xf0, 0xe4, + 0xe0, 0xcd, 0x56, 0xb6, 0xb5, 0x4d, 0x2e, 0x67, 0x27, 0x4b, 0xdb, 0x63, 0x82, 0xf1, 0x17, 0xc8, + 0x40, 0xc8, 0xfc, 0x1a, 0x14, 0x02, 0x16, 0x49, 0xc9, 0x8b, 0x40, 0x29, 0xc3, 0xe9, 0x5b, 0x86, + 0x08, 0x62, 0x13, 0x90, 0x16, 0xc4, 0x0a, 0xc4, 0xee, 0xba, 0x1e, 0x04, 0x08, 0x00, 0xe6, 0x11, + 0xea, 0x32, 0x5b, 0x89, 0x52, 0x86, 0x3b, 0x0f, 0x20, 0x55, 0x04, 0x04, 0x15, 0x44, 0x9a, 0x0b, + 0x1e, 0x0d, 0x6c, 0x7c, 0xa9, 0x7b, 0x00, 0xa2, 0x86, 0x89, 0x9f, 0xe8, 0x20, 0x32, 0xdf, 0x39, + 0x9d, 0x09, 0x1d, 0x1f, 0x94, 0xd9, 0x52, 0x51, 0xfa, 0x2a, 0xdb, 0x2e, 0x44, 0x77, 0x8d, 0xcb, + 0xe3, 0x23, 0xbd, 0x23, 0x35, 0x16, 0x3d, 0x4d, 0x5d, 0x60, 0x87, 0xa1, 0xc1, 0x45, 0x02, 0x83, + 0xbb, 0xe3, 0x3d, 0x82, 0xe3, 0x7b, 0xc8, 0xcc, 0xe9, 0x74, 0xfa, 0x2b, 0x6e, 0xf4, 0xbc, 0x97, + 0x10, 0x6c, 0x97, 0xc1, 0xad, 0xaa, 0x1d, 0x87, 0x8b, 0xae, 0xce, 0xdd, 0xa9, 0x57, 0x3b, 0x52, + 0x55, 0x3b, 0xc2, 0x9d, 0x79, 0x14, 0x5b, 0xd1, 0x13, 0xf3, 0x36, 0x3e, 0x44, 0x5d, 0x71, 0x9f, + 0x24, 0x61, 0x8a, 0xe5, 0xcd, 0xf2, 0x5f, 0x0b, 0xcd, 0xc4, 0xca, 0x2b, 0x40, 0x59, 0x9f, 0x5c, + 0x75, 0x39, 0x49, 0x90, 0x64, 0xd3, 0x2c, 0x58, 0x0c, 0x4b, 0x4f, 0x25, 0x5b, 0x64, 0xe8, 0x5f, + 0x8e, 0x98, 0xd5, 0xaa, 0x11, 0x58, 0xe4, 0x16, 0x39, 0x60, 0xd9, 0x55, 0x65, 0xa6, 0x7d, 0x79, + 0x3c, 0x6a, 0xb0, 0xe8, 0x33, 0x2e, 0x33, 0xa0, 0xcc, 0xd8, 0x02, 0x47, 0x3a, 0x56, 0xc5, 0x16, + 0x58, 0x53, 0xfb, 0x2f, 0x9a, 0x36, 0xee, 0x24, 0x88, 0x11, 0xfd, 0x6e, 0x2b, 0x25, 0xa4, 0x2d, + 0xb0, 0x94, 0xe8, 0x50, 0xe1, 0x9d, 0x3a, 0xbb, 0xd3, 0xe0, 0x4e, 0x64, 0x3e, 0x2d, 0x42, 0xa9, + 0x1a, 0x1e, 0xb0, 0x67, 0x7f, 0xa2, 0x50, 0x75, 0x65, 0x74, 0x10, 0x9b, 0x2c, 0x8d, 0xe4, 0x4b, + 0xfc, 0xe9, 0x02, 0x64, 0x31, 0x65, 0xfc, 0xe5, 0x48, 0x36, 0xfc, 0xc1, 0x3f, 0x11, 0xca, 0x31, + 0xc9, 0xf5, 0x40, 0x39, 0x9c, 0xf0, 0xb6, 0xcc, 0xfe, 0x13, 0xa0, 0x1c, 0xb5, 0x0d, 0x9b, 0x18, + 0x98, 0x9b, 0x18, 0x6a, 0x5d, 0x7b, 0x3c, 0x8f, 0xae, 0x0b, 0xf8, 0xbe, 0x04, 0x61, 0x67, 0xd5, + 0x05, 0x29, 0x39, 0xb2, 0x09, 0x4f, 0xc8, 0xdc, 0x1a, 0xe6, 0x84, 0x75, 0x21, 0x4a, 0x0a, 0x47, + 0x85, 0x3b, 0x06, 0xb8, 0xfd, 0x30, 0x8c, 0x2e, 0x44, 0x9b, 0x0b, 0x51, 0x53, 0x0c, 0xaa, 0x53, + 0x16, 0x38, 0x2c, 0x60, 0xcf, 0x61, 0xb4, 0xb4, 0x93, 0xd5, 0x70, 0x44, 0xbc, 0xf2, 0xf3, 0x97, + 0xbe, 0xee, 0xae, 0x7a, 0x7e, 0x49, 0xfe, 0x48, 0x0b, 0x5c, 0x10, 0x81, 0x18, 0x6d, 0x87, 0x1a, + 0x3e, 0xc3, 0xff, 0x05, 0xd8, 0x85, 0x3a, 0x3d, 0xc9, 0x61, 0xc0, 0xae, 0x3f, 0x48, 0x0c, 0xea, + 0x4b, 0x96, 0xa4, 0x88, 0x3f, 0xb3, 0x38, 0x90, 0xbb, 0x26, 0x1d, 0x1e, 0x7b, 0x26, 0x85, 0x00, + 0xa3, 0x3a, 0x57, 0x86, 0x3a, 0xac, 0x3c, 0xae, 0xff, 0x71, 0x97, 0x34, 0x0a, 0xb8, 0xa0, 0xf6, + 0xcc, 0xf0, 0x8d, 0x5e, 0xc0, 0x42, 0x88, 0xab, 0x8b, 0xa3, 0x26, 0x4c, 0xa1, 0x6d, 0xf1, 0xb2, + 0x14, 0x5f, 0x48, 0xf2, 0xac, 0x61, 0xc0, 0x2a, 0xc1, 0xb3, 0x9d, 0xc7, 0xb1, 0x18, 0x57, 0xa0, + 0xc3, 0x39, 0x0b, 0x10, 0xd0, 0x50, 0xce, 0x22, 0x84, 0x6d, 0xf0, 0x1f, 0x04, 0x96, 0x57, 0xd4, + 0x75, 0x96, 0xbb, 0x5b, 0x54, 0x07, 0xe0, 0xf3, 0xcf, 0xf1, 0x8b, 0xdc, 0x05, 0x6c, 0xcf, 0x9b, + 0xbc, 0xd1, 0xfb, 0xfe, 0x68, 0xa9, 0x41, 0x14, 0xc3, 0xb0, 0x0a, 0xbf, 0x8d, 0xee, 0x91, 0x8d, + 0x89, 0x1e, 0xdb, 0xdc, 0x61, 0xc9, 0xf4, 0x03, 0x28, 0xa0, 0xa2, 0x68, 0xf7, 0xa2, 0x28, 0x68, + 0x5d, 0x9c, 0xcd, 0xb2, 0x55, 0xcf, 0xa8, 0xa5, 0xd6, 0x6a, 0x8d, 0x10, 0x39, 0xd0, 0xcc, 0x39, + 0x5b, 0xa7, 0x90, 0xf7, 0x71, 0x13, 0xc4, 0xf4, 0x28, 0xb7, 0xfb, 0xfc, 0x3d, 0x52, 0xdc, 0x25, + 0x69, 0x51, 0x62, 0x10, 0xf6, 0x19, 0x0c, 0xae, 0x20, 0x99, 0x7e, 0xac, 0xa2, 0x7a, 0x4f, 0x68, + 0xc8, 0x90, 0xa5, 0xd4, 0x9e, 0xc1, 0x66, 0x1d, 0xa2, 0x8c, 0x32, 0x49, 0x9b, 0x04, 0x4b, 0x93, + 0x73, 0xfb, 0xdd, 0x3b, 0x64, 0xa9, 0x07, 0xc0, 0x8e, 0xd7, 0x4d, 0x09, 0x97, 0xa0, 0xc9, 0x03, + 0xe6, 0x0a, 0x92, 0xf7, 0x42, 0x1a, 0x1c, 0xd4, 0xf7, 0x68, 0x8e, 0x18, 0x8a, 0x11, 0x56, 0x7c, + 0xff, 0x21, 0xd2, 0xde, 0xe0, 0x13, 0x4e, 0x81, 0x78, 0x6b, 0xd8, 0x21, 0xf2, 0x7c, 0x5c, 0xb3, + 0x2b, 0x88, 0x20, 0x00, 0x2b, 0xd1, 0xa1, 0x94, 0x08, 0x00, 0xa3, 0x60, 0xe3, 0x55, 0x0d, 0x66, + 0x59, 0x2d, 0x31, 0x8a, 0x72, 0xc2, 0x66, 0xb7, 0x4a, 0xd7, 0x4f, 0x97, 0x26, 0x28, 0x06, 0x13, + 0xc6, 0x24, 0xc5, 0x12, 0x1c, 0x4b, 0x6d, 0x4c, 0xa7, 0x46, 0x30, 0x76, 0xdb, 0x64, 0x26, 0x9e, + 0x7d, 0x10, 0x74, 0xc6, 0xff, 0x60, 0x16, 0xfd, 0x46, 0x59, 0x61, 0xd7, 0x6c, 0x0e, 0xbb, 0x67, + 0x10, 0x89, 0xd3, 0xb5, 0x28, 0xf2, 0x0c, 0x9b, 0x18, 0x25, 0x4f, 0xde, 0xa8, 0x73, 0xd7, 0x16, + 0x5d, 0xaa, 0x99, 0x0f, 0x2a, 0x86, 0xfe, 0x78, 0xb2, 0x96, 0xfc, 0x20, 0x66, 0xeb, 0xcb, 0x23, + 0xb2, 0x8a, 0xd1, 0xfa, 0x03, 0x7f, 0x7f, 0x64, 0xd8, 0x21, 0xe9, 0x50, 0xf0, 0xb6, 0x52, 0xb1, + 0x0f, 0x23, 0xa4, 0x17, 0x33, 0xe3, 0x9c, 0x23, 0x69, 0x21, 0xe9, 0xce, 0x48, 0x93, 0x1e, 0x0d, + 0x2d, 0x0f, 0x7c, 0x0e, 0x74, 0x7f, 0x04, 0x09, 0x0f, 0x86, 0x53, 0x3f, 0x00, 0xe7, 0x39, 0xe9, + 0xd1, 0x40, 0x50, 0xb2, 0xe2, 0xd2, 0x4c, 0x25, 0x3c, 0x36, 0x5e, 0xa1, 0x53, 0x50, 0xad, 0xd1, + 0xf0, 0xa1, 0x8c, 0x35, 0x29, 0xb1, 0xba, 0x16, 0x69, 0x57, 0x9c, 0xb6, 0x31, 0x8c, 0xc6, 0x24, + 0x55, 0x5c, 0x4e, 0x7d, 0x1f, 0x03, 0x51, 0x11, 0x02, 0x53, 0x30, 0x9d, 0xe1, 0x87, 0xd4, 0x7a, + 0x42, 0xc6, 0x79, 0x48, 0x4c, 0x9c, 0x87, 0x97, 0x76, 0x8f, 0x7a, 0x03, 0x88, 0x19, 0x13, 0x39, + 0x09, 0xfa, 0x2a, 0x11, 0x01, 0x54, 0x55, 0x9a, 0x13, 0xc5, 0xca, 0x33, 0xc3, 0xcc, 0x73, 0xa0, + 0xbf, 0x34, 0xd0, 0xe2, 0x20, 0xd3, 0x95, 0x45, 0xb0, 0xa5, 0x82, 0x3f, 0x96, 0xd0, 0xa4, 0xc2, + 0xb1, 0xcd, 0x92, 0xea, 0x5f, 0x1b, 0x20, 0x60, 0x3c, 0xf1, 0x8f, 0xe9, 0xdc, 0xbd, 0x21, 0xf4, + 0x85, 0xb9, 0x5d, 0x0a, 0xc2, 0x0a, 0xec, 0xc3, 0xfd, 0x52, 0x18, 0x02, 0x65, 0x32, 0x05, 0x31, + 0x72, 0x24, 0x81, 0xb0, 0x76, 0x99, 0xd4, 0x61, 0x32, 0x0a, 0x28, 0x8e, 0x22, 0xc4, 0x99, 0xe0, + 0x1a, 0x43, 0xbb, 0x6b, 0x84, 0x9e, 0x0f, 0x86, 0xc4, 0xee, 0xb7, 0x3c, 0xc3, 0x37, 0xe5, 0x91, + 0x6f, 0x87, 0x94, 0xd9, 0xaa, 0xc8, 0x6f, 0x4e, 0xb0, 0x62, 0xde, 0xd2, 0x94, 0xe9, 0x23, 0x6d, + 0xd7, 0xb9, 0x9b, 0xcc, 0xb7, 0x1f, 0x84, 0x67, 0x79, 0x26, 0xec, 0x1b, 0x36, 0x26, 0x42, 0x40, + 0x45, 0x21, 0x20, 0x41, 0x13, 0x52, 0x21, 0xb0, 0xc8, 0x40, 0x03, 0x4c, 0x59, 0xfa, 0x7f, 0xdd, + 0x5c, 0x2c, 0x76, 0x7e, 0x0d, 0x14, 0xaa, 0xcb, 0xb6, 0x65, 0x5e, 0x06, 0xf9, 0xfc, 0x12, 0x08, + 0x26, 0xec, 0x5e, 0x00, 0x31, 0x5e, 0x84, 0x60, 0xc1, 0xe3, 0x4b, 0x03, 0x1d, 0x9d, 0xbc, 0x04, + 0xc2, 0x83, 0x9b, 0x17, 0x81, 0xae, 0x5e, 0x01, 0x73, 0xf2, 0x22, 0x0c, 0x13, 0xa4, 0x97, 0xe8, + 0x07, 0x2a, 0xee, 0x05, 0x10, 0xb4, 0x81, 0xab, 0x20, 0x98, 0xf3, 0x8b, 0x00, 0x30, 0xab, 0xfb, + 0x7c, 0x1f, 0x4c, 0x22, 0x03, 0xaf, 0xcf, 0xd2, 0xc1, 0x71, 0x4e, 0x93, 0xa5, 0x86, 0x93, 0x69, + 0xd2, 0x3b, 0x69, 0x66, 0xd8, 0x75, 0x00, 0x66, 0x59, 0x2f, 0x58, 0x47, 0xf2, 0xa3, 0x34, 0xb3, + 0xf1, 0x0b, 0xe5, 0x63, 0x89, 0xb9, 0x04, 0x20, 0x47, 0x33, 0x45, 0xbb, 0xb4, 0x11, 0xf6, 0x9a, + 0x85, 0x12, 0x6d, 0x96, 0x82, 0xd3, 0xa0, 0x2f, 0xbb, 0x20, 0x7d, 0xdf, 0x0b, 0x3d, 0x88, 0x3f, + 0x3e, 0x18, 0x0e, 0xf5, 0xc1, 0x9f, 0xff, 0x04, 0xcb, 0x98, 0x87, 0x26, 0x68, 0x6f, 0x87, 0x20, + 0xda, 0xec, 0xc4, 0xd0, 0xc8, 0x82, 0x38, 0x27, 0xa0, 0xfe, 0x10, 0x04, 0x1d, 0xdf, 0x01, 0x64, + 0x3a, 0x9b, 0xa4, 0x3c, 0x9f, 0xbf, 0xbd, 0x01, 0x96, 0xd0, 0x1f, 0xb8, 0xf3, 0x9a, 0x40, 0x14, + 0xc4, 0x4a, 0xd2, 0x5a, 0x4c, 0x23, 0xd1, 0xf2, 0x5c, 0x76, 0x86, 0x41, 0x67, 0x68, 0xe1, 0x2e, + 0x13, 0x73, 0x2a, 0x23, 0x0d, 0xab, 0xcf, 0x53, 0x17, 0x91, 0x0d, 0x53, 0x15, 0xe5, 0xaf, 0x59, + 0xc2, 0x7c, 0x5e, 0x0b, 0x3e, 0x7b, 0xa1, 0xc0, 0xf2, 0x89, 0xa1, 0xfe, 0xcf, 0xf2, 0xe7, 0x3a, + 0x36, 0x15, 0x93, 0x76, 0x25, 0xc2, 0x3e, 0xd6, 0xf1, 0xc7, 0x84, 0x4e, 0xff, 0x94, 0xd8, 0x8b, + 0x9e, 0xfc, 0x5e, 0xfc, 0xa7, 0xca, 0xba, 0xe1, 0xe6, 0x6e, 0xfe, 0x9a, 0x21, 0xf3, 0x10, 0xf5, + 0x70, 0xca, 0x84, 0x7b, 0x01, 0x49, 0x8e, 0x08, 0x14, 0x57, 0xdb, 0x47, 0x8b, 0xd8, 0xd2, 0x04, + 0xaa, 0xe1, 0x22, 0xaa, 0xb1, 0x72, 0xc9, 0x16, 0x54, 0x8d, 0xe1, 0xe9, 0xbe, 0x84, 0x67, 0x98, + 0xc0, 0x33, 0x64, 0x78, 0xd2, 0x67, 0x90, 0x74, 0xa7, 0xb8, 0xf7, 0xf9, 0x92, 0x24, 0xc6, 0xc9, + 0x52, 0x9e, 0x3b, 0x5a, 0xb7, 0x9b, 0x46, 0x97, 0x77, 0xd3, 0x68, 0x72, 0x37, 0x2d, 0x64, 0x9b, + 0x47, 0xd1, 0x86, 0x9a, 0x84, 0x11, 0xf9, 0x1b, 0x9d, 0xed, 0x3e, 0xf0, 0x89, 0xc5, 0x62, 0x19, + 0x99, 0x5d, 0x3c, 0xf3, 0xf2, 0x05, 0x5c, 0x0b, 0xee, 0x0e, 0xcc, 0x4f, 0xc0, 0x88, 0x2c, 0x36, + 0x3f, 0x63, 0x67, 0x53, 0xa2, 0xdd, 0xda, 0xc4, 0x61, 0x15, 0x5c, 0x73, 0x6c, 0x87, 0x19, 0xfd, + 0xcf, 0xb9, 0x13, 0x8c, 0x2d, 0xd9, 0xd9, 0x18, 0xca, 0x5e, 0x00, 0xa3, 0x32, 0x42, 0xc1, 0x4c, + 0x77, 0x69, 0xc7, 0x80, 0x40, 0x3a, 0x95, 0xec, 0x54, 0x66, 0xb2, 0x9e, 0x4a, 0xbc, 0x0b, 0x06, + 0x31, 0x6c, 0x77, 0x0f, 0x23, 0x96, 0x67, 0xda, 0x62, 0x16, 0x7b, 0x79, 0xd7, 0x10, 0xdb, 0x64, + 0x50, 0x78, 0x85, 0xa5, 0x8e, 0x4e, 0x87, 0xcf, 0xf5, 0x93, 0x84, 0xf3, 0xc0, 0x47, 0x35, 0x5f, + 0x37, 0x9c, 0x4f, 0x7b, 0x30, 0xce, 0xc2, 0x88, 0xd5, 0x41, 0x1f, 0x3f, 0xec, 0x76, 0xc6, 0x49, + 0x01, 0xa2, 0x83, 0x6b, 0x9f, 0x25, 0x1c, 0x3a, 0x30, 0x3f, 0x9c, 0x6a, 0x00, 0x81, 0x64, 0x62, + 0xc0, 0xd9, 0xec, 0xd9, 0x98, 0xcb, 0xad, 0xf9, 0xee, 0xfb, 0xba, 0x76, 0x4b, 0x90, 0x51, 0x1a, + 0x14, 0x44, 0x17, 0x43, 0x15, 0xf4, 0xba, 0x2f, 0x98, 0xcb, 0x53, 0x0d, 0xe3, 0xad, 0x0e, 0x26, + 0x48, 0x11, 0x8b, 0x58, 0x00, 0x80, 0xf8, 0x63, 0x2e, 0x43, 0x5a, 0x09, 0x24, 0xc0, 0x8d, 0x9c, + 0x4a, 0x58, 0x6d, 0x98, 0xb5, 0x00, 0x23, 0x2d, 0x08, 0x7d, 0x16, 0x5e, 0xd1, 0x5b, 0x13, 0x56, + 0x44, 0x2f, 0xec, 0x81, 0xbb, 0x13, 0x8b, 0xcf, 0x1a, 0xa1, 0x46, 0x42, 0x45, 0x01, 0xe8, 0x8c, + 0xb3, 0xe0, 0x0b, 0xfe, 0xb8, 0x01, 0x23, 0xac, 0x14, 0x33, 0xf0, 0x05, 0x70, 0xaf, 0x2f, 0x48, + 0x11, 0x0f, 0x7f, 0x08, 0x19, 0x69, 0xd5, 0x84, 0x74, 0x2e, 0x08, 0xe2, 0xf3, 0xeb, 0x71, 0xce, + 0xaf, 0xf9, 0x1e, 0xc6, 0x7f, 0x7b, 0x43, 0x7c, 0xc6, 0x87, 0x85, 0x53, 0x91, 0xaf, 0xd9, 0xc1, + 0x92, 0x50, 0xd1, 0xad, 0xdd, 0xc5, 0x72, 0x31, 0x98, 0x0b, 0xc1, 0xa6, 0x24, 0x9c, 0xbe, 0x0f, + 0xa9, 0x50, 0x5f, 0x72, 0x36, 0x01, 0x6e, 0x69, 0xab, 0x5c, 0x5a, 0x13, 0xba, 0x46, 0xa1, 0x09, + 0x78, 0xa4, 0x61, 0xb2, 0xbf, 0xa5, 0xc6, 0xee, 0x33, 0x6d, 0x01, 0x9b, 0xf5, 0xfe, 0x68, 0xb8, + 0x5a, 0x01, 0xe6, 0xdc, 0x8d, 0xf5, 0x57, 0x32, 0xf0, 0xa6, 0x3e, 0xac, 0x8e, 0x68, 0x37, 0xfa, + 0x94, 0xdb, 0x6f, 0xe6, 0x15, 0x82, 0x09, 0x4f, 0x46, 0x4b, 0x33, 0x16, 0xb2, 0x4d, 0xe0, 0xad, + 0xd8, 0xa5, 0xad, 0x86, 0x98, 0x0a, 0xe2, 0xaa, 0x1e, 0x77, 0x1e, 0xb9, 0x07, 0x20, 0x60, 0x1a, + 0x83, 0x5b, 0x75, 0x7a, 0x1b, 0x46, 0x1b, 0x9c, 0x51, 0x26, 0x85, 0x97, 0xe0, 0x1d, 0x14, 0xcc, + 0xfc, 0x00, 0x5e, 0xfa, 0x98, 0x28, 0x1a, 0xf3, 0xa2, 0x31, 0xf7, 0x3f, 0x40, 0x4c, 0x5d, 0xb3, + 0x8e, 0xec, 0x81, 0x30, 0x42, 0x52, 0x80, 0xfa, 0x21, 0x6e, 0x98, 0xce, 0xbc, 0x0c, 0x88, 0x1e, + 0x12, 0x2e, 0x86, 0xbb, 0xd6, 0xc1, 0x48, 0xf4, 0x2d, 0xae, 0xc4, 0x78, 0x89, 0x2d, 0x79, 0x9e, + 0x8a, 0x68, 0x0f, 0xf5, 0xb9, 0xab, 0x2e, 0xb5, 0x87, 0xf3, 0x40, 0x16, 0x1d, 0x66, 0x6e, 0x63, + 0x6e, 0xef, 0x22, 0x43, 0xe0, 0x3e, 0x13, 0x7f, 0xb5, 0x87, 0x8b, 0xd1, 0x56, 0x9c, 0xab, 0x4b, + 0x45, 0xae, 0x7a, 0x14, 0x2f, 0x61, 0x80, 0xdb, 0x8d, 0x37, 0x5f, 0x52, 0x13, 0xdb, 0xac, 0x40, + 0xa8, 0x6a, 0x4a, 0x2e, 0x5c, 0x5c, 0xe9, 0x31, 0xa8, 0xa0, 0x4d, 0x34, 0xfc, 0x50, 0x7a, 0xa4, + 0xec, 0xd6, 0xeb, 0x4b, 0xe3, 0x59, 0xe1, 0x8d, 0x34, 0x8e, 0x4b, 0x6f, 0xa6, 0xa2, 0xb8, 0xc4, + 0x27, 0x27, 0xc9, 0x27, 0xca, 0x37, 0x3b, 0x26, 0x7c, 0x8f, 0xde, 0x41, 0xf2, 0xc2, 0x28, 0xcc, + 0x71, 0x67, 0x0f, 0x2e, 0x6e, 0x7c, 0xda, 0xe8, 0xaf, 0xe1, 0x36, 0x68, 0x54, 0xcf, 0xd2, 0x76, + 0x8f, 0x1c, 0xe0, 0x91, 0x66, 0xf8, 0x35, 0x90, 0xc6, 0xbc, 0x64, 0x1c, 0x95, 0x8c, 0x03, 0x30, + 0x8a, 0xcf, 0x4a, 0x93, 0xb4, 0x72, 0x56, 0x20, 0x0a, 0xb5, 0xd7, 0xc5, 0xe0, 0x3c, 0x56, 0x7f, + 0x21, 0xf8, 0x7f, 0x45, 0xa4, 0xf6, 0x13, 0xf9, 0x86, 0x28, 0x64, 0xfb, 0x41, 0x78, 0xfa, 0x42, + 0x67, 0x2f, 0xc6, 0xa7, 0x3f, 0x8f, 0xcc, 0x3a, 0x9a, 0x71, 0xd2, 0x3c, 0x9b, 0xb7, 0x60, 0x7b, + 0x8b, 0xff, 0x15, 0x4c, 0x57, 0x42, 0xea, 0x5f, 0xc0, 0xf6, 0x39, 0x25, 0xc4, 0x52, 0x22, 0xc8, + 0x84, 0xb9, 0x24, 0xdf, 0xde, 0x81, 0x42, 0x54, 0xaa, 0xee, 0x16, 0xad, 0xba, 0x20, 0xc7, 0xe1, + 0x82, 0x1c, 0xbb, 0x5c, 0x82, 0x85, 0x66, 0xfc, 0xb6, 0x03, 0x0a, 0x30, 0xc8, 0xaf, 0x0b, 0x12, + 0x19, 0xbd, 0x1b, 0x1f, 0x4e, 0xff, 0xef, 0x5d, 0xc7, 0x29, 0x3f, 0x09, 0x10, 0xbd, 0xb3, 0x80, + 0xbe, 0x82, 0xbe, 0x76, 0xe2, 0xaa, 0xf2, 0xec, 0x42, 0x4a, 0xb4, 0x8d, 0x4e, 0x10, 0x26, 0x54, + 0xf4, 0x7b, 0xf6, 0xea, 0x80, 0x6d, 0xea, 0x8b, 0x87, 0x8b, 0xc8, 0x8f, 0xdf, 0xab, 0x5f, 0xfc, + 0xc4, 0xd3, 0x0b, 0xaf, 0x16, 0x2c, 0xe8, 0xc8, 0xf8, 0x45, 0x02, 0x18, 0x11, 0xa4, 0xe5, 0xd9, + 0x8f, 0xfb, 0x14, 0xe5, 0x02, 0xd1, 0x94, 0xf3, 0xbc, 0xac, 0x69, 0x70, 0x25, 0x9a, 0x5c, 0xc4, + 0xaf, 0xeb, 0xb0, 0x6f, 0xe4, 0x10, 0xb5, 0x28, 0x97, 0x0b, 0x78, 0xcd, 0xcb, 0x85, 0x32, 0x5e, + 0xa1, 0x36, 0x07, 0x20, 0x6a, 0x89, 0xa8, 0xaa, 0xac, 0x92, 0x9c, 0x9c, 0x2b, 0x90, 0x4d, 0xfc, + 0xf0, 0x4f, 0x41, 0xd6, 0xf0, 0x4e, 0x2d, 0xc0, 0x5d, 0xb1, 0x4c, 0x4a, 0x58, 0x01, 0x3f, 0x65, + 0x78, 0x2c, 0xe5, 0xa0, 0x42, 0xc1, 0x6f, 0x03, 0x41, 0x07, 0x6a, 0x74, 0x91, 0xcb, 0x39, 0x7e, + 0xa7, 0xc9, 0x79, 0x2c, 0x2b, 0x6e, 0x12, 0x95, 0x7d, 0x33, 0xa8, 0x8c, 0xd7, 0xa2, 0xac, 0x5e, + 0x43, 0x8d, 0x5a, 0x38, 0x82, 0x91, 0x8b, 0x44, 0x55, 0xe4, 0xe2, 0x91, 0x0a, 0x57, 0xed, 0x48, + 0xd5, 0x00, 0xa9, 0xa3, 0x32, 0xde, 0x6e, 0xca, 0xf9, 0xa8, 0x86, 0x75, 0xa3, 0x16, 0xae, 0xa1, + 0xd9, 0x39, 0xe0, 0x00, 0xcd, 0xf3, 0x05, 0x52, 0x96, 0x15, 0x44, 0x61, 0x33, 0x4f, 0x4a, 0x30, + 0x3e, 0x4c, 0x07, 0x71, 0x6e, 0xc0, 0x6c, 0xcf, 0x0b, 0x58, 0xc3, 0x30, 0x51, 0x72, 0xd8, 0x52, + 0xc1, 0x17, 0x1c, 0xd4, 0x1c, 0x96, 0xe6, 0xd8, 0x54, 0xf1, 0x52, 0x90, 0x37, 0x0b, 0x11, 0x04, + 0xce, 0x0e, 0x5b, 0x94, 0x09, 0xd2, 0x4a, 0x2d, 0x37, 0x80, 0x3c, 0x85, 0x73, 0xfc, 0x82, 0x10, + 0x2b, 0xd4, 0x14, 0x59, 0x83, 0xbe, 0x4b, 0xf8, 0xab, 0x31, 0x9a, 0x41, 0x39, 0xde, 0x14, 0x64, + 0x7e, 0xcd, 0x23, 0x3a, 0x11, 0x54, 0x0e, 0xc9, 0x11, 0x35, 0xcd, 0x21, 0x9d, 0xf1, 0xda, 0x50, + 0x4b, 0xd7, 0xaa, 0x7a, 0xae, 0x22, 0xa2, 0x65, 0xec, 0x80, 0xa1, 0x5f, 0x2a, 0x32, 0x2c, 0x60, + 0x0e, 0xd8, 0x04, 0xe6, 0x72, 0x0d, 0xb4, 0x28, 0x9f, 0x73, 0x9e, 0x14, 0x58, 0x0f, 0x30, 0x3d, + 0x98, 0x97, 0xac, 0x01, 0xe4, 0x26, 0xd9, 0xc4, 0x1f, 0x15, 0x0f, 0xb7, 0xb1, 0x06, 0xaa, 0x0c, + 0xfc, 0x04, 0x3a, 0x23, 0xd9, 0x10, 0x11, 0x36, 0xc5, 0x52, 0x99, 0xdd, 0x14, 0xa2, 0x02, 0xc6, + 0x21, 0x00, 0x42, 0xea, 0x97, 0xf1, 0x17, 0xda, 0x62, 0x89, 0xc2, 0x31, 0xd3, 0x94, 0x63, 0xa4, + 0xb7, 0x0a, 0xb3, 0x4f, 0x7e, 0x1a, 0x89, 0xa5, 0xc1, 0x96, 0xe5, 0x3a, 0x7e, 0xd7, 0x21, 0xfa, + 0x92, 0x97, 0x40, 0x7e, 0xfe, 0x15, 0x99, 0xed, 0x97, 0x3e, 0x43, 0xb5, 0x29, 0x69, 0x6a, 0xa3, + 0x7c, 0x5d, 0x6a, 0xa8, 0x9b, 0xf8, 0x50, 0x68, 0x94, 0x6b, 0x9a, 0xa4, 0x45, 0xe7, 0x00, 0x8b, + 0x52, 0xe9, 0x5a, 0x53, 0x13, 0x05, 0x65, 0x49, 0x03, 0xca, 0x6e, 0x26, 0x4a, 0xf0, 0x4b, 0x3e, + 0xea, 0x75, 0x29, 0x51, 0x82, 0xbd, 0x1c, 0xab, 0x45, 0x49, 0x6d, 0xe4, 0x93, 0x70, 0x52, 0xee, + 0x5a, 0x2d, 0x35, 0xf2, 0xd7, 0xd0, 0xbe, 0x78, 0xad, 0xb2, 0xb9, 0x13, 0x3e, 0xf9, 0xe8, 0xb8, + 0x77, 0x9d, 0x25, 0xd4, 0x3c, 0x32, 0x4b, 0xe0, 0xbd, 0x7f, 0x3e, 0x0f, 0xf2, 0x2f, 0x52, 0x0a, + 0x97, 0x76, 0x94, 0xe2, 0xfe, 0x2f, 0xae, 0xef, 0x32, 0x2e, 0x84, 0x32, 0x2e, 0xae, 0x12, 0x3e, + 0xe6, 0x71, 0x7d, 0x43, 0x33, 0x58, 0x8d, 0x1a, 0xfe, 0xe4, 0x51, 0x28, 0xf3, 0xd8, 0xf7, 0x66, + 0x11, 0x00, 0xf1, 0x93, 0x5d, 0x9b, 0xb8, 0x1a, 0x4b, 0xff, 0x82, 0x7c, 0x36, 0xd4, 0xdc, 0x39, + 0xae, 0x7d, 0x5e, 0xa6, 0xca, 0x05, 0x5c, 0x04, 0xa8, 0x51, 0xd8, 0x27, 0xc2, 0x50, 0xc9, 0xb0, + 0x1b, 0x54, 0x26, 0xe5, 0x02, 0xd7, 0x18, 0xb0, 0x1a, 0x51, 0x7f, 0xe4, 0x50, 0x95, 0x6c, 0x32, + 0xad, 0x92, 0x8b, 0xb4, 0x0c, 0xd6, 0xa8, 0x4c, 0x23, 0x94, 0x0b, 0xd7, 0xff, 0xea, 0x32, 0x2f, + 0x23, 0x89, 0xb0, 0x21, 0x28, 0x27, 0x39, 0xcf, 0x86, 0x56, 0x80, 0xa8, 0x48, 0xc0, 0x22, 0x10, + 0x13, 0x2a, 0x8b, 0x00, 0x03, 0x15, 0xac, 0x36, 0x82, 0xff, 0x7d, 0xfa, 0x6b, 0xf3, 0x3a, 0x5a, + 0xd9, 0xb9, 0x35, 0xa2, 0xdd, 0x04, 0x81, 0x42, 0xd1, 0x36, 0xd9, 0xcb, 0x96, 0xef, 0xa5, 0xb5, + 0x1b, 0x77, 0x3f, 0xe1, 0x2d, 0x2d, 0x7c, 0xd5, 0x26, 0xfa, 0x8e, 0x0d, 0xbf, 0x6e, 0xb1, 0xd7, + 0x63, 0xb6, 0xff, 0x07, 0xa0, 0xe7, 0x12, 0xe4, 0xa6, 0x5e, 0x00, 0x00 +}; diff --git a/wled00/html_pxmagic.h b/wled00/html_pxmagic.h new file mode 100644 index 0000000000..f451f907c0 --- /dev/null +++ b/wled00/html_pxmagic.h @@ -0,0 +1,552 @@ +/* + * Binary array for the Web UI. + * gzip is used for smaller size and improved speeds. + * + * Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui + * to find out how to easily modify the web UI source! + */ + +// Autogenerated from wled00/data/pxmagic/pxmagic.htm, do not edit!! +const uint16_t PAGE_pxmagic_L = 8626; +const uint8_t PAGE_pxmagic[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x13, 0xcd, 0x3d, 0xdb, 0x76, 0xdb, 0x46, + 0x92, 0xef, 0xfc, 0x0a, 0x18, 0xf6, 0x38, 0x44, 0x04, 0x52, 0xa4, 0x6e, 0x51, 0x40, 0x41, 0x9a, + 0xc4, 0x76, 0x26, 0xd9, 0xe3, 0x49, 0xb2, 0xb1, 0xe6, 0x92, 0xa3, 0xd5, 0x89, 0x9b, 0x44, 0x93, + 0x44, 0x0c, 0xa2, 0x39, 0x40, 0x53, 0x12, 0x43, 0xf1, 0x83, 0xf6, 0x79, 0x3f, 0x61, 0x7e, 0x6c, + 0xab, 0xaa, 0xbb, 0x81, 0xc6, 0x85, 0xb2, 0x64, 0xef, 0xc3, 0x7a, 0x66, 0x04, 0xa0, 0xd1, 0x97, + 0xea, 0xba, 0x57, 0x75, 0x81, 0x73, 0xf6, 0xec, 0xf5, 0x4f, 0xaf, 0x2e, 0x7f, 0xfd, 0xf9, 0x8d, + 0x33, 0x97, 0x8b, 0xe4, 0xdc, 0x39, 0xc3, 0x8b, 0x93, 0xb0, 0x74, 0x16, 0xba, 0x3c, 0x75, 0xb1, + 0x81, 0xb3, 0x08, 0x2e, 0x0b, 0x2e, 0x99, 0x33, 0x99, 0xb3, 0x2c, 0xe7, 0x32, 0x74, 0xff, 0x76, + 0xf9, 0x5d, 0xef, 0xd4, 0x35, 0xcd, 0x9d, 0x94, 0x2d, 0x78, 0xe8, 0xde, 0xc4, 0xfc, 0x76, 0x29, + 0x32, 0xe9, 0x3a, 0x13, 0x91, 0x4a, 0x9e, 0x42, 0xbf, 0xdb, 0x38, 0x92, 0xf3, 0x30, 0xe2, 0x37, + 0xf1, 0x84, 0xf7, 0xe8, 0xc1, 0x8f, 0xd3, 0x58, 0xc6, 0x2c, 0xe9, 0xe5, 0x13, 0x96, 0xf0, 0x70, + 0x58, 0x9f, 0x84, 0xad, 0xe4, 0x5c, 0x64, 0xd6, 0x14, 0x7f, 0x66, 0xbf, 0x0b, 0xc9, 0xd2, 0x09, + 0x76, 0x94, 0xb1, 0x4c, 0xf8, 0xf9, 0xcf, 0xf1, 0x1d, 0x4f, 0x9c, 0xbf, 0xb2, 0x59, 0x3c, 0x71, + 0x2e, 0x85, 0x48, 0xce, 0xf6, 0x55, 0xbb, 0x73, 0x96, 0xcb, 0x35, 0x5c, 0x3b, 0x41, 0x26, 0x84, + 0xdc, 0xf4, 0x7a, 0x79, 0x4f, 0xce, 0x57, 0x8b, 0x71, 0xf0, 0x7c, 0x30, 0x18, 0x9c, 0x8c, 0xf0, + 0x79, 0xcc, 0x26, 0x1f, 0x66, 0x99, 0x58, 0xa5, 0x11, 0x35, 0x1e, 0x42, 0xa3, 0xb8, 0xe1, 0x59, + 0xc2, 0xd6, 0x41, 0x36, 0x1b, 0xb3, 0xee, 0xc0, 0x77, 0xf4, 0x7f, 0xfb, 0xc7, 0x1e, 0xbc, 0xb4, + 0xfb, 0x0f, 0x87, 0x43, 0x68, 0x91, 0xfc, 0x4e, 0x06, 0xcf, 0xc7, 0xe3, 0x31, 0xdc, 0xcf, 0x32, + 0xb6, 0xee, 0x45, 0x2c, 0xfb, 0x10, 0x3c, 0x3f, 0x38, 0x38, 0x30, 0x0d, 0x0b, 0x1e, 0xc5, 0xab, + 0x45, 0xf0, 0xfc, 0xf0, 0xf0, 0xd0, 0x34, 0x25, 0xf1, 0x6c, 0x0e, 0xa3, 0x38, 0xfd, 0xc3, 0x69, + 0x93, 0x15, 0x37, 0x03, 0x4f, 0x8f, 0x0e, 0x8f, 0x0f, 0x4c, 0x5b, 0x31, 0x76, 0xfc, 0xd5, 0xe0, + 0x74, 0x6c, 0x5a, 0xf5, 0xf0, 0xa3, 0x53, 0x86, 0x9b, 0x58, 0x4d, 0x26, 0x3c, 0xcf, 0xf5, 0xf0, + 0xc1, 0xe1, 0xd1, 0xd1, 0x60, 0x62, 0x35, 0x9b, 0x19, 0x8e, 0x8f, 0x4e, 0x27, 0x07, 0x53, 0xeb, + 0x85, 0x9e, 0xe4, 0x94, 0x4d, 0xbe, 0x3e, 0x40, 0x64, 0xf0, 0x2c, 0x13, 0x99, 0x9e, 0x85, 0x9d, + 0x0e, 0xd8, 0x80, 0x15, 0x8d, 0x66, 0x0e, 0xec, 0x79, 0x30, 0x2e, 0x9a, 0xf5, 0x0c, 0xd3, 0xe9, + 0xf1, 0xd7, 0xc7, 0xb8, 0x8b, 0x5b, 0x96, 0xa5, 0x71, 0x3a, 0xd3, 0x73, 0x44, 0xb0, 0xdc, 0xe0, + 0xc0, 0x6a, 0x36, 0xb3, 0xf0, 0xd3, 0xe3, 0x68, 0x70, 0x64, 0xbd, 0x30, 0xf3, 0x00, 0x84, 0x83, + 0x93, 0x6d, 0x10, 0xf4, 0x6e, 0xf9, 0xf8, 0x43, 0x2c, 0x81, 0x23, 0x32, 0x91, 0x24, 0x63, 0x96, + 0x6d, 0x88, 0x53, 0x82, 0x93, 0xe5, 0x5d, 0xdb, 0xdb, 0x9e, 0xcc, 0x80, 0x2a, 0x1b, 0x8b, 0x34, + 0x03, 0x67, 0xd0, 0xde, 0x11, 0xa9, 0x6f, 0x77, 0xbc, 0x61, 0x59, 0xb7, 0x60, 0x0b, 0x6f, 0x24, + 0x96, 0x6c, 0x12, 0xcb, 0x75, 0xd0, 0x3f, 0x18, 0x8d, 0x45, 0x16, 0xf1, 0xac, 0x97, 0x31, 0x80, + 0x39, 0x0f, 0x8e, 0x77, 0xad, 0x4c, 0xec, 0x34, 0x47, 0x96, 0x69, 0x9b, 0xb6, 0x6c, 0xf2, 0x60, + 0x78, 0xce, 0x13, 0x3e, 0x91, 0xb1, 0x48, 0x9b, 0x5d, 0x4b, 0xaa, 0x7a, 0xdb, 0x2f, 0x37, 0x53, + 0x60, 0xf5, 0xde, 0x94, 0x2d, 0xe2, 0x64, 0x1d, 0x7c, 0xcf, 0x93, 0x1b, 0x2e, 0xe3, 0x09, 0xf3, + 0xff, 0xce, 0xb3, 0x88, 0xa5, 0xcc, 0xcf, 0x59, 0x9a, 0xf7, 0x72, 0x9e, 0xc5, 0x53, 0x00, 0xf2, + 0xae, 0x97, 0xc7, 0x7f, 0x00, 0x0e, 0x03, 0x0d, 0x2f, 0xb4, 0x8c, 0x16, 0x2c, 0x9b, 0xc5, 0x69, + 0x30, 0x18, 0x2d, 0x59, 0x14, 0xe1, 0xbb, 0xc1, 0x76, 0x2c, 0xa2, 0xf5, 0x26, 0x8a, 0xf3, 0x25, + 0x32, 0xf6, 0x34, 0xe1, 0x77, 0xa3, 0xdf, 0x57, 0xb9, 0x8c, 0xa7, 0xeb, 0x9e, 0x16, 0xab, 0x60, + 0x02, 0x7f, 0x78, 0x36, 0x62, 0x00, 0x44, 0xda, 0x8b, 0x25, 0x5f, 0xe4, 0xa6, 0x69, 0x11, 0xa7, + 0xbd, 0x39, 0x27, 0x12, 0x0d, 0x07, 0x83, 0x9b, 0xf9, 0xa8, 0x09, 0xbd, 0xb5, 0xcd, 0x09, 0x4b, + 0x6f, 0x58, 0xae, 0x09, 0x06, 0xfd, 0xff, 0xb4, 0x9d, 0x8a, 0x6c, 0xb1, 0x51, 0x30, 0x01, 0x78, + 0x52, 0x8a, 0x45, 0x70, 0x30, 0x00, 0x7c, 0xe6, 0x0b, 0x96, 0x24, 0x05, 0x50, 0xe3, 0x44, 0x4c, + 0x3e, 0x8c, 0x68, 0xe7, 0xb7, 0x6a, 0xb1, 0xa3, 0xc1, 0xc0, 0x6c, 0xe5, 0x60, 0x79, 0xe7, 0x0c, + 0x1c, 0x20, 0xc2, 0x68, 0x22, 0x12, 0x91, 0xe9, 0x65, 0x4b, 0x49, 0xf2, 0xd4, 0x40, 0x40, 0x05, + 0x0f, 0x86, 0xd0, 0x19, 0xd6, 0x14, 0x00, 0xba, 0x05, 0x45, 0x81, 0x14, 0x07, 0xf4, 0x89, 0x70, + 0x10, 0x80, 0x51, 0x75, 0x69, 0x94, 0xe3, 0x1e, 0x6d, 0x5f, 0x6f, 0x7c, 0xcb, 0x36, 0xd4, 0x16, + 0xf1, 0x89, 0xc8, 0x18, 0xd2, 0x2d, 0x48, 0x45, 0xca, 0x2b, 0x20, 0x58, 0x74, 0xab, 0x81, 0x50, + 0xd9, 0xca, 0xc9, 0x60, 0xb0, 0x65, 0x41, 0x9c, 0x77, 0x15, 0xab, 0xf8, 0xc1, 0x54, 0x4c, 0x56, + 0xb9, 0x1f, 0x30, 0x60, 0x87, 0x1b, 0xee, 0x6d, 0x1a, 0x53, 0x2a, 0x51, 0xf1, 0xb6, 0xcf, 0x6f, + 0x13, 0x1e, 0xbd, 0x89, 0x62, 0xb9, 0x31, 0xb4, 0x3c, 0x02, 0x54, 0x9c, 0xc2, 0xf4, 0x0f, 0xb1, + 0x90, 0xde, 0x6c, 0x2f, 0xe1, 0x53, 0x89, 0x42, 0x53, 0xec, 0x34, 0x4e, 0x93, 0x38, 0xe5, 0x3d, + 0xb5, 0xe1, 0x2a, 0x87, 0x1f, 0xed, 0x46, 0xee, 0xb6, 0xbf, 0xe8, 0xfd, 0xc1, 0x33, 0xb1, 0x31, + 0x38, 0x7c, 0x16, 0x2f, 0x50, 0xad, 0xb3, 0x54, 0xe2, 0x2b, 0x45, 0xd3, 0x1a, 0x85, 0x87, 0x80, + 0xe0, 0x6a, 0x37, 0x29, 0x96, 0xa6, 0x0f, 0xdc, 0x36, 0x3a, 0x20, 0x1f, 0x32, 0x00, 0xae, 0x42, + 0xb4, 0x0a, 0xc7, 0xb6, 0xb0, 0xe6, 0x0e, 0x26, 0xc6, 0xee, 0xbd, 0x28, 0xce, 0x94, 0xb4, 0x05, + 0xb0, 0xad, 0xd5, 0x22, 0x55, 0x4b, 0x40, 0x07, 0xbd, 0x00, 0xb0, 0x75, 0xf7, 0xab, 0x13, 0x40, + 0xa5, 0x0f, 0x76, 0x67, 0xd2, 0xc5, 0xf5, 0x9c, 0x9e, 0x73, 0x04, 0x60, 0x79, 0x5e, 0xc1, 0x77, + 0xc8, 0xa7, 0xfd, 0x4c, 0xdc, 0x56, 0x65, 0x87, 0xe6, 0xbf, 0xcd, 0xd8, 0x12, 0x18, 0x02, 0x2f, + 0x0d, 0x38, 0x72, 0xd0, 0x23, 0x80, 0x66, 0x2e, 0x6f, 0x39, 0x4f, 0x47, 0xd6, 0xae, 0xd5, 0x84, + 0x0a, 0xa0, 0x0d, 0x4d, 0x33, 0x66, 0x79, 0x0c, 0xfb, 0x41, 0x10, 0x8e, 0x09, 0x02, 0x44, 0x8c, + 0x37, 0x5a, 0x8a, 0x3c, 0x26, 0xe0, 0x33, 0x9e, 0x30, 0x64, 0x12, 0x23, 0xcc, 0x3d, 0x45, 0x43, + 0xd2, 0x48, 0x7a, 0xa2, 0xde, 0x74, 0x05, 0x82, 0x64, 0xcd, 0x46, 0xb8, 0x7b, 0xdc, 0x0c, 0x68, + 0xcc, 0x01, 0xe7, 0xcd, 0xed, 0xd5, 0xd1, 0xd7, 0x86, 0x7e, 0x33, 0xa1, 0x2d, 0xd5, 0x7a, 0x46, + 0xa7, 0x3f, 0xce, 0x58, 0x1a, 0x55, 0x25, 0xf0, 0x4e, 0x59, 0x7c, 0xe8, 0x88, 0xe2, 0x57, 0xea, + 0x94, 0x3f, 0xd5, 0x44, 0x51, 0xac, 0x24, 0x41, 0x38, 0xd0, 0x3c, 0x0a, 0x0a, 0x2c, 0x61, 0x63, + 0x9e, 0x54, 0xc1, 0xac, 0x32, 0xdc, 0x71, 0x4d, 0xe4, 0xbe, 0x02, 0xed, 0x61, 0xf3, 0x33, 0x8a, + 0xb2, 0xd7, 0xb2, 0x89, 0x6d, 0x9c, 0x2e, 0x57, 0xf2, 0x4a, 0xae, 0x97, 0x3c, 0x4c, 0x41, 0x99, + 0xf3, 0xec, 0xda, 0xb7, 0x9a, 0x70, 0xd8, 0xb5, 0xaf, 0x34, 0xb7, 0x8f, 0x0f, 0x2c, 0xe3, 0xcc, + 0xde, 0x95, 0x91, 0x4b, 0x24, 0x5b, 0xdd, 0x68, 0x0c, 0x2a, 0x72, 0xda, 0x6b, 0x88, 0x97, 0x96, + 0x72, 0xb3, 0xcb, 0x21, 0x88, 0x76, 0x2e, 0x92, 0x38, 0x72, 0x5a, 0xfa, 0x94, 0x38, 0x79, 0x84, + 0x0a, 0x04, 0x61, 0xb6, 0xf7, 0x45, 0x23, 0xae, 0x35, 0xd4, 0x87, 0x07, 0x25, 0xee, 0xe9, 0x7e, + 0xb2, 0xca, 0x72, 0x98, 0x70, 0x29, 0xe2, 0x0a, 0x51, 0x35, 0x97, 0x00, 0x50, 0xe5, 0xda, 0xdb, + 0x3e, 0xcd, 0xda, 0xc3, 0x0d, 0x2d, 0x3f, 0xd1, 0x9e, 0x54, 0xe6, 0x70, 0xe8, 0x1e, 0xc4, 0x48, + 0x76, 0x15, 0xa8, 0xc0, 0x34, 0x33, 0x7e, 0xed, 0x6d, 0xaa, 0x98, 0x3c, 0x25, 0xfd, 0x3f, 0x40, + 0xc5, 0x57, 0x1d, 0xae, 0x1f, 0x22, 0x0e, 0x26, 0x39, 0x5e, 0x92, 0x6d, 0x6d, 0xe5, 0xb8, 0xc3, + 0x53, 0x6b, 0xd3, 0xa7, 0x96, 0x46, 0x7c, 0xa2, 0x2d, 0x6c, 0xe0, 0x1e, 0x5d, 0x1d, 0xaf, 0xa9, + 0x8c, 0x6d, 0xba, 0x54, 0xb7, 0x42, 0x9b, 0xa0, 0xff, 0x0d, 0x1e, 0xa4, 0x7b, 0x75, 0x30, 0x69, + 0xf2, 0x41, 0x8d, 0xc4, 0x23, 0x52, 0xe6, 0x46, 0x8c, 0x4e, 0x1a, 0xb8, 0xc9, 0xff, 0xb5, 0x02, + 0x6e, 0x6d, 0xa2, 0xb2, 0x54, 0xbb, 0x15, 0x43, 0x31, 0x1c, 0xec, 0x98, 0x41, 0x11, 0x69, 0xd3, + 0x30, 0x90, 0xa3, 0xaa, 0xc7, 0x55, 0xba, 0x1b, 0x85, 0xd8, 0xee, 0xc0, 0xd7, 0xb6, 0x10, 0xa4, + 0x8c, 0xd3, 0x6e, 0xc8, 0xae, 0x5a, 0x7e, 0x86, 0x52, 0x10, 0x0d, 0xc0, 0x47, 0x68, 0x3c, 0xa7, + 0x89, 0xb8, 0xed, 0xdd, 0x05, 0xf3, 0x38, 0x8a, 0x38, 0x2a, 0x75, 0xa0, 0x9d, 0x58, 0xf4, 0x94, + 0x84, 0x6e, 0x1a, 0x4a, 0xaf, 0xd6, 0xc1, 0xd1, 0xfd, 0xd8, 0x72, 0xc9, 0x19, 0xf0, 0xda, 0x44, + 0xaf, 0x6d, 0x3c, 0xbb, 0x46, 0xfb, 0x42, 0xfc, 0xd1, 0x68, 0xb4, 0x44, 0x3a, 0x5e, 0xb0, 0x99, + 0x6e, 0x35, 0x72, 0x93, 0xe9, 0x1d, 0x34, 0x24, 0xab, 0x0e, 0x0a, 0xa9, 0xb4, 0x20, 0x60, 0x53, + 0xf4, 0x54, 0x0c, 0xeb, 0xb9, 0x6e, 0xa9, 0xb8, 0xd9, 0x18, 0xb8, 0x62, 0x25, 0xf9, 0x08, 0x2d, + 0x47, 0x61, 0x21, 0xf6, 0x9c, 0x13, 0x34, 0x10, 0x59, 0x41, 0xf5, 0x11, 0xb8, 0xbf, 0x69, 0x8e, + 0x5e, 0x16, 0x84, 0x39, 0x92, 0x49, 0xde, 0x1d, 0x1e, 0x1e, 0x47, 0x7c, 0xe6, 0x8d, 0x0a, 0xaf, + 0xd9, 0xb0, 0xfe, 0x49, 0x89, 0x55, 0xb2, 0x46, 0x1f, 0x67, 0x3c, 0xbd, 0x9b, 0x07, 0x3a, 0xea, + 0xdd, 0xf5, 0xf8, 0x0d, 0xc0, 0x9f, 0x13, 0x2e, 0xb6, 0xfd, 0x28, 0x13, 0xcb, 0x3f, 0xe0, 0xce, + 0x96, 0x46, 0x8b, 0xdb, 0x23, 0x96, 0xcf, 0x79, 0xeb, 0xaa, 0x0f, 0x68, 0x4b, 0x25, 0x6a, 0xbb, + 0xb4, 0x5f, 0x93, 0x3b, 0x0b, 0x8f, 0x09, 0x48, 0xe1, 0x0c, 0xdb, 0x19, 0xaa, 0x66, 0x9a, 0x15, + 0x2a, 0x35, 0xf6, 0x93, 0xc4, 0xe9, 0x1f, 0xe7, 0x0e, 0x67, 0x39, 0x07, 0x65, 0xd8, 0x03, 0x25, + 0x58, 0xee, 0x4b, 0xbb, 0xfc, 0x35, 0xd5, 0xf9, 0x51, 0xed, 0xd0, 0xdb, 0x05, 0xbd, 0x86, 0x6c, + 0x87, 0xb8, 0x14, 0xcb, 0xc2, 0x0d, 0x9b, 0xd5, 0x82, 0x8d, 0x9d, 0x66, 0x05, 0x1c, 0x17, 0x54, + 0xa6, 0xbd, 0x1c, 0xe8, 0x06, 0x23, 0x1e, 0xe0, 0xe2, 0x5d, 0x50, 0x69, 0xae, 0x41, 0x44, 0x35, + 0xe8, 0x68, 0xb0, 0x48, 0x88, 0x2d, 0x8d, 0x93, 0x31, 0xca, 0x68, 0xc7, 0x83, 0x21, 0xb9, 0x18, + 0x36, 0x14, 0x10, 0x43, 0xa1, 0x44, 0xa9, 0x26, 0x0a, 0x9e, 0xfc, 0xfa, 0x7b, 0x13, 0x63, 0xd1, + 0xb3, 0x8e, 0xd8, 0xea, 0xb0, 0x5b, 0x3a, 0xcf, 0x40, 0x76, 0xb2, 0xdb, 0xd6, 0xda, 0x9e, 0x71, + 0xdd, 0x42, 0xff, 0xa9, 0x6e, 0xfe, 0x0a, 0x8f, 0xa3, 0x9f, 0xdf, 0xc6, 0x72, 0x32, 0x6f, 0xaa, + 0x94, 0x76, 0x6f, 0xba, 0x69, 0x67, 0x94, 0x3b, 0xa4, 0x66, 0xd1, 0x2a, 0xb4, 0xc4, 0x93, 0x99, + 0x43, 0x89, 0x8b, 0xea, 0x64, 0x28, 0xd5, 0x54, 0x00, 0x35, 0x18, 0x91, 0x5d, 0x07, 0x23, 0x6d, + 0x13, 0x94, 0x84, 0xa2, 0xce, 0x25, 0x3f, 0x68, 0xf0, 0x04, 0x8f, 0xc3, 0xa0, 0xe1, 0xf0, 0xa8, + 0xca, 0xfa, 0xfd, 0xa3, 0xbc, 0x06, 0x53, 0x30, 0xe6, 0xa0, 0x60, 0x78, 0x1b, 0x68, 0xa5, 0xe2, + 0x32, 0x54, 0x39, 0x2a, 0xa9, 0x42, 0x56, 0x0a, 0xe1, 0x3c, 0x24, 0xf1, 0x23, 0x08, 0x0f, 0xdb, + 0x28, 0xf5, 0x7c, 0x3a, 0x9d, 0xb6, 0x10, 0xa7, 0x06, 0x94, 0xf2, 0x16, 0x26, 0x73, 0x3e, 0xf9, + 0xc0, 0xa3, 0xbd, 0x1a, 0xda, 0x3e, 0x4e, 0x7d, 0x3d, 0x9e, 0x82, 0xaf, 0xc6, 0x68, 0x0c, 0xa5, + 0xe7, 0x2c, 0x12, 0xb7, 0x68, 0xcb, 0x1c, 0xd4, 0x52, 0xbb, 0xc6, 0xb7, 0xaf, 0x6f, 0x50, 0x54, + 0x6a, 0x63, 0xba, 0x03, 0x9e, 0xe1, 0xff, 0xec, 0x0e, 0x81, 0x2f, 0x20, 0x9a, 0x93, 0x82, 0xe5, + 0xb2, 0x57, 0x86, 0x3a, 0x05, 0x3a, 0xa7, 0xf1, 0x1d, 0x8f, 0x46, 0x96, 0x1f, 0x3d, 0xb2, 0xcc, + 0xc8, 0x1f, 0xa0, 0x7e, 0x22, 0x7e, 0x17, 0x7c, 0x0d, 0xff, 0xb6, 0x7d, 0x9a, 0x63, 0xf3, 0xb1, + 0xc8, 0x48, 0xe1, 0x1f, 0x63, 0xdd, 0x42, 0x0f, 0x9e, 0xa0, 0x1a, 0x3c, 0xa8, 0x2a, 0xbd, 0x1d, + 0x6a, 0xb1, 0x65, 0x0f, 0xbf, 0x76, 0x0f, 0x29, 0x30, 0x31, 0x29, 0x92, 0xc1, 0xe8, 0x26, 0xce, + 0xe3, 0x71, 0x9c, 0xe0, 0x83, 0x31, 0xc6, 0x04, 0x9b, 0xa3, 0x2e, 0x3d, 0xca, 0x39, 0x14, 0x21, + 0x02, 0x29, 0x84, 0xd3, 0x66, 0x5c, 0xdc, 0xe2, 0xa4, 0x27, 0x5c, 0xa2, 0x41, 0xc1, 0x20, 0x0a, + 0xe1, 0xee, 0x93, 0x16, 0xa1, 0x39, 0xfb, 0x3a, 0x77, 0xb5, 0x8b, 0xd6, 0xd5, 0x9c, 0x97, 0x67, + 0x46, 0x51, 0xb6, 0x6a, 0xd7, 0x18, 0x2b, 0x95, 0x55, 0x0c, 0xd0, 0x89, 0xa9, 0x5d, 0x43, 0x2a, + 0x79, 0x2b, 0x33, 0xa8, 0xb7, 0xcc, 0xc4, 0x2c, 0x43, 0xe0, 0x9a, 0x42, 0x42, 0x22, 0x70, 0x54, + 0x8a, 0x40, 0x29, 0x21, 0x76, 0xec, 0x89, 0x3c, 0x52, 0x38, 0xa9, 0x15, 0x2a, 0x50, 0x6a, 0xf4, + 0x9f, 0xdd, 0x81, 0x57, 0xb6, 0xf5, 0x04, 0x70, 0x08, 0x84, 0xa8, 0x38, 0x75, 0x93, 0x82, 0x35, + 0x7c, 0x39, 0x75, 0x10, 0x2d, 0xcf, 0x0d, 0x15, 0x12, 0xcb, 0x50, 0x3d, 0x44, 0x31, 0xf0, 0x4e, + 0x57, 0x0a, 0x87, 0x78, 0xcf, 0xaf, 0xa2, 0x54, 0xed, 0xd5, 0x6f, 0xc5, 0x73, 0x15, 0xd1, 0x9f, + 0xb1, 0x98, 0x4d, 0x8b, 0x4a, 0x53, 0x7d, 0x21, 0x4d, 0x81, 0xcf, 0x58, 0xaa, 0x4a, 0xc3, 0x5a, + 0x63, 0xb9, 0xdc, 0x84, 0xc1, 0x6c, 0x79, 0x3d, 0xf4, 0xb4, 0x43, 0x57, 0xcb, 0x32, 0xd6, 0xfd, + 0xbd, 0xf1, 0x0a, 0xa8, 0x9d, 0xb6, 0xf8, 0x40, 0x83, 0x4a, 0xd8, 0xe8, 0x0c, 0x4f, 0xdb, 0x63, + 0xc7, 0xa6, 0x68, 0xd4, 0x0c, 0x41, 0x33, 0xe7, 0xb2, 0x23, 0x14, 0x79, 0x4c, 0xa4, 0xa9, 0x9c, + 0x95, 0x87, 0x3d, 0xa0, 0x7a, 0x04, 0x52, 0x4f, 0x72, 0xe9, 0x2d, 0xef, 0xca, 0x88, 0xda, 0x4e, + 0xd1, 0xc7, 0x63, 0xde, 0x62, 0xb6, 0x84, 0xb4, 0xe6, 0x3c, 0x4e, 0xa2, 0x5a, 0x96, 0xa9, 0x58, + 0x30, 0x7f, 0x4c, 0x06, 0xc3, 0xca, 0x49, 0x3e, 0xd7, 0x49, 0xfe, 0xba, 0x06, 0xae, 0x58, 0x56, + 0x8b, 0x6e, 0x36, 0xc1, 0x77, 0x28, 0x05, 0x3d, 0xa3, 0x57, 0x35, 0xed, 0x66, 0xa1, 0x7e, 0x22, + 0x18, 0x11, 0xbc, 0xe9, 0xef, 0x57, 0x73, 0x1f, 0x1f, 0xf1, 0xfe, 0x7b, 0xce, 0xf0, 0x6b, 0x54, + 0x13, 0x04, 0x62, 0xa3, 0x55, 0xa7, 0x57, 0x2c, 0x8f, 0xff, 0xa0, 0x74, 0xf9, 0xc9, 0x00, 0x7c, + 0xc4, 0xdd, 0x87, 0xb5, 0x76, 0x39, 0xde, 0x4d, 0xe3, 0xcc, 0x52, 0x08, 0x7f, 0x08, 0xd6, 0x7c, + 0x19, 0xa7, 0xce, 0x30, 0x77, 0x94, 0xcc, 0x81, 0xb3, 0x33, 0xc5, 0x43, 0x1d, 0xd8, 0x3d, 0x50, + 0x00, 0xc2, 0x3c, 0xc9, 0xa3, 0x1f, 0x30, 0x50, 0xda, 0x54, 0xbc, 0xc3, 0x03, 0x1d, 0x6f, 0xde, + 0x80, 0xf9, 0x8a, 0x36, 0x3b, 0xf8, 0xa1, 0x3c, 0x94, 0xf0, 0xec, 0x6c, 0xa1, 0xd1, 0x0a, 0x79, + 0x8e, 0xd3, 0x56, 0x31, 0xd8, 0x54, 0xee, 0x6a, 0x03, 0x55, 0x53, 0xd4, 0xc2, 0xbd, 0xf5, 0x24, + 0xf2, 0x9f, 0x91, 0x0f, 0x99, 0xd3, 0x2d, 0xd3, 0x08, 0x5f, 0x9d, 0x7c, 0x05, 0x68, 0xde, 0x50, + 0x46, 0xb0, 0x4c, 0x02, 0x52, 0x0a, 0xb0, 0x9d, 0xe5, 0x4c, 0xc6, 0xd4, 0xa4, 0xea, 0xfc, 0x07, + 0x53, 0x76, 0xf5, 0x68, 0xa4, 0xcc, 0xdf, 0x6f, 0xff, 0xfc, 0x81, 0xaf, 0xa7, 0x19, 0x83, 0x1d, + 0x3b, 0x88, 0xec, 0x8d, 0x14, 0x9b, 0x46, 0xd8, 0x77, 0x78, 0x32, 0xc0, 0xb0, 0xaf, 0xd2, 0xb9, + 0xd0, 0x8c, 0x95, 0x01, 0xda, 0x9e, 0x0c, 0xab, 0x7d, 0xa7, 0x2c, 0xe2, 0x3f, 0xa4, 0x9b, 0xe3, + 0x3f, 0x6d, 0x8c, 0x91, 0x1f, 0xda, 0x46, 0x9e, 0x6e, 0x13, 0xde, 0xee, 0x1c, 0x0c, 0xbc, 0xed, + 0xd7, 0x95, 0x81, 0xbb, 0x7a, 0x6d, 0x3b, 0x67, 0xfb, 0xea, 0x54, 0xce, 0x39, 0xdb, 0xd7, 0x47, + 0x89, 0xe8, 0x29, 0xc0, 0x25, 0x8a, 0x6f, 0x9c, 0x09, 0xc8, 0x7a, 0x1e, 0xba, 0x85, 0x8b, 0xe4, + 0x36, 0xdb, 0x41, 0x6c, 0xb0, 0x15, 0xe7, 0x76, 0x3a, 0x71, 0x14, 0xba, 0x78, 0xf7, 0x17, 0x0e, + 0x9d, 0x61, 0x0d, 0xd7, 0x49, 0x05, 0x31, 0x14, 0xdc, 0x57, 0x87, 0xaa, 0xbc, 0x24, 0x8e, 0x8c, + 0x17, 0x33, 0xa7, 0x93, 0x67, 0x93, 0xd0, 0x85, 0x5e, 0x2c, 0x80, 0xe0, 0x22, 0x89, 0x27, 0xc4, + 0xc8, 0xfb, 0x62, 0x22, 0x39, 0xb0, 0x80, 0x04, 0xa6, 0x5d, 0x80, 0xa0, 0xe7, 0xfc, 0xe4, 0xc8, + 0x77, 0x1d, 0x96, 0xc8, 0xd0, 0xad, 0x1f, 0x32, 0xba, 0x4e, 0x47, 0xcf, 0x4c, 0x99, 0x4e, 0x9c, + 0x78, 0x1f, 0x96, 0xab, 0x2e, 0x0a, 0x8c, 0xd2, 0xd8, 0x01, 0xd2, 0xdf, 0x75, 0x2c, 0x20, 0x29, + 0x11, 0xe0, 0x74, 0x60, 0x1b, 0x00, 0xa5, 0xc8, 0x25, 0x9e, 0x7b, 0xba, 0xe7, 0xdf, 0xeb, 0xbb, + 0xb3, 0x7d, 0x7a, 0x8f, 0x70, 0xa3, 0xf7, 0xe9, 0x50, 0x9a, 0xcc, 0x45, 0x73, 0x00, 0x7b, 0xa5, + 0x13, 0xd2, 0x62, 0x8c, 0x42, 0x47, 0xf9, 0x98, 0xf1, 0x7f, 0xad, 0x80, 0x23, 0xa3, 0x56, 0xd0, + 0x76, 0x02, 0x42, 0x70, 0x28, 0x18, 0x3a, 0x3f, 0x03, 0xef, 0x70, 0xe9, 0xfc, 0xf8, 0x28, 0x38, + 0xd4, 0xa2, 0x08, 0x82, 0xba, 0x83, 0x99, 0x57, 0xd0, 0xfc, 0x23, 0xbf, 0x75, 0xd4, 0x3c, 0x00, + 0x60, 0x13, 0xa4, 0x4f, 0x44, 0x1a, 0xbe, 0x35, 0x04, 0xa8, 0xe4, 0x55, 0xdc, 0xea, 0x46, 0x96, + 0x0c, 0x9d, 0xc9, 0xd4, 0x3d, 0xff, 0x59, 0xdd, 0x94, 0xdb, 0xd0, 0x69, 0x18, 0x7d, 0xcc, 0x6c, + 0xfa, 0x11, 0xfc, 0xc5, 0x83, 0x05, 0xae, 0xa0, 0xf4, 0xa2, 0xda, 0xd4, 0xf9, 0x3b, 0x35, 0x16, + 0x0f, 0xc0, 0x45, 0x3c, 0x01, 0xdc, 0xa8, 0xb7, 0xe7, 0x4e, 0xa7, 0xd2, 0x2f, 0x74, 0x87, 0xae, + 0x43, 0x67, 0xd1, 0xa1, 0x7b, 0xf5, 0xc5, 0x94, 0xfe, 0x7d, 0x71, 0xed, 0x9e, 0xff, 0x90, 0x02, + 0xf8, 0x71, 0xb4, 0x62, 0x49, 0x39, 0xb2, 0x36, 0xf0, 0x00, 0x90, 0x65, 0x46, 0x0e, 0x7c, 0xa7, + 0x3a, 0x98, 0xdf, 0xed, 0x1c, 0x77, 0x58, 0x2e, 0x08, 0xc3, 0x8e, 0xed, 0x91, 0xc0, 0xf4, 0x04, + 0x36, 0x87, 0x1d, 0x9a, 0x3b, 0xc0, 0xd6, 0x2f, 0x18, 0x7f, 0x3b, 0xd6, 0x84, 0xfb, 0xea, 0x65, + 0x2b, 0x89, 0x3a, 0x0f, 0x53, 0xe4, 0x11, 0x04, 0x01, 0x2f, 0x02, 0xd8, 0xc7, 0x3d, 0xff, 0x89, + 0xae, 0x9d, 0x06, 0x3d, 0x14, 0x39, 0x74, 0x2f, 0xa2, 0x86, 0xb9, 0xdf, 0x45, 0x8c, 0xce, 0x4e, + 0x6a, 0xd4, 0x70, 0xf3, 0x7b, 0x2e, 0x00, 0xe4, 0x36, 0x24, 0xfc, 0xe3, 0xed, 0x9b, 0xd7, 0xce, + 0x7f, 0xbc, 0xfb, 0xe9, 0xc7, 0xce, 0xce, 0xc1, 0x73, 0x86, 0x32, 0xb9, 0xe0, 0xce, 0x37, 0x39, + 0x28, 0x6c, 0x34, 0x43, 0x3b, 0xbb, 0x82, 0x97, 0x96, 0xb8, 0xe7, 0xaf, 0xfe, 0xf6, 0xcb, 0xdb, + 0xce, 0x23, 0xd0, 0xda, 0x2a, 0x00, 0x8e, 0xd9, 0x74, 0x87, 0x94, 0x25, 0xa8, 0x2a, 0xcb, 0xab, + 0x70, 0x9f, 0x22, 0xc9, 0xaa, 0xd6, 0xc2, 0x3d, 0x7f, 0x4d, 0xd7, 0xce, 0xc7, 0xc5, 0x58, 0x0f, + 0x20, 0xd4, 0x9b, 0xfb, 0x76, 0x4d, 0xd2, 0x79, 0x14, 0x00, 0xab, 0x34, 0xfe, 0xd7, 0x8a, 0xff, + 0x00, 0x58, 0xfe, 0x1b, 0xdd, 0x39, 0x3f, 0x44, 0x75, 0x20, 0x3a, 0x4d, 0x28, 0x8a, 0x51, 0x04, + 0x47, 0xf9, 0xf4, 0x28, 0x9d, 0xd6, 0x69, 0x87, 0x64, 0x9a, 0x41, 0x24, 0x10, 0x25, 0xeb, 0x1f, + 0x49, 0xb9, 0x7d, 0xa7, 0x9f, 0x3e, 0xaa, 0xdd, 0xb4, 0x86, 0xa8, 0x8c, 0x26, 0xa8, 0xaa, 0x2d, + 0x8f, 0x57, 0x6d, 0x9d, 0x47, 0xe8, 0xb6, 0x47, 0x48, 0x52, 0xce, 0x67, 0x0b, 0xcc, 0xb9, 0xba, + 0x28, 0x01, 0x74, 0x5b, 0x41, 0x6d, 0x45, 0x9e, 0x8a, 0xbe, 0x04, 0x78, 0x39, 0xb2, 0xce, 0xba, + 0x03, 0xd8, 0x2e, 0x5a, 0x45, 0xe5, 0xf0, 0x80, 0xfe, 0x3a, 0x71, 0x1d, 0x7a, 0x56, 0xfe, 0x24, + 0x35, 0x9c, 0x3b, 0x66, 0xb9, 0xc1, 0x23, 0xd4, 0x46, 0xe7, 0xf1, 0xac, 0x3a, 0xa6, 0x00, 0x2d, + 0x05, 0x77, 0xc5, 0x3d, 0xff, 0xb6, 0xb8, 0x2f, 0x77, 0x64, 0xf3, 0x9c, 0x75, 0x4e, 0xe1, 0xd6, + 0xa8, 0x46, 0x09, 0x45, 0xc3, 0x47, 0xd6, 0x9c, 0xb4, 0x75, 0xfb, 0xb9, 0xb3, 0x88, 0x53, 0xda, + 0x32, 0x78, 0x78, 0xa0, 0x71, 0x8f, 0x8f, 0x0b, 0x9b, 0x35, 0x3c, 0x38, 0x75, 0x0b, 0x9a, 0x59, + 0xf9, 0x49, 0xb7, 0x15, 0x0a, 0xeb, 0xf8, 0xc9, 0x51, 0x47, 0x26, 0xee, 0x03, 0xf2, 0x55, 0x42, + 0xf0, 0x77, 0x5c, 0x4c, 0x5b, 0xed, 0x46, 0xab, 0x05, 0xc9, 0x2e, 0x95, 0xf1, 0xa9, 0xfc, 0x65, + 0xa1, 0xbc, 0xf0, 0xe1, 0xdd, 0xf3, 0x6f, 0xcc, 0x6d, 0x89, 0x70, 0xed, 0x99, 0xe8, 0x99, 0x54, + 0xba, 0xab, 0xbe, 0x35, 0xca, 0x86, 0x8d, 0xc5, 0x9d, 0xd9, 0x5e, 0x39, 0x23, 0xe1, 0xdb, 0x7a, + 0x54, 0x8c, 0xb5, 0x04, 0xf4, 0x60, 0x19, 0x97, 0xb5, 0x32, 0xf0, 0xea, 0x92, 0xa5, 0x4e, 0x65, + 0x99, 0x02, 0xe1, 0xc0, 0x57, 0xf0, 0x12, 0x77, 0x6b, 0xa0, 0x7a, 0x32, 0x5f, 0x91, 0x07, 0xaa, + 0xd6, 0xa5, 0x38, 0xc4, 0x3d, 0xbf, 0x2c, 0x5b, 0x1c, 0x6a, 0xea, 0xd4, 0x37, 0xfd, 0xa4, 0x3d, + 0x37, 0x16, 0x50, 0x34, 0x6d, 0x36, 0x57, 0x30, 0xd0, 0x04, 0x4b, 0x23, 0xa2, 0xf3, 0x34, 0x4c, + 0x3c, 0x8c, 0x88, 0x8e, 0x8d, 0x09, 0x75, 0xde, 0xa6, 0x57, 0xfb, 0x85, 0x1e, 0xd4, 0xfe, 0x1f, + 0xb9, 0xfd, 0x4e, 0xfb, 0xfe, 0xed, 0x69, 0x89, 0xea, 0x95, 0x86, 0xca, 0xa6, 0x2b, 0x6f, 0x3a, + 0x3a, 0x95, 0xaa, 0x67, 0x24, 0x77, 0xe4, 0x53, 0x78, 0xa1, 0x8d, 0x25, 0xd0, 0x88, 0x56, 0xb6, + 0xfb, 0x04, 0x9b, 0x49, 0xca, 0x0f, 0xd4, 0xea, 0x3f, 0xf0, 0xda, 0x6e, 0x1a, 0x54, 0xad, 0x80, + 0x41, 0x80, 0x1a, 0x40, 0x5b, 0xd7, 0xb7, 0x46, 0x7c, 0x4f, 0xdc, 0x4f, 0xe1, 0x58, 0xa5, 0x6d, + 0xc1, 0xe1, 0xa0, 0xeb, 0x0e, 0x73, 0x59, 0x05, 0x41, 0x0f, 0x21, 0x18, 0xcc, 0x7d, 0x0b, 0x10, + 0x6d, 0xf6, 0x1b, 0x51, 0x65, 0xc9, 0xe9, 0xa7, 0x7b, 0x1c, 0x1d, 0x63, 0x67, 0x31, 0x9e, 0x44, + 0x0b, 0x8b, 0xd7, 0xc7, 0xe0, 0x4f, 0x8f, 0xd0, 0x56, 0x55, 0xdd, 0x77, 0x34, 0xf4, 0x47, 0xee, + 0x27, 0xc4, 0x2f, 0xd1, 0x2a, 0xd3, 0xca, 0xe5, 0xb5, 0xbe, 0xeb, 0x54, 0xed, 0xc8, 0xc7, 0xcd, + 0x48, 0x15, 0xc2, 0x62, 0x42, 0x25, 0xda, 0xe5, 0xa3, 0x31, 0x9c, 0xfd, 0xe3, 0xd2, 0x05, 0x40, + 0x36, 0x57, 0x77, 0x60, 0x5b, 0xb4, 0x8d, 0xc9, 0x25, 0x5f, 0x62, 0xb7, 0x21, 0xce, 0x80, 0xcb, + 0x2c, 0x44, 0xa4, 0x56, 0xe1, 0x59, 0x3c, 0x71, 0xdb, 0xc0, 0xb2, 0xec, 0x8a, 0x11, 0x8c, 0xf3, + 0x9c, 0x4f, 0x4a, 0x29, 0xd0, 0x6c, 0xb5, 0xd3, 0x1c, 0x3c, 0x42, 0x2d, 0xc6, 0x6a, 0xf6, 0xcb, + 0xe2, 0xfe, 0x33, 0xf1, 0x64, 0x4d, 0x6a, 0x29, 0xc1, 0xb8, 0x86, 0xab, 0x83, 0xff, 0x27, 0xb8, + 0x6a, 0xf7, 0xbd, 0x9b, 0x7a, 0xbb, 0x5d, 0x24, 0x5a, 0x04, 0x9a, 0x12, 0x3b, 0xbb, 0x10, 0x4e, + 0x99, 0x29, 0x08, 0x0c, 0xe6, 0x42, 0xe4, 0x1c, 0xc3, 0x15, 0x7c, 0x2e, 0xd0, 0xdd, 0x39, 0xa3, + 0x8a, 0xc8, 0x73, 0xe7, 0x15, 0x36, 0x3b, 0x72, 0xce, 0xa4, 0x73, 0x1b, 0x27, 0x09, 0x60, 0x0a, + 0x16, 0x9d, 0x70, 0x68, 0xe1, 0x58, 0xd1, 0x9c, 0x89, 0x74, 0x76, 0x6e, 0x81, 0xe8, 0x2c, 0x31, + 0x33, 0x91, 0x63, 0x5a, 0x85, 0x5e, 0x75, 0x9c, 0x38, 0xa5, 0xbe, 0x54, 0x9c, 0x80, 0x7e, 0x99, + 0x9a, 0xb6, 0x6a, 0xc1, 0x08, 0x14, 0x4d, 0x33, 0xfd, 0x80, 0xd4, 0xd2, 0xb7, 0x46, 0xf6, 0x9e, + 0x0f, 0x06, 0xdf, 0x7e, 0xf7, 0xdd, 0x77, 0xee, 0xa7, 0x84, 0xeb, 0x1a, 0x15, 0x9d, 0xa7, 0x3b, + 0xb6, 0x04, 0x78, 0x5e, 0x90, 0xb1, 0x43, 0x44, 0xc8, 0x9d, 0xd5, 0x12, 0x93, 0xa8, 0x8e, 0x14, + 0x0e, 0x86, 0x6b, 0x05, 0x65, 0x99, 0x52, 0xbb, 0xba, 0x50, 0xd2, 0x75, 0xe6, 0x19, 0x9f, 0x82, + 0x0a, 0x94, 0x72, 0x19, 0xec, 0xef, 0x5f, 0x61, 0x7b, 0x2f, 0x5e, 0x5e, 0xef, 0x73, 0x7a, 0xd9, + 0x91, 0x2c, 0x9b, 0x61, 0xc1, 0xfa, 0x6f, 0xe3, 0x84, 0xa5, 0x1f, 0x60, 0x0d, 0x3d, 0xeb, 0xd9, + 0x3e, 0xb3, 0xed, 0x4a, 0xc5, 0x79, 0xd6, 0xf0, 0xd0, 0x3a, 0x25, 0x6c, 0xca, 0xf1, 0xed, 0x54, + 0x33, 0x02, 0xb1, 0x32, 0xa6, 0x4f, 0x09, 0xfa, 0x48, 0x9d, 0xe8, 0x03, 0xfe, 0x32, 0xa9, 0x54, + 0xb6, 0x58, 0xf8, 0x5b, 0x56, 0x3a, 0xbf, 0x45, 0x58, 0xc1, 0x4a, 0x39, 0xaf, 0x33, 0x36, 0x03, + 0x05, 0x1e, 0x39, 0xf8, 0x06, 0xf8, 0x6a, 0x1a, 0x27, 0xdc, 0x99, 0xf3, 0x8c, 0x3b, 0xc0, 0x47, + 0x93, 0x24, 0x9e, 0x7c, 0x40, 0xa4, 0xe5, 0x26, 0x4a, 0x4e, 0xc4, 0x84, 0x25, 0xaa, 0xd3, 0xd9, + 0xfe, 0xb2, 0x6e, 0x55, 0xb0, 0xdd, 0x30, 0x46, 0x2e, 0x56, 0x99, 0x09, 0x05, 0xcd, 0x7d, 0x87, + 0x4d, 0x26, 0x7c, 0x29, 0x35, 0x26, 0xf6, 0x7f, 0x5f, 0xce, 0x7c, 0x73, 0xc7, 0xcd, 0xed, 0x32, + 0x35, 0x77, 0xb3, 0x78, 0xba, 0xd3, 0xa0, 0xb4, 0x1b, 0xed, 0x07, 0x78, 0x09, 0x53, 0x85, 0x94, + 0xee, 0xd7, 0x4c, 0xac, 0x1e, 0xac, 0x44, 0x9c, 0x7e, 0x26, 0x7f, 0x5a, 0xa6, 0x45, 0x4e, 0xf0, + 0xdc, 0x31, 0xb7, 0xb0, 0xa8, 0xea, 0xd4, 0x24, 0x08, 0x66, 0x11, 0x8d, 0x5d, 0xa4, 0x1c, 0x50, + 0xc6, 0xf1, 0xfb, 0x85, 0x07, 0xcd, 0xa1, 0x72, 0x75, 0xec, 0x14, 0x37, 0xfa, 0x28, 0x6a, 0x42, + 0x53, 0xe3, 0x64, 0xc2, 0x47, 0xf0, 0x45, 0x96, 0x22, 0xcd, 0x4b, 0x07, 0x49, 0x3f, 0x41, 0x97, + 0x48, 0xa4, 0xc9, 0x1a, 0xdb, 0xd4, 0x1d, 0x92, 0xb4, 0xf8, 0x77, 0xb6, 0x6f, 0x26, 0xaa, 0xe2, + 0x45, 0x9f, 0x7b, 0xb8, 0x3b, 0x65, 0xb1, 0x16, 0x9c, 0xef, 0xc2, 0x5d, 0x3b, 0xea, 0x5e, 0x89, + 0xe5, 0xfa, 0x52, 0xbc, 0x4a, 0xe2, 0xe5, 0x58, 0xb0, 0x2c, 0x42, 0x88, 0xb0, 0x09, 0xd9, 0xa8, + 0x68, 0x6c, 0xc1, 0x65, 0xd3, 0xf4, 0x3c, 0x91, 0x64, 0xef, 0xd8, 0x0d, 0xa0, 0x10, 0xff, 0x3e, + 0x6d, 0xf2, 0xce, 0x63, 0x36, 0xf5, 0x5a, 0xdc, 0xa6, 0x28, 0xeb, 0x3a, 0x19, 0x79, 0xee, 0x98, + 0x86, 0xe6, 0x4e, 0x3a, 0x3b, 0xf4, 0x9c, 0x92, 0x84, 0x78, 0xb1, 0x4a, 0x28, 0xd7, 0xfc, 0x00, + 0x6b, 0x74, 0x9e, 0xc0, 0xbf, 0x35, 0x70, 0x3b, 0x06, 0x19, 0x7a, 0x9d, 0x02, 0x5e, 0xd3, 0xf0, + 0x00, 0x17, 0xb7, 0x5c, 0x3a, 0x67, 0xaa, 0x1a, 0x9e, 0x54, 0x65, 0xa9, 0x1c, 0x73, 0xd0, 0x8e, + 0xb3, 0x58, 0xce, 0x57, 0xe3, 0xfe, 0x44, 0x2c, 0xf6, 0xf5, 0x47, 0x35, 0xfb, 0x94, 0xe6, 0xa6, + 0x2c, 0xb7, 0x4a, 0x72, 0xd7, 0xf5, 0x65, 0xc7, 0xf9, 0x0b, 0x0d, 0x72, 0x5e, 0x4e, 0x80, 0x21, + 0x46, 0x4e, 0x3d, 0x2d, 0x6e, 0x14, 0x69, 0xb1, 0x66, 0x4d, 0xd3, 0xd5, 0x4a, 0x20, 0x60, 0xc2, + 0x7a, 0x0f, 0x7d, 0x62, 0x56, 0x4a, 0xd2, 0xbe, 0x3e, 0x1f, 0x50, 0x06, 0xfe, 0xbc, 0x03, 0x83, + 0x73, 0xe9, 0x44, 0x61, 0x24, 0x26, 0x2b, 0x4c, 0x43, 0xf8, 0x60, 0x07, 0xd9, 0x22, 0x0f, 0x53, + 0x7e, 0xeb, 0xfc, 0xed, 0x97, 0xb7, 0xef, 0x38, 0xcb, 0x26, 0xf3, 0x9f, 0xa9, 0xad, 0x7b, 0x1b, + 0xa7, 0x91, 0xb8, 0xed, 0xa3, 0xca, 0x43, 0x8d, 0xdc, 0xcf, 0xe9, 0xa5, 0xe7, 0x63, 0x92, 0x3c, + 0x54, 0xe3, 0xfa, 0xb0, 0xbf, 0xae, 0x3b, 0x4f, 0x5d, 0xef, 0xa2, 0xde, 0x10, 0xd4, 0x87, 0xe3, + 0xb0, 0x8b, 0xb6, 0xc6, 0x00, 0xbc, 0x15, 0xfa, 0x8f, 0xeb, 0x2f, 0x33, 0x21, 0x05, 0x90, 0x5d, + 0xa9, 0xd2, 0xc0, 0x0d, 0xc3, 0xb0, 0x3e, 0xc2, 0x74, 0xb9, 0x50, 0x66, 0xca, 0x0d, 0x76, 0x75, + 0xc0, 0x5a, 0x08, 0xb2, 0x76, 0xbf, 0xc1, 0xc6, 0xc2, 0xf7, 0x2f, 0x36, 0xe6, 0xc5, 0x76, 0x7f, + 0xff, 0xc5, 0x06, 0x17, 0xde, 0xbe, 0x1f, 0x29, 0x7c, 0x98, 0xb4, 0x7f, 0x08, 0x6a, 0x1e, 0xb1, + 0xd2, 0x2d, 0x0f, 0x02, 0xbc, 0x91, 0xb9, 0xed, 0x2b, 0xbb, 0x8e, 0x8f, 0x7e, 0xd1, 0xc6, 0xa2, + 0xe8, 0x0d, 0x16, 0xef, 0xbd, 0x8d, 0xc1, 0xef, 0x02, 0x9a, 0x74, 0xdd, 0x71, 0xb2, 0xca, 0x5c, + 0x9f, 0xe5, 0xeb, 0x74, 0xd2, 0xf5, 0xc2, 0xf3, 0xcd, 0x83, 0x10, 0x94, 0xf3, 0x6e, 0xdf, 0xfb, + 0xec, 0x96, 0xc5, 0x58, 0x65, 0xa9, 0xf2, 0x4b, 0x5d, 0x4f, 0x37, 0x28, 0xab, 0xd9, 0xf5, 0x8a, + 0x45, 0xc9, 0x76, 0x75, 0xbd, 0x2d, 0x55, 0x7b, 0x38, 0x98, 0x98, 0x45, 0xa1, 0xc7, 0x75, 0xc2, + 0xab, 0x6b, 0x9f, 0x9e, 0x79, 0x1a, 0xd1, 0xf3, 0x66, 0x3b, 0x9a, 0xae, 0x52, 0x3a, 0x56, 0x73, + 0xcc, 0xde, 0xb8, 0xb7, 0xc9, 0xb8, 0x5c, 0x65, 0xa9, 0x13, 0x21, 0xb5, 0xde, 0xa8, 0xe6, 0x6f, + 0xd7, 0x3f, 0x44, 0xf0, 0x6a, 0x5b, 0x74, 0xaf, 0xad, 0xb6, 0x29, 0x50, 0x53, 0x78, 0x0b, 0x5e, + 0x9f, 0x24, 0xc2, 0x6c, 0x70, 0xcf, 0x55, 0x7e, 0xc2, 0x96, 0xf6, 0xee, 0x14, 0x13, 0xa1, 0x78, + 0x27, 0x80, 0x9e, 0x2e, 0x7e, 0xab, 0x01, 0xd8, 0xde, 0xd0, 0x7e, 0x03, 0xbe, 0x2d, 0xb1, 0x5d, + 0x44, 0x04, 0x9e, 0xaf, 0xdf, 0x4a, 0xeb, 0xad, 0xe5, 0x05, 0x17, 0xef, 0x53, 0xeb, 0xbd, 0xa2, + 0x93, 0xcf, 0x6c, 0x48, 0x96, 0x24, 0xf7, 0x79, 0x9f, 0xd2, 0xd6, 0x7e, 0x6e, 0xbf, 0xa2, 0xa6, + 0x91, 0xcc, 0xd6, 0x0a, 0x1c, 0x47, 0x84, 0x0a, 0xcf, 0x53, 0x0e, 0xe1, 0x72, 0x97, 0x79, 0x7e, + 0xa2, 0x1b, 0x04, 0x8d, 0x06, 0xc4, 0xc7, 0xe1, 0x4f, 0xe3, 0xdf, 0xc1, 0xfe, 0xf7, 0x3f, 0xf0, + 0x75, 0xde, 0x4d, 0x3c, 0x3f, 0x0b, 0xe3, 0x3e, 0x30, 0x28, 0x08, 0x69, 0x97, 0x87, 0xe7, 0xae, + 0xa0, 0xb7, 0xc0, 0xac, 0xa8, 0x9a, 0xc4, 0xd4, 0x49, 0xae, 0xf8, 0xf5, 0xcb, 0x97, 0xcf, 0xf0, + 0xd2, 0x37, 0xbb, 0x7f, 0xf9, 0x92, 0x1e, 0x53, 0x73, 0xed, 0xe7, 0xa0, 0x1d, 0x64, 0xfe, 0x0f, + 0x50, 0x08, 0xdd, 0xd4, 0xf3, 0xfc, 0x09, 0x4d, 0x99, 0x46, 0x0f, 0x4c, 0xf8, 0xe0, 0x7c, 0x20, + 0x29, 0xa9, 0x77, 0x7f, 0xbf, 0xc4, 0x6f, 0x0a, 0x7f, 0x00, 0xac, 0xc4, 0xfd, 0xa5, 0x58, 0x76, + 0x3d, 0x6f, 0x6f, 0xe8, 0x47, 0xe1, 0x66, 0x99, 0x03, 0x87, 0x04, 0xc5, 0xcb, 0x89, 0xe7, 0xa7, + 0x41, 0xea, 0x8b, 0x34, 0x78, 0x36, 0xf0, 0x45, 0xf0, 0x6c, 0xe8, 0x9b, 0x69, 0x03, 0xe8, 0x1a, + 0x64, 0x3e, 0x10, 0x24, 0xf8, 0x26, 0xcb, 0xd8, 0xba, 0x3f, 0xcd, 0xc4, 0xa2, 0xbb, 0x49, 0x78, + 0x3a, 0x93, 0xf3, 0x20, 0xeb, 0xab, 0x9b, 0xad, 0x8f, 0xac, 0x3d, 0x1c, 0x7c, 0xc9, 0x3d, 0xdf, + 0x2a, 0x7a, 0x78, 0xcc, 0x08, 0x09, 0xd8, 0xe3, 0x4b, 0x30, 0xfa, 0xc1, 0xc0, 0xe7, 0x58, 0xfc, + 0xbc, 0xdd, 0xfa, 0x8b, 0x70, 0xb3, 0xe0, 0x72, 0x2e, 0xa2, 0xc0, 0xfd, 0xf9, 0xa7, 0x77, 0x97, + 0xae, 0x8f, 0x4a, 0x2b, 0xc0, 0x23, 0x05, 0xc0, 0x52, 0x16, 0xa7, 0xe0, 0x08, 0xad, 0xbb, 0x91, + 0xb7, 0xb5, 0x88, 0xc6, 0x2b, 0x44, 0xcb, 0xfd, 0x05, 0x70, 0x86, 0xae, 0xac, 0x41, 0xde, 0x51, + 0x2f, 0xb9, 0x26, 0xe0, 0x08, 0xb0, 0x45, 0xba, 0xb3, 0xfb, 0xfe, 0x67, 0xbd, 0x4f, 0xc7, 0x7d, + 0xb1, 0x49, 0xb7, 0x60, 0x85, 0x00, 0x2f, 0x8e, 0x1e, 0x88, 0x76, 0x66, 0xfd, 0x1e, 0x3f, 0xc3, + 0xc2, 0x39, 0x41, 0x4c, 0xd4, 0x18, 0xf7, 0x0d, 0x55, 0xe6, 0x40, 0x4f, 0xac, 0x9a, 0x51, 0x8c, + 0x15, 0x38, 0xee, 0x1e, 0xf7, 0x5d, 0x3a, 0x22, 0x77, 0xbd, 0x6d, 0x7d, 0x88, 0xfd, 0xaa, 0x26, + 0x0d, 0x31, 0x38, 0x2e, 0x19, 0xf6, 0x90, 0x21, 0xa0, 0x3d, 0x0d, 0x8f, 0xf9, 0xa1, 0x96, 0x0b, + 0xe7, 0x69, 0x0c, 0x8c, 0xb2, 0x2f, 0xc2, 0xc1, 0x28, 0x9f, 0x8b, 0xdb, 0xae, 0xa7, 0x15, 0x59, + 0x12, 0xf2, 0xfe, 0x82, 0x2d, 0xbb, 0x6a, 0x51, 0x60, 0xa5, 0x94, 0x0e, 0x10, 0xc5, 0x22, 0xce, + 0x79, 0xb7, 0x2b, 0xfd, 0x04, 0x35, 0x12, 0xcc, 0x7b, 0x19, 0x2f, 0xb8, 0x58, 0xc9, 0x6e, 0xa9, + 0xa6, 0x4a, 0xd4, 0xa6, 0x75, 0x79, 0x30, 0x02, 0x92, 0x1a, 0x79, 0x48, 0x2a, 0xf2, 0x20, 0x50, + 0x40, 0x12, 0xc5, 0xbc, 0xb2, 0x85, 0x79, 0xc5, 0x95, 0x44, 0x69, 0xc0, 0x8b, 0xc5, 0xbd, 0xf4, + 0x88, 0x5c, 0xcb, 0xfb, 0x15, 0xbe, 0x4d, 0x4a, 0xbe, 0xcd, 0xcc, 0x32, 0x60, 0xe1, 0xe3, 0x59, + 0x0a, 0x38, 0xab, 0xf3, 0x71, 0x0c, 0x4a, 0x10, 0x24, 0xe7, 0x11, 0xdc, 0x93, 0x55, 0xb8, 0x47, + 0xd6, 0xb8, 0x67, 0x62, 0x71, 0x4f, 0x6a, 0xb8, 0x47, 0x1a, 0xee, 0x01, 0x19, 0xeb, 0x1a, 0xf6, + 0x51, 0x67, 0xba, 0xc0, 0x3c, 0x00, 0x76, 0x3b, 0xfb, 0xf8, 0xda, 0x1e, 0xa9, 0x00, 0x13, 0x76, + 0x93, 0xcb, 0xbf, 0xaa, 0x5a, 0x8b, 0xae, 0x5b, 0x7a, 0x50, 0xb9, 0xeb, 0x1b, 0x8a, 0x7a, 0xff, + 0xc7, 0x1c, 0x07, 0x84, 0x80, 0x98, 0x75, 0xbd, 0x01, 0x85, 0x0b, 0xb2, 0x95, 0x7e, 0x29, 0xf6, + 0xf6, 0x00, 0x4d, 0xde, 0x48, 0xed, 0x4a, 0x33, 0x43, 0x1f, 0xba, 0xa0, 0x26, 0x93, 0x17, 0xad, + 0xdc, 0xa0, 0xfa, 0x96, 0x9a, 0xbb, 0x0f, 0x31, 0x71, 0xda, 0xa5, 0x57, 0xf3, 0x38, 0xe2, 0x68, + 0x7c, 0xfa, 0x7a, 0x9d, 0x6a, 0x2b, 0x2c, 0xe8, 0x05, 0xfa, 0xa1, 0xce, 0xf9, 0xda, 0x8e, 0x15, + 0x12, 0x5c, 0x33, 0x1c, 0x17, 0xb8, 0x54, 0xb8, 0xef, 0x82, 0x64, 0x14, 0xaa, 0x5d, 0x47, 0x8c, + 0x9e, 0xe1, 0xf2, 0x5d, 0x5c, 0xca, 0x51, 0xfd, 0xd7, 0xb8, 0x34, 0x0f, 0xaf, 0x5c, 0x8c, 0xab, + 0x5c, 0x1f, 0x2e, 0xf8, 0x17, 0x22, 0x2b, 0xf8, 0x8b, 0x51, 0xd5, 0x35, 0xf2, 0xb4, 0xa5, 0xc1, + 0xb5, 0x61, 0x42, 0x43, 0x42, 0x96, 0xc7, 0xbf, 0x4a, 0x7d, 0x76, 0x1d, 0xca, 0x3e, 0xf8, 0xa6, + 0x31, 0x80, 0xd1, 0x07, 0x08, 0xb4, 0xcd, 0x04, 0xba, 0xa3, 0xa3, 0x20, 0xfb, 0x3a, 0xb3, 0xd0, + 0x75, 0x01, 0x62, 0x17, 0xcc, 0x4f, 0xde, 0x8f, 0xd3, 0x49, 0xb2, 0x8a, 0x60, 0x8b, 0x0c, 0xd9, + 0x32, 0x09, 0xaf, 0xe8, 0xb3, 0x8f, 0xc0, 0xb5, 0xc3, 0x5b, 0xd7, 0x57, 0xf6, 0xcb, 0x75, 0xb7, + 0xd7, 0x23, 0xa1, 0x95, 0xe3, 0xf9, 0x00, 0x38, 0x0c, 0x18, 0x7f, 0x95, 0xcf, 0xbb, 0xfd, 0x7e, + 0x5f, 0x90, 0x08, 0x77, 0x15, 0x38, 0x7c, 0x0b, 0xe8, 0xed, 0xaa, 0x99, 0xb8, 0x1e, 0x0c, 0x9e, + 0x84, 0xc1, 0xdd, 0x16, 0x1c, 0x09, 0xf0, 0x1d, 0x80, 0xbe, 0x40, 0x4b, 0x80, 0x00, 0x5c, 0x90, + 0xef, 0x2f, 0xff, 0xfa, 0x36, 0x74, 0x5d, 0x1f, 0x44, 0x52, 0x64, 0x6f, 0x18, 0x60, 0xa7, 0x36, + 0x3c, 0xc5, 0x29, 0x0b, 0x9d, 0x83, 0x1a, 0xe2, 0x27, 0x0a, 0xb9, 0x81, 0x89, 0x52, 0x6f, 0x34, + 0x00, 0x99, 0xa4, 0x9a, 0x59, 0x80, 0x89, 0xf5, 0x8b, 0x03, 0xdf, 0x67, 0x03, 0x5c, 0x00, 0x8b, + 0xc9, 0xd3, 0xe8, 0x15, 0x96, 0x93, 0xd1, 0x2e, 0xbd, 0x47, 0xf0, 0xa1, 0xe6, 0x86, 0x3a, 0x3b, + 0x94, 0x7e, 0x4e, 0xc1, 0x10, 0x05, 0xd9, 0x8b, 0x33, 0x36, 0xcf, 0x66, 0x06, 0x95, 0x1d, 0x06, + 0xd3, 0x65, 0xb9, 0x69, 0x2a, 0x59, 0xdb, 0xc2, 0x1f, 0xac, 0xae, 0x31, 0xf7, 0x73, 0x2c, 0xf4, + 0x41, 0x55, 0x5a, 0x53, 0x6f, 0xa0, 0x0f, 0x67, 0x81, 0x30, 0x52, 0x9f, 0x97, 0x4a, 0xae, 0xa4, + 0x9f, 0x3a, 0xb1, 0x7b, 0xcd, 0xa7, 0x6c, 0x95, 0x48, 0x43, 0xc2, 0x81, 0x6f, 0xca, 0xe7, 0xfd, + 0xa2, 0xa6, 0x1e, 0x89, 0x4a, 0xb4, 0x44, 0xc6, 0x01, 0xe6, 0x00, 0xa4, 0x35, 0xe9, 0x1a, 0x47, + 0x40, 0x8a, 0x34, 0x90, 0x3e, 0xb9, 0x00, 0x60, 0x87, 0x73, 0xac, 0x5a, 0x62, 0xea, 0xf1, 0xd7, + 0x20, 0xa7, 0xe7, 0x5f, 0x01, 0xa4, 0x92, 0xf2, 0xf2, 0xfe, 0xbe, 0x00, 0x03, 0xf5, 0x80, 0x76, + 0xa1, 0x34, 0x04, 0xac, 0x97, 0x1a, 0x10, 0x44, 0x2f, 0x47, 0x5e, 0xf0, 0x7c, 0xfe, 0x11, 0x66, + 0x60, 0x7a, 0x8e, 0x5c, 0xcf, 0x21, 0xcc, 0x0c, 0xc9, 0xd6, 0x8f, 0x4b, 0xfe, 0xc8, 0x6c, 0xfe, + 0x00, 0x10, 0x41, 0x0e, 0xfa, 0x78, 0x26, 0x01, 0x5a, 0xa3, 0xaf, 0x8e, 0x39, 0x85, 0x5f, 0xb6, + 0xe8, 0x83, 0xce, 0xc4, 0x27, 0x26, 0x02, 0x44, 0x64, 0x36, 0x03, 0x01, 0xff, 0x28, 0x17, 0x5a, + 0xf8, 0xa9, 0xbe, 0x4b, 0x10, 0x50, 0xf0, 0xa1, 0x51, 0x3f, 0x3f, 0x85, 0x97, 0x9e, 0x55, 0x99, + 0x09, 0x78, 0xe8, 0xa9, 0xce, 0x73, 0x61, 0x32, 0x4d, 0x46, 0xc8, 0xf2, 0x46, 0x4d, 0xfa, 0x08, + 0x84, 0x9a, 0x12, 0x37, 0x16, 0x63, 0xaa, 0x44, 0x8e, 0x57, 0x7a, 0xd5, 0xa8, 0xd2, 0x29, 0x7b, + 0x61, 0xf9, 0xd5, 0x35, 0xa3, 0x9b, 0xd6, 0xc5, 0x8d, 0xfa, 0x8f, 0x58, 0x4b, 0xf0, 0x80, 0xb3, + 0xb9, 0x7e, 0xd7, 0xda, 0x97, 0x54, 0x72, 0xe6, 0xb7, 0xf5, 0x56, 0xd8, 0xa9, 0x74, 0x4f, 0xbb, + 0xb8, 0x00, 0xd9, 0x0f, 0x63, 0x46, 0x74, 0xf5, 0xa4, 0xd6, 0x3f, 0x9e, 0x9e, 0x0d, 0x6b, 0xb8, + 0x40, 0xb7, 0xd4, 0xa5, 0x72, 0xa6, 0x93, 0x3d, 0xad, 0x52, 0xa9, 0x37, 0x8f, 0x8a, 0x93, 0xe7, + 0x57, 0x83, 0xeb, 0x36, 0x67, 0xdd, 0x68, 0xec, 0x36, 0x47, 0x5d, 0xd7, 0x65, 0xb4, 0x4a, 0x6b, + 0xd1, 0xa9, 0xc8, 0xee, 0xa0, 0x06, 0xb7, 0x5a, 0x2b, 0x99, 0x22, 0x74, 0x4a, 0x40, 0x26, 0x40, + 0xba, 0xfb, 0xaa, 0x5d, 0x79, 0x0a, 0xf0, 0x8c, 0xa6, 0xc0, 0x38, 0xf0, 0x25, 0x6d, 0xc0, 0x47, + 0xd9, 0xa8, 0xaf, 0xeb, 0x83, 0xd8, 0x1f, 0x67, 0x31, 0xb8, 0xb8, 0x20, 0x85, 0x13, 0x3f, 0x0e, + 0xa2, 0x6d, 0x68, 0xe6, 0x06, 0x7b, 0xa8, 0x8d, 0xa4, 0x4d, 0x40, 0x34, 0x0f, 0x55, 0x3f, 0xc4, + 0x8e, 0xb0, 0xfc, 0x8d, 0x72, 0xa2, 0xd5, 0x9c, 0xa8, 0x49, 0x36, 0xc5, 0xc4, 0x60, 0x04, 0xc8, + 0x26, 0x6a, 0x46, 0x5b, 0x84, 0x98, 0x26, 0x63, 0x52, 0x15, 0xef, 0x74, 0x53, 0xbf, 0xe6, 0xa2, + 0xd8, 0xd3, 0xa2, 0xb9, 0xd6, 0x22, 0xb2, 0x00, 0xcb, 0x62, 0xab, 0xdd, 0xf8, 0x33, 0x74, 0xae, + 0xa1, 0x6e, 0x71, 0x5c, 0xfd, 0x38, 0x32, 0xb7, 0x18, 0x64, 0x5b, 0x09, 0x5b, 0x14, 0xd3, 0x44, + 0x67, 0xcd, 0xe8, 0x4c, 0xbf, 0xc9, 0xad, 0x37, 0xfa, 0xb0, 0xaa, 0x78, 0x27, 0x5a, 0x58, 0xc5, + 0xbc, 0x4b, 0x76, 0x44, 0x8b, 0xa4, 0xcb, 0x62, 0xad, 0xcb, 0xb2, 0x2d, 0x9a, 0x6c, 0xad, 0x70, + 0x94, 0xce, 0x42, 0xf8, 0x9b, 0xec, 0x26, 0x49, 0x0c, 0xbf, 0x03, 0x72, 0xbc, 0x06, 0xcd, 0x35, + 0x8a, 0xa7, 0x5d, 0x57, 0xa5, 0xb2, 0x31, 0xe1, 0x90, 0x79, 0xc6, 0xce, 0xe9, 0xed, 0xba, 0x3e, + 0xf7, 0x46, 0x3c, 0xc9, 0x79, 0x6b, 0xec, 0x91, 0x21, 0x2e, 0x4c, 0xbc, 0x31, 0x4e, 0xc4, 0x18, + 0x75, 0x8f, 0x9a, 0x1f, 0xf0, 0xd7, 0xbd, 0x4a, 0xaf, 0xfd, 0x18, 0xe0, 0x04, 0x5f, 0x38, 0x48, + 0xfb, 0x78, 0x01, 0x86, 0x68, 0x2c, 0x00, 0x32, 0xae, 0xe6, 0x9e, 0x54, 0xe6, 0x2e, 0x92, 0x4d, + 0x74, 0x4c, 0xb1, 0xc0, 0x44, 0x91, 0x14, 0x22, 0xe9, 0xdf, 0x70, 0xa0, 0x50, 0x82, 0x93, 0xec, + 0xb3, 0x65, 0xbc, 0x8f, 0x11, 0xf9, 0xbe, 0xc2, 0xe6, 0x05, 0x5b, 0x88, 0x15, 0x9e, 0x15, 0xef, + 0xe5, 0x7e, 0x9b, 0x5b, 0x2c, 0x81, 0x29, 0x37, 0xba, 0xf6, 0x37, 0x88, 0x7c, 0x35, 0x28, 0x58, + 0x18, 0xeb, 0x37, 0x31, 0x3e, 0x2f, 0xa0, 0xe4, 0xd9, 0xa4, 0x2f, 0x3e, 0x78, 0x5a, 0xa5, 0xdd, + 0x88, 0x18, 0x4f, 0x0f, 0x90, 0xdd, 0xa2, 0x82, 0xdd, 0x46, 0xb5, 0xdc, 0x83, 0x66, 0xf4, 0x15, + 0xa6, 0x21, 0x96, 0xf8, 0x67, 0x1e, 0x0e, 0xf9, 0xe1, 0x97, 0x89, 0x3f, 0x0b, 0x17, 0x65, 0x58, + 0x82, 0xb1, 0x4f, 0xa9, 0x11, 0xd3, 0x86, 0xac, 0xf2, 0x52, 0x56, 0x73, 0x92, 0x2b, 0x81, 0xb2, + 0x9a, 0x80, 0x48, 0xc5, 0x96, 0xac, 0xa6, 0x18, 0x85, 0x5b, 0xc2, 0x27, 0x0a, 0xe1, 0x53, 0x3d, + 0xb7, 0x10, 0x17, 0x80, 0x9f, 0xc4, 0xb6, 0xce, 0x8b, 0x8d, 0xdc, 0x1b, 0x6e, 0xdf, 0x8f, 0x56, + 0xca, 0x10, 0x43, 0xe3, 0x64, 0x7b, 0xff, 0x62, 0xd3, 0x0c, 0x10, 0xc0, 0x77, 0x5f, 0xaa, 0x3e, + 0xb9, 0xe7, 0xdb, 0x5b, 0x53, 0x8d, 0x55, 0x25, 0x90, 0xf9, 0x9b, 0x14, 0xe4, 0x1c, 0x82, 0xe7, + 0x01, 0xba, 0x41, 0x6d, 0xee, 0xf5, 0xcc, 0x48, 0xfe, 0x3a, 0x5c, 0xd1, 0xf6, 0x0b, 0x4f, 0xf3, + 0x0a, 0x8c, 0xc2, 0x35, 0x44, 0x3e, 0xda, 0xbd, 0xbc, 0xa7, 0xec, 0x45, 0x45, 0x3b, 0x08, 0x74, + 0xc5, 0x14, 0xee, 0x01, 0xe0, 0x2b, 0xac, 0xa0, 0xf3, 0x55, 0x6d, 0xdc, 0x75, 0xe9, 0x67, 0x0a, + 0xef, 0xc2, 0x7d, 0xee, 0x06, 0xee, 0xfe, 0xbe, 0x4b, 0xdb, 0xdc, 0xfe, 0x57, 0x8a, 0x3b, 0x06, + 0x6f, 0x70, 0x64, 0x0c, 0xec, 0xba, 0xff, 0xbb, 0x88, 0xd3, 0xae, 0xfb, 0x5f, 0x28, 0x2d, 0x9a, + 0x7e, 0x10, 0x32, 0xe9, 0x2f, 0x33, 0x1a, 0x7a, 0xd5, 0x5f, 0xfa, 0xf3, 0xa7, 0xe8, 0x97, 0x42, + 0xb3, 0x14, 0x74, 0xe1, 0xd5, 0x2c, 0x8f, 0x6d, 0x1a, 0x4c, 0x65, 0x68, 0xab, 0x6d, 0xb0, 0x5c, + 0xbe, 0x16, 0x25, 0x62, 0x55, 0x1e, 0xb5, 0xaa, 0x12, 0xe3, 0x1a, 0xb6, 0x68, 0x12, 0xe3, 0x22, + 0x02, 0x53, 0xa9, 0x3a, 0x89, 0x8a, 0x2e, 0xb1, 0xab, 0x1c, 0x90, 0xa3, 0x8a, 0xd0, 0x32, 0xc7, + 0xc0, 0xb2, 0x78, 0x42, 0xfb, 0xa1, 0xfc, 0xa4, 0xc8, 0xf8, 0x49, 0x8b, 0x56, 0xe0, 0x5b, 0xd4, + 0x8f, 0xf1, 0x8e, 0xfc, 0x55, 0x39, 0x1f, 0xd0, 0x60, 0x59, 0x3e, 0x2d, 0xc0, 0x3d, 0xc1, 0x38, + 0x98, 0x01, 0xb9, 0x59, 0x42, 0x85, 0x13, 0x20, 0x32, 0x45, 0x83, 0x2a, 0x64, 0xf0, 0xd7, 0x61, + 0x72, 0x91, 0x05, 0x73, 0x7f, 0x0a, 0xd7, 0x49, 0x30, 0xf3, 0x6f, 0xe0, 0xba, 0x0a, 0xd6, 0xfe, + 0x2d, 0x5c, 0x97, 0xc1, 0xd4, 0x7f, 0x1b, 0xfe, 0x95, 0xc9, 0x79, 0x7f, 0x9a, 0x08, 0xb0, 0xfd, + 0xdd, 0x55, 0x6f, 0xed, 0xed, 0x1f, 0x78, 0xfe, 0xb8, 0xd2, 0xba, 0xec, 0x4d, 0xa9, 0xf5, 0x4d, + 0x78, 0x74, 0xfa, 0xe5, 0x8d, 0xff, 0x3b, 0x5e, 0x6e, 0xfd, 0xbb, 0xf0, 0xcd, 0x59, 0xf8, 0xd5, + 0xc9, 0xe9, 0xc5, 0xf0, 0x28, 0x18, 0x9e, 0xfa, 0xef, 0x50, 0x74, 0x37, 0x13, 0x79, 0x17, 0xbc, + 0xde, 0x86, 0x8a, 0xac, 0xaf, 0x48, 0x1e, 0xbb, 0x6b, 0x7f, 0xea, 0x8d, 0x5e, 0xe3, 0x97, 0xb4, + 0xb7, 0x5a, 0x56, 0xfd, 0x01, 0xfc, 0x87, 0x9a, 0x15, 0xab, 0x5f, 0x86, 0xaf, 0x31, 0xa9, 0x48, + 0x6f, 0x51, 0xb7, 0x76, 0xcd, 0x7b, 0xc2, 0x42, 0x21, 0xd8, 0x2f, 0x7c, 0x9c, 0xfe, 0x43, 0x6d, + 0xfa, 0x37, 0xfe, 0xef, 0xde, 0xe8, 0x03, 0xda, 0x9d, 0xe4, 0x9d, 0x3a, 0x1d, 0xc0, 0xdf, 0x95, + 0x82, 0x7f, 0xae, 0xaf, 0x5a, 0x7f, 0x01, 0xd4, 0xd2, 0x8c, 0xd4, 0x13, 0x04, 0xa6, 0x8b, 0x59, + 0x10, 0x1e, 0x0e, 0x46, 0xfc, 0xec, 0x76, 0xc4, 0x21, 0xd0, 0x35, 0x6d, 0x12, 0xda, 0xe4, 0xd9, + 0xcd, 0x48, 0x42, 0xdb, 0x06, 0x1b, 0xc0, 0x5e, 0x7d, 0x79, 0xb3, 0x07, 0x9a, 0x00, 0x24, 0x0d, + 0x36, 0x0d, 0xce, 0x37, 0x5e, 0x38, 0xfa, 0x11, 0xbd, 0xb7, 0x7e, 0x1c, 0xf2, 0xde, 0x18, 0xc8, + 0xef, 0xea, 0xf5, 0x50, 0xff, 0x25, 0x2f, 0x5f, 0x8a, 0xf3, 0x10, 0x42, 0x33, 0x71, 0xb6, 0x7e, + 0xf9, 0x32, 0xa6, 0xdb, 0xf8, 0x6c, 0x7a, 0x7f, 0xff, 0x0c, 0xde, 0x48, 0x6c, 0xe3, 0x67, 0x53, + 0x33, 0x79, 0x72, 0x11, 0x7f, 0xb9, 0xde, 0x13, 0x01, 0x87, 0xbf, 0x72, 0x04, 0x7c, 0x84, 0x0a, + 0x9b, 0x0e, 0xa3, 0xbb, 0x97, 0x57, 0x47, 0x5f, 0x82, 0x11, 0xa0, 0xcb, 0xde, 0xd0, 0xdc, 0x1c, + 0x98, 0x9b, 0xc3, 0x6b, 0xca, 0xe3, 0x6e, 0x32, 0xd0, 0x26, 0x33, 0x60, 0xaf, 0x31, 0x72, 0xd6, + 0x9c, 0xdf, 0x5d, 0x8a, 0x5f, 0x66, 0x63, 0x50, 0x4b, 0xa3, 0x77, 0x4a, 0xfd, 0x80, 0x9d, 0xb1, + 0x51, 0xf3, 0x3e, 0x83, 0xb7, 0xa8, 0xca, 0x7c, 0x90, 0xfc, 0x88, 0xfe, 0x2e, 0xb6, 0xde, 0x7b, + 0x1b, 0x51, 0xe0, 0xae, 0xfb, 0x47, 0xa7, 0xf0, 0x5f, 0x6f, 0x8b, 0xd6, 0xcb, 0xd1, 0x13, 0x99, + 0x3d, 0x22, 0xaa, 0xf1, 0xa3, 0x13, 0xe2, 0xb7, 0x70, 0x08, 0x23, 0xf1, 0x10, 0xfc, 0x03, 0x37, + 0xa8, 0x1f, 0xd2, 0x3f, 0xb7, 0x68, 0xaf, 0xcd, 0x49, 0x09, 0x28, 0x30, 0x57, 0x7b, 0x07, 0x47, + 0x7e, 0x14, 0xe6, 0x70, 0x41, 0xd2, 0x09, 0xb0, 0x3d, 0x77, 0x7b, 0xee, 0xf2, 0xce, 0xf9, 0x26, + 0x8b, 0x59, 0xe2, 0x56, 0x60, 0x36, 0x2b, 0xa3, 0x91, 0x05, 0xcd, 0xa5, 0x7e, 0xc1, 0x0b, 0xf4, + 0x97, 0xb5, 0x14, 0xda, 0xf3, 0x6f, 0xf0, 0x93, 0xcc, 0xd0, 0x55, 0x5f, 0x63, 0x9a, 0xc6, 0x6f, + 0x41, 0x84, 0x10, 0xda, 0xd0, 0x5d, 0xc4, 0x51, 0x94, 0x70, 0x33, 0xf3, 0x25, 0xbc, 0x03, 0x5f, + 0x6a, 0xe2, 0x47, 0xde, 0x56, 0x55, 0x3c, 0x75, 0x25, 0xa8, 0x20, 0xe8, 0xed, 0x0e, 0xdd, 0x20, + 0x0e, 0xdf, 0x8d, 0xc6, 0xc0, 0x62, 0x1f, 0x46, 0xd4, 0x72, 0x80, 0x2d, 0x14, 0xdc, 0x76, 0xdf, + 0x79, 0xf6, 0x8b, 0x43, 0x7c, 0x41, 0xc5, 0x8a, 0xf0, 0x62, 0xab, 0xf4, 0x6e, 0xc9, 0xab, 0x68, + 0x5f, 0x0a, 0x61, 0x85, 0x88, 0x11, 0x8c, 0x4c, 0xf1, 0x08, 0x76, 0x88, 0xcc, 0x4d, 0xa9, 0x09, + 0x69, 0x16, 0xae, 0x18, 0x03, 0xd8, 0x10, 0xbc, 0x83, 0x01, 0xf0, 0x1c, 0x47, 0x47, 0x2a, 0x47, + 0x23, 0xc9, 0xcb, 0x50, 0x0c, 0xd8, 0x0f, 0x2d, 0x21, 0x30, 0x1b, 0x7f, 0x16, 0x86, 0x0c, 0xdc, + 0xf7, 0x50, 0x8c, 0x86, 0x61, 0x98, 0xf6, 0xe4, 0x45, 0xae, 0xa8, 0xc5, 0xbc, 0x40, 0xdf, 0x81, + 0xcd, 0x00, 0x3f, 0x01, 0x9c, 0x31, 0xb1, 0x65, 0xe8, 0xba, 0x9b, 0x8c, 0xc4, 0x90, 0x92, 0x68, + 0x2a, 0x93, 0xd0, 0x3e, 0xce, 0xbc, 0xc5, 0xe1, 0x79, 0x09, 0xa9, 0xc5, 0xa3, 0x60, 0x92, 0x69, + 0x76, 0xad, 0xbb, 0x8d, 0xa6, 0xcc, 0xeb, 0x59, 0x78, 0xbb, 0xb0, 0xa3, 0x55, 0xdb, 0xaa, 0x1a, + 0x88, 0x02, 0x36, 0x8c, 0x03, 0xd9, 0x45, 0x7e, 0x21, 0xca, 0x74, 0xc9, 0x73, 0x4a, 0x97, 0xf4, + 0xa5, 0x78, 0x2b, 0x6e, 0x79, 0xf6, 0x0a, 0xb0, 0xdf, 0xf5, 0xf0, 0xf7, 0xe2, 0x2e, 0xc5, 0xf7, + 0x40, 0x96, 0x83, 0xe3, 0x63, 0x5f, 0xff, 0xcf, 0x6a, 0x25, 0xf0, 0xec, 0xb3, 0x09, 0x23, 0x1e, + 0x80, 0xe6, 0x43, 0x58, 0x02, 0x2c, 0x2b, 0xaf, 0xaf, 0xe0, 0xe9, 0x4d, 0x43, 0x14, 0x6a, 0xbf, + 0xdd, 0xef, 0xf6, 0xbd, 0xfd, 0x99, 0xef, 0xbe, 0x18, 0xbe, 0x18, 0x42, 0xa7, 0x51, 0x61, 0x8f, + 0x31, 0xe1, 0x83, 0xc9, 0x53, 0xe4, 0xa0, 0xfd, 0xfe, 0xe6, 0x60, 0xbb, 0x3f, 0xf3, 0x8c, 0xd5, + 0x2e, 0xa8, 0xcd, 0xfd, 0xe1, 0x89, 0x67, 0x76, 0x07, 0x22, 0x2b, 0x41, 0x64, 0x53, 0x10, 0x59, + 0x66, 0x33, 0x40, 0x15, 0x6a, 0x1d, 0x14, 0x16, 0x62, 0xb7, 0xd7, 0xe5, 0x67, 0x67, 0xc3, 0x93, + 0x7b, 0x79, 0x76, 0x76, 0x7a, 0x9f, 0x22, 0x22, 0xde, 0x91, 0x13, 0xd2, 0xc5, 0x99, 0xfb, 0x39, + 0xa5, 0x0d, 0x7a, 0x27, 0xd6, 0x66, 0x15, 0xbf, 0x1a, 0xbb, 0x0a, 0x2c, 0x05, 0x3c, 0x54, 0x24, + 0xa3, 0x2c, 0x56, 0xa2, 0x30, 0x53, 0x2a, 0x9a, 0xa7, 0xe0, 0xb2, 0x82, 0xa7, 0x27, 0xcb, 0x59, + 0x2a, 0x8a, 0x16, 0x3d, 0xb0, 0xc2, 0xff, 0x8a, 0x74, 0x08, 0xf5, 0xa6, 0xa0, 0x21, 0x75, 0x72, + 0xd1, 0x89, 0x50, 0x21, 0x3e, 0xcc, 0x6d, 0x42, 0x7b, 0x39, 0x2a, 0x02, 0x59, 0xd4, 0xf3, 0xaf, + 0xf0, 0x9b, 0x22, 0x10, 0x40, 0xf7, 0x00, 0xc2, 0xd6, 0xcd, 0x2d, 0x69, 0x1e, 0x16, 0x7d, 0x87, + 0x55, 0x49, 0x30, 0x55, 0xb2, 0x26, 0xef, 0xc8, 0x40, 0xcb, 0xfa, 0xe4, 0xed, 0xbe, 0x5b, 0x08, + 0x21, 0xe7, 0xb0, 0xe3, 0xff, 0x5c, 0x31, 0xfc, 0x38, 0x2a, 0x74, 0xe7, 0x30, 0x35, 0xf8, 0xc0, + 0xb5, 0xd7, 0x6f, 0x52, 0x36, 0x4e, 0x30, 0x71, 0x30, 0x2c, 0xcc, 0x46, 0x4a, 0x66, 0xc3, 0x46, + 0x76, 0xc5, 0x69, 0xa2, 0x6d, 0xa9, 0xc5, 0xd0, 0x61, 0x42, 0xb9, 0xb8, 0x58, 0xb3, 0x45, 0x02, + 0x1a, 0x21, 0x50, 0xce, 0x13, 0x35, 0xe1, 0x1d, 0x36, 0x59, 0xd8, 0x89, 0xf4, 0x69, 0x36, 0xb9, + 0xec, 0x9a, 0x6e, 0x28, 0xbf, 0xa0, 0xee, 0x46, 0x5a, 0xab, 0xa4, 0x5a, 0xab, 0x50, 0xf6, 0x3d, + 0x60, 0xa1, 0x6b, 0x7f, 0xfb, 0x64, 0xb2, 0xf4, 0xea, 0xad, 0xad, 0x59, 0x00, 0x8e, 0x7a, 0xe7, + 0xbb, 0x1e, 0x02, 0x45, 0xdd, 0xe9, 0xc6, 0xee, 0x4e, 0x50, 0xe2, 0x00, 0x44, 0xeb, 0x3e, 0xb0, + 0x6c, 0xac, 0xe6, 0x95, 0x77, 0xd2, 0xdd, 0x9a, 0xf3, 0x2a, 0x0c, 0x2e, 0xbe, 0xc5, 0x48, 0xe3, + 0x8a, 0x5f, 0xeb, 0xd0, 0x82, 0x51, 0xc6, 0xb1, 0x2d, 0x1e, 0xa6, 0xf4, 0x7c, 0x83, 0xc6, 0x0c, + 0xc8, 0x1b, 0xab, 0xf3, 0x3b, 0xf0, 0x97, 0xfb, 0x66, 0xff, 0xe8, 0x32, 0xcb, 0x6d, 0xff, 0xc5, + 0x26, 0xdf, 0xbe, 0x87, 0x66, 0xaa, 0x7f, 0x81, 0x78, 0x06, 0xe7, 0xcd, 0xf8, 0x0d, 0x18, 0x81, + 0x72, 0xde, 0xc4, 0x62, 0x51, 0x42, 0xf2, 0x03, 0x9e, 0x9f, 0xfe, 0x14, 0xa2, 0xd5, 0xf1, 0xab, + 0x7c, 0x08, 0xd0, 0xea, 0xfc, 0x15, 0x1f, 0x30, 0xb4, 0xba, 0x7e, 0xd6, 0x49, 0x2d, 0xe8, 0xd1, + 0x82, 0x01, 0xf0, 0x37, 0x2f, 0x21, 0x4c, 0x92, 0x97, 0xe2, 0x57, 0x04, 0x6e, 0xa3, 0xa8, 0x08, + 0xb1, 0xce, 0xe6, 0x4a, 0x5e, 0x07, 0x1b, 0xb3, 0xea, 0x6f, 0x94, 0x64, 0x4d, 0x7d, 0xb5, 0xc6, + 0x6f, 0xa0, 0xd9, 0x99, 0x3f, 0x11, 0x8b, 0x05, 0x4b, 0xa3, 0xdf, 0x20, 0xb8, 0x78, 0x8f, 0xf4, + 0x70, 0x7a, 0xff, 0x74, 0x30, 0x84, 0x72, 0x4c, 0x61, 0x14, 0xa2, 0xc7, 0xce, 0x29, 0x3a, 0xbd, + 0xc8, 0xf9, 0x02, 0xf3, 0xb0, 0x5f, 0x38, 0xbd, 0xef, 0x1d, 0xf7, 0x95, 0xfa, 0xd0, 0xae, 0x77, + 0x89, 0x84, 0x71, 0x1a, 0x6c, 0xf2, 0xbe, 0x5c, 0x60, 0x3a, 0x7d, 0xca, 0x0a, 0x1b, 0x17, 0x39, + 0x6f, 0xca, 0xc0, 0x96, 0x3f, 0x72, 0xa5, 0xed, 0x76, 0x6b, 0x3b, 0xe9, 0xc4, 0xf8, 0x0f, 0x90, + 0xc9, 0x42, 0xa5, 0x0e, 0x3d, 0x76, 0x00, 0x27, 0x3f, 0x67, 0xfb, 0x16, 0x3c, 0x15, 0x12, 0x01, + 0x60, 0xc5, 0x1b, 0x94, 0x65, 0xb0, 0x9b, 0xe5, 0xc9, 0x98, 0xeb, 0x38, 0x6e, 0x5f, 0x1d, 0x1c, + 0x76, 0x8b, 0xc8, 0xc8, 0xd1, 0x21, 0x19, 0xac, 0x06, 0xe4, 0x04, 0xb5, 0xa6, 0x14, 0x36, 0x88, + 0x85, 0x9f, 0x5f, 0x7b, 0xe5, 0x51, 0xd4, 0x33, 0x73, 0x14, 0x95, 0xdf, 0xdf, 0xab, 0x33, 0xca, + 0x38, 0xa7, 0x2b, 0x78, 0xf9, 0x17, 0x14, 0x24, 0x22, 0xf4, 0x01, 0xb8, 0x50, 0x45, 0x26, 0x8d, + 0x97, 0x9a, 0xbb, 0x31, 0x0b, 0xaf, 0xcf, 0xc2, 0xbd, 0x0b, 0x40, 0x24, 0x8e, 0xd9, 0x1b, 0x7a, + 0xdb, 0x6e, 0x8e, 0xb6, 0xea, 0x7d, 0x50, 0x4e, 0x8c, 0x61, 0x99, 0xc4, 0x66, 0x7c, 0xfd, 0xde, + 0xb3, 0x42, 0x32, 0xed, 0x6a, 0xe0, 0x7e, 0x2d, 0x71, 0x32, 0x61, 0x17, 0x84, 0xf1, 0xfa, 0x6c, + 0xc9, 0x05, 0x6c, 0x1c, 0xf0, 0x43, 0xf0, 0x23, 0x8a, 0xda, 0x2a, 0x83, 0x9b, 0x32, 0x49, 0x46, + 0x47, 0x75, 0x0d, 0x49, 0x8f, 0xe2, 0x1b, 0xa0, 0xa7, 0xe8, 0x53, 0xbd, 0x4d, 0xbf, 0x28, 0x55, + 0x0e, 0x5d, 0xf5, 0xc9, 0xaa, 0xeb, 0x37, 0x5e, 0x99, 0xaa, 0xdf, 0x10, 0xe0, 0x1d, 0x0c, 0xf6, + 0xdc, 0x45, 0xde, 0xd2, 0xe9, 0x32, 0x5e, 0x80, 0x8e, 0xfe, 0x4e, 0x83, 0x1c, 0xba, 0xea, 0xf3, + 0x65, 0xec, 0x48, 0x15, 0x39, 0x98, 0xa4, 0xa4, 0xc4, 0xae, 0x2a, 0x5a, 0x71, 0x7d, 0x59, 0x1e, + 0x51, 0x36, 0x40, 0xc4, 0x62, 0x44, 0x80, 0x31, 0x69, 0x1d, 0x4a, 0xbf, 0x85, 0xe1, 0x62, 0x1e, + 0x1d, 0x55, 0xa2, 0x66, 0x2e, 0x30, 0x49, 0xa2, 0x92, 0x15, 0x4b, 0xcc, 0xf4, 0xf1, 0x2e, 0x0c, + 0xc4, 0xed, 0xb3, 0x9b, 0x8f, 0x7c, 0x61, 0x85, 0xb8, 0x89, 0xa3, 0xe2, 0x6d, 0xf3, 0x65, 0x1b, + 0x96, 0x9a, 0x9d, 0x78, 0xc2, 0xc0, 0xc0, 0x0d, 0x08, 0x83, 0xd5, 0x2c, 0x9e, 0x5f, 0x7a, 0x56, + 0xb5, 0xb2, 0x1e, 0xaf, 0xd2, 0x13, 0xf4, 0xb7, 0x75, 0x44, 0x47, 0x87, 0x6d, 0x86, 0x18, 0xfa, + 0x8b, 0x61, 0xf0, 0x2f, 0x9b, 0x3d, 0x32, 0xbe, 0x10, 0x37, 0x18, 0xa3, 0xfb, 0x00, 0x1d, 0x1d, + 0xcb, 0x99, 0x28, 0x1e, 0x6c, 0xa0, 0x04, 0xa7, 0x28, 0xd7, 0x93, 0xe8, 0x02, 0xac, 0xd0, 0xa5, + 0x0a, 0x2c, 0x8b, 0x0b, 0x8b, 0x24, 0x01, 0xd9, 0xc3, 0xf0, 0x10, 0x0f, 0xaa, 0xc9, 0x26, 0x86, + 0x83, 0x91, 0x61, 0xbd, 0x1d, 0xb8, 0xce, 0xeb, 0xb8, 0x36, 0x73, 0x61, 0x12, 0xc2, 0xf6, 0x59, + 0x28, 0x11, 0x24, 0xd1, 0x49, 0xbc, 0xbf, 0xef, 0xf2, 0x76, 0x88, 0x6a, 0xf9, 0x4f, 0x5e, 0x66, + 0x54, 0x45, 0x58, 0xd6, 0xb4, 0x50, 0x38, 0x92, 0x94, 0x79, 0xf9, 0x18, 0x53, 0x9b, 0x09, 0xb0, + 0xa4, 0xc1, 0x4b, 0x82, 0x06, 0xb2, 0x8e, 0x27, 0x79, 0xc5, 0xae, 0x5b, 0x57, 0xf5, 0xd9, 0xde, + 0x9e, 0xcf, 0xce, 0x43, 0x59, 0x7a, 0x91, 0xb0, 0x6f, 0x80, 0xbe, 0x65, 0x00, 0x7d, 0x14, 0x0f, + 0xc4, 0x07, 0x43, 0x49, 0xfe, 0xee, 0xcb, 0x97, 0x80, 0x5b, 0xfa, 0x99, 0xdb, 0x4b, 0x81, 0xd1, + 0x2a, 0x61, 0x7f, 0x2b, 0xaa, 0x07, 0x32, 0x55, 0x76, 0xc8, 0x3d, 0x1a, 0x9e, 0xb7, 0x24, 0xf9, + 0x17, 0x88, 0x38, 0xac, 0xeb, 0x72, 0xa9, 0x84, 0xa2, 0xbe, 0xa9, 0xed, 0x43, 0xa3, 0x56, 0x52, + 0x0f, 0x8a, 0xe9, 0x6c, 0xb6, 0x3c, 0x7b, 0xa3, 0x84, 0x68, 0x33, 0xf5, 0x6b, 0xca, 0xc7, 0x3c, + 0x08, 0x52, 0x6a, 0x14, 0xd4, 0xa7, 0x08, 0x2e, 0x1e, 0xd8, 0xb4, 0xef, 0xdf, 0xb4, 0xab, 0xdf, + 0xd9, 0x40, 0x3c, 0xe2, 0x8f, 0x8a, 0x25, 0xe0, 0xe5, 0x83, 0x6f, 0x18, 0xf5, 0x51, 0x8c, 0x0d, + 0xdf, 0xea, 0x9f, 0xa2, 0x43, 0xcf, 0x0f, 0x7f, 0xfc, 0xc6, 0xb5, 0x3c, 0x7b, 0x4a, 0x2c, 0x3d, + 0x16, 0x32, 0xcd, 0xe4, 0x0f, 0x01, 0xa7, 0xa8, 0x59, 0x87, 0x2d, 0x32, 0x47, 0x79, 0x3b, 0xe0, + 0xc2, 0x1f, 0xff, 0xb1, 0xa0, 0x32, 0x65, 0xc4, 0x68, 0xa3, 0x78, 0x1f, 0x35, 0x30, 0x00, 0xa5, + 0x8f, 0x03, 0x8b, 0x83, 0x24, 0x2b, 0x4b, 0x5e, 0xf9, 0xaa, 0xde, 0xeb, 0x83, 0x67, 0x91, 0xad, + 0xd5, 0x29, 0xb0, 0xc8, 0xbe, 0x49, 0x92, 0xae, 0xaa, 0xbb, 0xf7, 0x75, 0x59, 0xb1, 0xef, 0x98, + 0x32, 0x55, 0x57, 0xb1, 0x71, 0x1a, 0x3e, 0x1b, 0x14, 0x7e, 0xb6, 0xbb, 0x04, 0x15, 0x48, 0x52, + 0xdb, 0x4d, 0xd9, 0x4d, 0x3c, 0x63, 0x30, 0x47, 0x1f, 0x7f, 0x53, 0x7c, 0x05, 0xce, 0xf2, 0xfd, + 0x7d, 0xd9, 0x06, 0x04, 0xcf, 0xde, 0xea, 0x76, 0xef, 0xc2, 0x7d, 0x03, 0xac, 0x00, 0x32, 0xbc, + 0x58, 0x0a, 0xe7, 0xdf, 0xff, 0xed, 0x08, 0xcc, 0xa0, 0x31, 0xf9, 0xef, 0xff, 0xc9, 0x62, 0x01, + 0x11, 0xf7, 0xe5, 0x3c, 0xce, 0x9d, 0x69, 0xcc, 0x93, 0xc8, 0x81, 0x9b, 0xe2, 0x23, 0x02, 0x63, + 0x57, 0x4b, 0x19, 0x2d, 0x52, 0x96, 0xb8, 0x3d, 0xc0, 0xbc, 0xc8, 0x39, 0x16, 0x28, 0x5c, 0x19, + 0x8c, 0x5c, 0x2b, 0xb7, 0xeb, 0x99, 0xd4, 0x89, 0x62, 0x82, 0x3f, 0xc7, 0xc3, 0x3e, 0x11, 0x4a, + 0xca, 0xd0, 0x8c, 0x04, 0x84, 0xb6, 0x0a, 0xcd, 0xa3, 0x92, 0xb0, 0xba, 0x48, 0x82, 0x42, 0x88, + 0x05, 0xe0, 0x82, 0x47, 0x94, 0x22, 0x00, 0x6d, 0x47, 0xd9, 0x78, 0xa2, 0x1a, 0xc5, 0xb5, 0x9a, + 0x90, 0xf7, 0xf7, 0x86, 0x57, 0xec, 0x56, 0x6f, 0x03, 0x4b, 0x0d, 0x95, 0xff, 0xbc, 0x15, 0xa1, + 0xd0, 0x45, 0x17, 0x3f, 0x8a, 0x88, 0x6f, 0x61, 0x9a, 0x1c, 0xa4, 0xf6, 0x19, 0x40, 0x8d, 0xd1, + 0xec, 0xdf, 0x11, 0x60, 0x50, 0x96, 0x5d, 0x0f, 0xa6, 0xa2, 0x3a, 0x6d, 0x9a, 0x08, 0x4d, 0xfa, + 0xcb, 0x97, 0x03, 0xba, 0xa7, 0xf3, 0x0e, 0x2d, 0xef, 0x1e, 0x12, 0xba, 0x26, 0x02, 0xfa, 0xe7, + 0x36, 0x5c, 0xaf, 0x50, 0x7f, 0xbc, 0x38, 0xf9, 0x24, 0xd5, 0xaa, 0xeb, 0x3c, 0xee, 0xef, 0xa9, + 0xa6, 0x0f, 0xf1, 0x03, 0xcc, 0x53, 0x34, 0x32, 0x5d, 0xb2, 0x23, 0xab, 0xfc, 0xd0, 0x75, 0xab, + 0xbf, 0xc8, 0x81, 0xc6, 0x1a, 0x94, 0xe1, 0x2e, 0x4b, 0xde, 0x34, 0xb0, 0xb5, 0xd1, 0xb5, 0x53, + 0x7a, 0xe1, 0xe1, 0x10, 0x52, 0x3d, 0x98, 0x2a, 0x09, 0xc1, 0x0b, 0x01, 0x94, 0x51, 0x42, 0x68, + 0xd3, 0x26, 0x4a, 0xf5, 0x4d, 0xa6, 0x8f, 0x80, 0x37, 0x7d, 0xf9, 0xd2, 0x4c, 0xa0, 0x16, 0x05, + 0x75, 0x07, 0x7a, 0x29, 0xdd, 0x16, 0xbf, 0xbd, 0xd7, 0x54, 0x50, 0xe6, 0xd7, 0xf8, 0x40, 0x36, + 0x81, 0xc5, 0x9a, 0x42, 0x05, 0xe3, 0x1f, 0x1c, 0x2d, 0x96, 0xbb, 0x46, 0x36, 0xc5, 0xb1, 0x5a, + 0xf4, 0x0f, 0xe6, 0x89, 0x4e, 0xb8, 0x14, 0xbd, 0x35, 0x0d, 0xe9, 0x1b, 0x9e, 0x29, 0xcf, 0x54, + 0xe3, 0xc8, 0xaa, 0x07, 0x81, 0x58, 0xc5, 0xee, 0x8f, 0xe7, 0x49, 0xb2, 0xe2, 0x8a, 0xbc, 0xa7, + 0x74, 0x88, 0x83, 0xf5, 0x64, 0xc5, 0xf7, 0xf1, 0xcf, 0xde, 0xfb, 0x96, 0xce, 0x78, 0x78, 0x2f, + 0x14, 0x64, 0x69, 0x3d, 0xad, 0x57, 0xd2, 0x71, 0xd7, 0xd6, 0x1c, 0x40, 0xb7, 0x8d, 0x9a, 0xd3, + 0x97, 0xba, 0x7e, 0x45, 0x40, 0x77, 0x6c, 0xd8, 0x0e, 0xbb, 0xfa, 0xaa, 0x28, 0x19, 0xa5, 0x2c, + 0xf5, 0x36, 0xd5, 0x7d, 0x9a, 0x97, 0x4f, 0xdb, 0xa9, 0x4a, 0x2e, 0x56, 0x7b, 0xba, 0x4f, 0xfc, + 0xac, 0x82, 0x04, 0x72, 0x54, 0x45, 0x59, 0x99, 0xfc, 0x6f, 0x94, 0x73, 0x7b, 0xbb, 0xd1, 0x58, + 0xd6, 0x30, 0xb5, 0x94, 0x19, 0xd9, 0x75, 0x20, 0xf2, 0x31, 0x65, 0x63, 0x95, 0x33, 0xd9, 0xed, + 0x47, 0xab, 0x90, 0xc0, 0x9f, 0x29, 0x0b, 0xc9, 0xd8, 0xb6, 0x56, 0x91, 0x34, 0x62, 0xa6, 0x0c, + 0xd1, 0x7d, 0x67, 0x95, 0x8c, 0x39, 0xa6, 0x26, 0x3e, 0xd2, 0xa5, 0x5e, 0xee, 0x53, 0x4e, 0x60, + 0x6a, 0x78, 0xc2, 0x6f, 0x00, 0x1e, 0x8f, 0x9d, 0x22, 0xd1, 0x67, 0x17, 0xe3, 0x96, 0x5f, 0x33, + 0xb6, 0x56, 0xe3, 0xea, 0x00, 0x91, 0x5f, 0xe8, 0x02, 0x0b, 0x55, 0xcf, 0x68, 0x1f, 0x97, 0xf9, + 0xcf, 0x06, 0x5e, 0x50, 0x69, 0xb1, 0xca, 0x61, 0x3e, 0x76, 0xae, 0xb6, 0xd9, 0xfa, 0xd5, 0xf3, + 0x75, 0x2c, 0x94, 0xc1, 0x12, 0x55, 0x2a, 0x5b, 0xd8, 0xb5, 0xa4, 0x57, 0x67, 0x98, 0xfa, 0x47, + 0x18, 0x4f, 0xe2, 0x98, 0x96, 0x93, 0x6d, 0x70, 0x3b, 0x14, 0xb3, 0x6a, 0xf2, 0x6b, 0xca, 0x16, + 0xc6, 0x77, 0x62, 0x56, 0xea, 0xdf, 0x66, 0xb1, 0xe4, 0x94, 0x95, 0xd6, 0x95, 0xdd, 0xe6, 0xdc, + 0xcd, 0xc5, 0x46, 0x88, 0x79, 0x97, 0x31, 0xa7, 0x0f, 0xb1, 0x26, 0x25, 0x70, 0x16, 0xbd, 0x8b, + 0xb9, 0xa3, 0x3e, 0xbf, 0xe3, 0x93, 0x57, 0x2a, 0x55, 0x80, 0x89, 0xd5, 0xe5, 0xda, 0x7d, 0xda, + 0x54, 0x8d, 0x52, 0xd4, 0x1a, 0x8e, 0x6a, 0xdf, 0x74, 0x78, 0x1f, 0xd1, 0x4d, 0xbb, 0x8a, 0xb7, + 0x9b, 0xc7, 0xff, 0xb2, 0xad, 0xe6, 0xa3, 0x96, 0x62, 0x73, 0x49, 0x2c, 0xdd, 0x3d, 0xaa, 0x57, + 0x79, 0x0d, 0xdc, 0xef, 0xa1, 0x33, 0x80, 0x7e, 0x2d, 0xf8, 0xc1, 0xb2, 0x42, 0x4e, 0xeb, 0x74, + 0xed, 0x89, 0x7a, 0xf0, 0x81, 0x8a, 0x31, 0x73, 0x9e, 0xc7, 0xcc, 0x79, 0x5e, 0x6e, 0xe9, 0xbf, + 0xdd, 0x87, 0x78, 0x23, 0x53, 0xc3, 0xc4, 0x8a, 0x1a, 0xa6, 0xdc, 0x86, 0xd5, 0x6c, 0xf7, 0x91, + 0x90, 0xda, 0x15, 0xf8, 0xf9, 0xb7, 0xeb, 0x57, 0x68, 0x8e, 0x31, 0x11, 0xd6, 0x2c, 0x80, 0x48, + 0x1f, 0x04, 0x6f, 0x64, 0x95, 0x5e, 0x4b, 0xaf, 0x8c, 0xb0, 0xca, 0x7c, 0x06, 0xfa, 0x33, 0x35, + 0xd7, 0x58, 0xa5, 0x48, 0xd3, 0x0b, 0x17, 0x7f, 0x4b, 0x0b, 0x9c, 0x42, 0x72, 0xba, 0xb6, 0x35, + 0x51, 0xaa, 0xfd, 0xb4, 0x42, 0xdb, 0xc6, 0xc8, 0x99, 0xdd, 0x41, 0x81, 0xb6, 0x83, 0x5b, 0xdb, + 0x0c, 0xa9, 0x80, 0x32, 0x3d, 0x0b, 0x0f, 0x8e, 0x8f, 0x2f, 0xd2, 0x00, 0xfe, 0x5a, 0x18, 0x2e, + 0x36, 0xac, 0x1b, 0xda, 0x01, 0xfb, 0x0c, 0x98, 0xf4, 0x9e, 0xda, 0x00, 0x33, 0x8b, 0xa6, 0xf6, + 0xa2, 0xa6, 0xfa, 0xe5, 0x53, 0xed, 0xf1, 0xa3, 0xc9, 0x49, 0xd6, 0x59, 0xb6, 0xc6, 0x32, 0xd5, + 0x4a, 0x94, 0x9d, 0x26, 0x3f, 0x6d, 0x29, 0x5a, 0xe3, 0x9f, 0x65, 0xa3, 0xcb, 0x4f, 0x1f, 0xdd, + 0x82, 0xf7, 0x21, 0x90, 0x95, 0xed, 0xe1, 0x60, 0x45, 0xd9, 0x34, 0xcf, 0x8e, 0x3e, 0x5b, 0x3e, + 0x9a, 0x53, 0x52, 0x41, 0x9a, 0x31, 0x68, 0x55, 0x52, 0x56, 0x21, 0x6c, 0x70, 0xbc, 0xdf, 0x5e, + 0x0e, 0xf0, 0xd9, 0x30, 0x56, 0x66, 0xab, 0x83, 0xd7, 0x6d, 0xa9, 0x8b, 0x30, 0x20, 0x7b, 0x4f, + 0x81, 0xd9, 0xb2, 0xd7, 0x9f, 0x0d, 0xb1, 0x35, 0x17, 0xc2, 0x9b, 0xb6, 0x43, 0x59, 0x67, 0x2c, + 0xab, 0xb6, 0x82, 0x59, 0x78, 0x67, 0x17, 0xba, 0x6c, 0xfe, 0x8b, 0x1f, 0xa6, 0xce, 0x5a, 0xac, + 0x9c, 0x5b, 0x96, 0x02, 0x23, 0x25, 0xc0, 0x44, 0xea, 0x47, 0xf3, 0xec, 0x6f, 0xad, 0x31, 0x18, + 0x06, 0xd3, 0x2e, 0x91, 0xe5, 0xdc, 0x81, 0xfb, 0x85, 0xef, 0xea, 0x1f, 0x14, 0x75, 0x7d, 0xfc, + 0x44, 0xc2, 0xc7, 0xcf, 0xcf, 0xe4, 0x37, 0x12, 0x3c, 0xb3, 0x31, 0x44, 0x8b, 0x00, 0x29, 0x7d, + 0x49, 0xeb, 0xfa, 0x6e, 0xf9, 0xad, 0xac, 0xd7, 0xe4, 0x45, 0xc2, 0x19, 0x38, 0x23, 0x1f, 0x19, + 0xfd, 0x88, 0x0f, 0x71, 0x5b, 0x26, 0xd7, 0x09, 0xb3, 0xaa, 0x51, 0xb5, 0xc2, 0xfe, 0x87, 0x3d, + 0x0e, 0x87, 0x37, 0xbc, 0x30, 0xb9, 0xc3, 0x0b, 0xb3, 0x64, 0xb9, 0xc8, 0xfe, 0xfa, 0xec, 0x81, + 0xe2, 0x48, 0xab, 0x70, 0xb2, 0xf8, 0xe0, 0xd2, 0xb3, 0xdd, 0x6b, 0x08, 0x8c, 0x77, 0xf8, 0x67, + 0xe9, 0x8e, 0xec, 0x0e, 0xab, 0xa6, 0xaf, 0xe4, 0xc5, 0x8e, 0x04, 0xa2, 0xf6, 0xd1, 0x5a, 0x2a, + 0x0d, 0x81, 0x0c, 0x79, 0x2b, 0x7d, 0x6a, 0x43, 0xa0, 0x27, 0x20, 0x15, 0x7f, 0xea, 0x50, 0x7d, + 0xb3, 0x88, 0xbf, 0x75, 0x48, 0xff, 0x77, 0x6a, 0xff, 0x0b, 0xb1, 0x78, 0x2f, 0xcf, 0x5f, 0x6d, + 0x00, 0x00 +}; diff --git a/wled00/html_settings.h b/wled00/html_settings.h index 2f727daf28..df22118f06 100644 --- a/wled00/html_settings.h +++ b/wled00/html_settings.h @@ -6,476 +6,2238 @@ */ // Autogenerated from wled00/data/style.css, do not edit!! -const char PAGE_settingsCss[] PROGMEM = R"=====()====="; +const uint16_t PAGE_settingsCss_length = 883; +const uint8_t PAGE_settingsCss[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xad, 0x56, 0x5d, 0x8b, 0xab, 0x48, + 0x10, 0xfd, 0x2b, 0x2e, 0x21, 0x30, 0x17, 0xa2, 0xa8, 0xd1, 0x4c, 0xb6, 0x65, 0x61, 0xd9, 0xf7, + 0x7d, 0xbb, 0x2c, 0x0b, 0xcb, 0x3c, 0xb4, 0x76, 0x19, 0x9b, 0xf4, 0x17, 0xdd, 0xed, 0xc4, 0x5c, + 0xf1, 0xbf, 0x2f, 0xdd, 0xea, 0x68, 0x32, 0x32, 0xf7, 0xe5, 0x12, 0x12, 0xb4, 0xab, 0xad, 0x3e, + 0xa7, 0xea, 0xd4, 0x31, 0x8d, 0xe5, 0xac, 0xb7, 0xb2, 0xad, 0x9a, 0x10, 0x57, 0x96, 0x4a, 0x81, + 0x38, 0x16, 0x54, 0xb5, 0x0c, 0xbb, 0x9b, 0xa1, 0x94, 0xe4, 0xde, 0xd7, 0x52, 0xd8, 0xb0, 0xc6, + 0x9c, 0xb2, 0x3b, 0xfa, 0x07, 0x34, 0xc1, 0x02, 0x1f, 0x0c, 0x16, 0x26, 0x34, 0xa0, 0x69, 0x5d, + 0xf8, 0xb0, 0xa1, 0x3f, 0x00, 0x25, 0x1a, 0x78, 0x61, 0xa1, 0xb3, 0x21, 0x66, 0xf4, 0x22, 0x50, + 0x05, 0xc2, 0x82, 0x2e, 0x4a, 0x5c, 0x5d, 0x2f, 0x5a, 0xb6, 0x82, 0xa0, 0x5d, 0x9a, 0xa6, 0x45, + 0x25, 0x99, 0xd4, 0x68, 0x57, 0xd7, 0x75, 0xc1, 0xa8, 0x80, 0xb0, 0x01, 0x7a, 0x69, 0x2c, 0x4a, + 0xe3, 0x78, 0x5f, 0x70, 0xac, 0x2f, 0x54, 0xa0, 0x78, 0x68, 0x74, 0x5f, 0x4a, 0x4d, 0x40, 0x87, + 0xd3, 0xf6, 0xd3, 0xe9, 0x34, 0x34, 0x3a, 0x32, 0x9c, 0xf5, 0x37, 0x4a, 0x6c, 0x83, 0xd2, 0x53, + 0xac, 0xba, 0x01, 0x1f, 0x30, 0x6a, 0xe4, 0x3b, 0xe8, 0x7e, 0xda, 0x97, 0x9e, 0xeb, 0x11, 0x03, + 0x81, 0x4a, 0x6a, 0x4f, 0x03, 0x09, 0x29, 0x60, 0x88, 0x4a, 0x2b, 0x0e, 0x65, 0x6b, 0xad, 0x14, + 0xfd, 0x1a, 0xd2, 0xf1, 0x78, 0x5c, 0x43, 0xfa, 0x09, 0xdb, 0x11, 0x14, 0x8a, 0x8e, 0x55, 0x13, + 0x18, 0xc9, 0x28, 0x09, 0x7c, 0x82, 0x09, 0xab, 0xc6, 0x84, 0xb6, 0x06, 0xa5, 0x99, 0xea, 0x0a, + 0x42, 0x8d, 0x62, 0xf8, 0x8e, 0xa8, 0xf0, 0x2c, 0x4b, 0x26, 0xab, 0xeb, 0xaa, 0x58, 0x69, 0xac, + 0xba, 0x99, 0x6e, 0x92, 0xaa, 0x2e, 0x38, 0x8f, 0xdf, 0x42, 0x61, 0x42, 0xa8, 0xb8, 0x20, 0x77, + 0xef, 0x02, 0x05, 0xa7, 0x22, 0x1c, 0x29, 0x67, 0x2e, 0x5e, 0xb5, 0xda, 0x48, 0x8d, 0x94, 0xa4, + 0xbe, 0xba, 0x9b, 0x5c, 0x47, 0x9a, 0xbe, 0x58, 0xab, 0x74, 0xcf, 0x28, 0x1d, 0x82, 0x55, 0xf7, + 0xf2, 0xc7, 0xb3, 0x56, 0xf8, 0xe2, 0xc0, 0x7d, 0x12, 0x57, 0xef, 0x9d, 0xa9, 0xb0, 0xe8, 0xc7, + 0xf5, 0xd0, 0x4a, 0x85, 0x42, 0xbf, 0x1c, 0x59, 0xa9, 0xb4, 0xbc, 0xf5, 0x6e, 0x25, 0x2e, 0x94, + 0x34, 0xd4, 0x83, 0x31, 0x96, 0x56, 0xd7, 0xfb, 0x4a, 0x01, 0x73, 0x37, 0x9d, 0x0e, 0x7e, 0x84, + 0x54, 0x10, 0xe8, 0x50, 0x32, 0x44, 0x4c, 0x5c, 0xa7, 0x76, 0xa3, 0x78, 0x88, 0x1a, 0x60, 0xea, + 0xaf, 0x7e, 0x25, 0x24, 0x06, 0xb5, 0x5d, 0x92, 0xe2, 0xd2, 0x48, 0xd6, 0x5a, 0x28, 0x46, 0xa0, + 0x5e, 0x06, 0x51, 0x43, 0x09, 0xf4, 0x73, 0xc9, 0xc7, 0x7e, 0x83, 0x9e, 0x45, 0xa1, 0x81, 0x0c, + 0xd1, 0x0d, 0x6b, 0x31, 0x8b, 0xa4, 0xc6, 0xf1, 0x40, 0x85, 0x6a, 0xed, 0x2f, 0x50, 0x42, 0xfe, + 0xa0, 0x84, 0x31, 0x2d, 0x22, 0xd4, 0xe0, 0x92, 0x01, 0x99, 0x0f, 0x3c, 0x9f, 0xcf, 0x63, 0xe4, + 0x3f, 0x7b, 0x57, 0xf0, 0x87, 0x68, 0x79, 0x09, 0xfa, 0xed, 0xb0, 0x5a, 0x72, 0x74, 0xdf, 0x0e, + 0x06, 0x18, 0x54, 0xb6, 0x5f, 0xba, 0xc2, 0x81, 0xd0, 0x96, 0xcf, 0x8d, 0x48, 0x55, 0xb7, 0x91, + 0x66, 0x1a, 0x88, 0x0c, 0xf8, 0x46, 0x30, 0xea, 0xba, 0x79, 0x62, 0x92, 0x38, 0xde, 0x7c, 0x3e, + 0xfa, 0xd8, 0x71, 0xce, 0xb7, 0x37, 0xcc, 0xf1, 0x53, 0xb6, 0x1d, 0xe7, 0x53, 0x3c, 0x3f, 0x6d, + 0xc7, 0x4d, 0xbf, 0x08, 0x78, 0x13, 0xc0, 0xc7, 0x86, 0x27, 0x84, 0x55, 0x03, 0xd5, 0xb5, 0x94, + 0xdd, 0x5b, 0x6f, 0x35, 0x16, 0xa6, 0x96, 0x9a, 0x23, 0x53, 0x61, 0x06, 0x2f, 0x49, 0x94, 0x7f, + 0x9b, 0xca, 0x12, 0x6a, 0x6f, 0x20, 0x5e, 0x88, 0x96, 0x04, 0x9b, 0x8f, 0x3f, 0xec, 0xd4, 0xf0, + 0x0e, 0xda, 0xae, 0xcf, 0xa9, 0x29, 0x83, 0xb7, 0x55, 0xd9, 0x13, 0x47, 0x64, 0x6a, 0xc6, 0x52, + 0xfb, 0xe2, 0x97, 0xab, 0x65, 0x3c, 0x22, 0x52, 0xd4, 0xcd, 0x54, 0x37, 0x8d, 0x5e, 0xe2, 0x27, + 0xd3, 0xeb, 0xdf, 0x39, 0x5b, 0xcd, 0xe4, 0x0d, 0x01, 0x63, 0x54, 0x19, 0x6a, 0x06, 0xab, 0xfb, + 0xb5, 0x6b, 0x26, 0x71, 0xbc, 0x1f, 0x2c, 0xf9, 0x18, 0x73, 0xa7, 0x90, 0x88, 0xe4, 0x73, 0x39, + 0x35, 0xf0, 0xdf, 0x28, 0x57, 0x52, 0x5b, 0x2c, 0xec, 0x10, 0x55, 0x98, 0xad, 0x49, 0x46, 0xb9, + 0x73, 0xec, 0x47, 0x43, 0x19, 0x76, 0xdf, 0xff, 0xfe, 0x1e, 0x58, 0xa7, 0xde, 0x45, 0x36, 0xfb, + 0x61, 0xc7, 0xcd, 0xe5, 0x71, 0xbe, 0x76, 0x56, 0x62, 0x63, 0x7b, 0xa9, 0x70, 0x45, 0xed, 0x1d, + 0xc5, 0x1b, 0x53, 0x9e, 0x65, 0xd9, 0x93, 0xe7, 0xe4, 0xde, 0x85, 0xac, 0x95, 0xdc, 0x6b, 0xe9, + 0x53, 0x01, 0x47, 0x5c, 0xaf, 0x2b, 0x1b, 0x74, 0x9d, 0x28, 0x26, 0x6c, 0x21, 0xbc, 0x83, 0xb0, + 0xc6, 0x9f, 0xbf, 0xf8, 0x41, 0x4d, 0x3b, 0x20, 0x1b, 0xef, 0x9d, 0xd9, 0x5f, 0xf2, 0x62, 0xd1, + 0x8e, 0xbf, 0x62, 0xd8, 0xc2, 0xbf, 0x2f, 0x61, 0x1e, 0xef, 0x9d, 0x82, 0xe6, 0xaa, 0xff, 0x1e, + 0xef, 0x0b, 0x67, 0x34, 0x28, 0x77, 0x74, 0x3d, 0xb9, 0xc8, 0x34, 0xf2, 0xf6, 0xc1, 0x30, 0xd9, + 0xf2, 0xb1, 0x53, 0x56, 0x60, 0x41, 0xf9, 0xe8, 0xbd, 0x35, 0x26, 0x40, 0x45, 0x10, 0xe5, 0xe6, + 0xb0, 0x5c, 0x06, 0xa9, 0xfb, 0xf1, 0x92, 0x33, 0x73, 0xd5, 0x9c, 0x37, 0x49, 0xfd, 0x65, 0xe6, + 0x32, 0x4d, 0x36, 0x33, 0x0f, 0x7f, 0x3a, 0x4b, 0xc0, 0x81, 0xa9, 0x34, 0x80, 0x08, 0xb0, 0x20, + 0xc1, 0xcb, 0x42, 0xe2, 0xf5, 0xf4, 0xaa, 0xba, 0x6f, 0xfd, 0x4a, 0xd9, 0xc0, 0x31, 0x65, 0x0f, + 0x4e, 0xe3, 0xb5, 0x7e, 0xf8, 0xda, 0x8d, 0x14, 0x36, 0xe6, 0x26, 0x35, 0x79, 0xb2, 0x28, 0xf6, + 0xd9, 0xb2, 0x9e, 0x87, 0xe6, 0x6b, 0x7c, 0xd9, 0x39, 0x7e, 0xc2, 0xf7, 0xc9, 0x22, 0xe2, 0x9f, + 0x58, 0xc4, 0xf1, 0xc9, 0x04, 0xc7, 0xd1, 0x9d, 0xfe, 0x13, 0xb8, 0x37, 0xef, 0xb0, 0x2b, 0xad, + 0x30, 0xc1, 0x34, 0xbd, 0x93, 0x86, 0x33, 0x17, 0x18, 0xfe, 0x07, 0x39, 0x38, 0xc0, 0xa3, 0xef, + 0x08, 0x00, 0x00 +}; // Autogenerated from wled00/data/settings.htm, do not edit!! -const char PAGE_settings[] PROGMEM = R"=====(WLED Settings -
-
-
%DMXMENU%
)====="; +const uint16_t PAGE_settings_length = 1121; +const uint8_t PAGE_settings[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xad, 0x56, 0xdb, 0x72, 0xdb, 0x36, + 0x10, 0x7d, 0xd7, 0x57, 0xc0, 0xc8, 0xd4, 0x21, 0xc7, 0x14, 0x75, 0x49, 0xa7, 0xd3, 0x92, 0x02, + 0x3d, 0x8d, 0xed, 0xa4, 0xee, 0x38, 0x13, 0x4f, 0x1d, 0x27, 0xed, 0xb4, 0x7d, 0x80, 0x80, 0x25, + 0x89, 0x18, 0x04, 0x38, 0xc0, 0x52, 0x96, 0xaa, 0xe8, 0xdf, 0x3b, 0xa0, 0x64, 0xd9, 0x49, 0xfc, + 0xd0, 0x34, 0x79, 0x91, 0x84, 0xe5, 0xe2, 0xec, 0x9e, 0xb3, 0x17, 0x6a, 0x76, 0x70, 0xfa, 0xfa, + 0xe4, 0xcd, 0x1f, 0x97, 0x67, 0xa4, 0xc6, 0x46, 0x17, 0xb3, 0xf0, 0x49, 0x34, 0x37, 0x15, 0xa3, + 0x60, 0x68, 0x31, 0xab, 0x81, 0xcb, 0x62, 0xd6, 0x00, 0x72, 0x22, 0x6a, 0xee, 0x3c, 0x20, 0xa3, + 0xd7, 0x6f, 0x5e, 0x0c, 0x7f, 0xa4, 0x3b, 0xeb, 0x40, 0x58, 0x83, 0x60, 0x90, 0xd1, 0x5b, 0x25, + 0xb1, 0x66, 0x12, 0x16, 0x4a, 0xc0, 0xb0, 0x3f, 0x24, 0xca, 0x28, 0x54, 0x5c, 0x0f, 0xbd, 0xe0, + 0x1a, 0xd8, 0x24, 0x69, 0xf8, 0x52, 0x35, 0x5d, 0xb3, 0x3f, 0x77, 0x1e, 0x5c, 0x7f, 0xe0, 0x73, + 0x0d, 0xcc, 0x58, 0x4a, 0x06, 0x86, 0x37, 0xc0, 0xe8, 0x42, 0xc1, 0x6d, 0x6b, 0x1d, 0xd2, 0x62, + 0x86, 0x0a, 0x35, 0x14, 0xef, 0x2e, 0xce, 0x4e, 0xc9, 0x15, 0x20, 0x2a, 0x53, 0xf9, 0xd9, 0x68, + 0x6b, 0x9c, 0x79, 0xe1, 0x54, 0x8b, 0xc5, 0x60, 0xc1, 0x1d, 0xd1, 0x56, 0xa8, 0x36, 0x91, 0x4c, + 0x5a, 0xd1, 0x35, 0x60, 0x30, 0xd1, 0x56, 0xb0, 0x83, 0x49, 0xf8, 0x6a, 0x9d, 0x45, 0xcb, 0x68, + 0x8d, 0xd8, 0x66, 0x34, 0x2f, 0x3b, 0x23, 0x50, 0x59, 0x43, 0xaa, 0x73, 0x19, 0x61, 0xbc, 0x76, + 0x80, 0x9d, 0x33, 0x44, 0xa6, 0x15, 0xe0, 0x99, 0x86, 0x70, 0xf7, 0xf9, 0xaa, 0x7f, 0xb4, 0xd9, + 0xbb, 0x6a, 0xcb, 0xe5, 0xaf, 0x57, 0x11, 0x26, 0x96, 0x1d, 0x8c, 0xe3, 0xb5, 0x06, 0x24, 0xc0, + 0x64, 0x2a, 0x1c, 0x70, 0x84, 0xdd, 0xa5, 0x88, 0x6e, 0xd3, 0xa1, 0x71, 0x0e, 0xa9, 0x07, 0xfc, + 0x19, 0xd1, 0xa9, 0x79, 0x87, 0x10, 0x51, 0xef, 0x04, 0x4d, 0x30, 0x4e, 0x3e, 0xb5, 0xe3, 0xaa, + 0x05, 0x9a, 0x50, 0x84, 0x25, 0x8e, 0xde, 0xf3, 0x05, 0xbf, 0x03, 0xf8, 0xcc, 0x91, 0xfb, 0x95, + 0x11, 0x34, 0xb1, 0x71, 0x22, 0xd3, 0xb9, 0x95, 0xab, 0x94, 0xb7, 0x2d, 0x18, 0x79, 0x52, 0x2b, + 0x2d, 0x23, 0x08, 0xfe, 0x5c, 0xca, 0xb3, 0x05, 0x18, 0xbc, 0x50, 0x1e, 0xc1, 0x80, 0x8b, 0x68, + 0xc8, 0x99, 0x26, 0x51, 0xcc, 0x8a, 0xf5, 0x4b, 0xc0, 0xb7, 0x51, 0xbc, 0x79, 0xdc, 0x0f, 0x9c, + 0xb3, 0x8e, 0x26, 0xc8, 0x8a, 0xb5, 0xb0, 0xc6, 0x5b, 0x0d, 0xa9, 0xb6, 0x55, 0x44, 0xcf, 0x82, + 0x9d, 0xec, 0xc8, 0x2b, 0x53, 0x91, 0x52, 0x69, 0xe8, 0x69, 0x70, 0x0d, 0x0e, 0x23, 0x7a, 0xb1, + 0xb3, 0xdb, 0x92, 0x08, 0x6b, 0x4a, 0x55, 0x75, 0x8e, 0xf7, 0x6a, 0x6d, 0x69, 0x90, 0x92, 0x2b, + 0x0d, 0x32, 0xfd, 0xcb, 0x9c, 0x1b, 0x61, 0x9b, 0x56, 0x03, 0x02, 0x69, 0x79, 0x05, 0x44, 0x72, + 0xe4, 0x07, 0x34, 0xde, 0x3c, 0x10, 0xf8, 0x2a, 0xda, 0xaa, 0x8a, 0xec, 0x56, 0x19, 0x69, 0x6f, + 0x53, 0x6d, 0x45, 0x0f, 0x96, 0xab, 0x32, 0xa2, 0x21, 0x72, 0x46, 0x19, 0xc3, 0xb4, 0x2f, 0xa5, + 0xb0, 0x3a, 0xee, 0xab, 0x3b, 0x4e, 0xa2, 0xbe, 0xec, 0x2c, 0x78, 0xeb, 0x2b, 0xb4, 0x8e, 0x57, + 0x10, 0xea, 0x78, 0x8e, 0xd0, 0x04, 0x05, 0xc4, 0x79, 0x4b, 0xe3, 0xf8, 0xc3, 0x87, 0x9d, 0x5b, + 0xeb, 0x6c, 0xd3, 0x62, 0x44, 0x5f, 0x28, 0x0d, 0xe4, 0x95, 0x95, 0x90, 0x92, 0x4b, 0x0d, 0xdc, + 0x03, 0x01, 0x83, 0xe0, 0x48, 0xdf, 0x65, 0xe7, 0x97, 0x07, 0x34, 0x4e, 0x3e, 0x42, 0xf4, 0x1f, + 0x23, 0x26, 0x3d, 0x5a, 0x1c, 0xe7, 0xa0, 0x3d, 0xf4, 0x69, 0xdb, 0x90, 0x1a, 0xc7, 0x3a, 0x74, + 0x6f, 0x02, 0xcc, 0xa6, 0x5e, 0x2b, 0x01, 0xd1, 0x24, 0xb1, 0x29, 0x18, 0xe9, 0xdf, 0x29, 0xac, + 0x23, 0x3a, 0xa2, 0xf1, 0xf1, 0x70, 0x92, 0x2d, 0xac, 0x92, 0x64, 0x1c, 0xa7, 0xbe, 0xd5, 0x0a, + 0x7b, 0x6b, 0x0e, 0xa9, 0x06, 0x53, 0x61, 0x5d, 0x4c, 0x0e, 0x0f, 0xa3, 0x7d, 0xc3, 0xde, 0xb3, + 0x4d, 0x76, 0x6c, 0xb7, 0x2c, 0x30, 0xad, 0xad, 0xc7, 0x10, 0xea, 0x28, 0xc2, 0x34, 0x8c, 0xc9, + 0x31, 0xcd, 0xe8, 0xd1, 0xf6, 0x67, 0x46, 0x69, 0x7c, 0x44, 0x47, 0xf4, 0x08, 0xfe, 0x1c, 0xff, + 0x1d, 0x6f, 0x76, 0x8d, 0x5b, 0x01, 0x5e, 0xff, 0x76, 0x11, 0xd1, 0x91, 0xdf, 0x8d, 0xd0, 0xc8, + 0xa7, 0xef, 0xfd, 0x71, 0xcb, 0xc6, 0x34, 0x4e, 0x0e, 0x26, 0x0f, 0x0a, 0xb1, 0xf3, 0xdc, 0xcf, + 0x45, 0x48, 0xe7, 0xf8, 0x2e, 0xa5, 0x23, 0x3a, 0x1a, 0xd1, 0xa3, 0x3e, 0x8b, 0x3e, 0x0e, 0x6e, + 0x06, 0xb3, 0xd1, 0x6e, 0x08, 0x67, 0x1e, 0x57, 0x1a, 0x8a, 0x41, 0x68, 0xcf, 0x75, 0xe8, 0xe8, + 0x21, 0xd7, 0xaa, 0x32, 0x99, 0xe8, 0xa5, 0xcd, 0xe7, 0x5c, 0xdc, 0x54, 0xce, 0x76, 0x46, 0x66, + 0x4f, 0xa6, 0xd3, 0x69, 0x5e, 0x83, 0xaa, 0x6a, 0xcc, 0x26, 0xe3, 0x71, 0xbb, 0xcc, 0x1b, 0xee, + 0x2a, 0x65, 0xb2, 0xf1, 0x26, 0xac, 0x9f, 0xf5, 0x70, 0x58, 0x67, 0x3f, 0x2d, 0xea, 0xcd, 0xbc, + 0x43, 0xb4, 0x66, 0xfd, 0xf0, 0xe6, 0xb3, 0x67, 0xcf, 0x72, 0x61, 0xb5, 0x75, 0xd9, 0x93, 0xb2, + 0x2c, 0xf3, 0xd2, 0x1a, 0x1c, 0x96, 0xbc, 0x51, 0x7a, 0x95, 0xbd, 0x05, 0x27, 0xb9, 0xe1, 0xc9, + 0x2f, 0xa0, 0x17, 0x80, 0x4a, 0xf0, 0xc4, 0x73, 0xe3, 0x87, 0x1e, 0x9c, 0x2a, 0x73, 0xa9, 0x7c, + 0xab, 0xf9, 0x2a, 0x9b, 0x6b, 0x2b, 0x6e, 0xf2, 0xb9, 0x75, 0x12, 0x5c, 0x36, 0x69, 0x97, 0xc4, + 0x5b, 0xad, 0x24, 0xe9, 0x71, 0xb7, 0xd6, 0xa1, 0xe3, 0x52, 0x75, 0x3e, 0x5b, 0x70, 0x17, 0x0d, + 0x87, 0x75, 0xbc, 0x8d, 0xe1, 0xd5, 0x3f, 0x90, 0xfd, 0xb0, 0x68, 0x94, 0xb9, 0x4b, 0x7c, 0xff, + 0xbc, 0xdf, 0x74, 0x99, 0xe0, 0x5a, 0x44, 0x93, 0xf1, 0xf8, 0x3b, 0x32, 0x24, 0xdf, 0x8f, 0xdb, + 0x65, 0x7c, 0xc7, 0x69, 0xba, 0xa8, 0x09, 0xef, 0xd0, 0x92, 0x71, 0x2e, 0x3a, 0xe7, 0xad, 0xcb, + 0x5a, 0xab, 0x82, 0x22, 0xbd, 0x74, 0xbd, 0x64, 0xb3, 0xd1, 0x76, 0xcb, 0x06, 0xe5, 0x88, 0x35, + 0xa1, 0x64, 0x8c, 0x5e, 0x45, 0x31, 0x2d, 0x66, 0x5b, 0x09, 0x48, 0xd8, 0x13, 0x8c, 0xfa, 0x6e, + 0xde, 0x28, 0xa4, 0x44, 0x49, 0x46, 0xe7, 0x94, 0x0c, 0xac, 0x11, 0x5a, 0x89, 0x1b, 0xf6, 0xf4, + 0x93, 0x91, 0x61, 0xfb, 0x6a, 0xd3, 0xf8, 0x69, 0xf1, 0x9c, 0x8b, 0x9b, 0xd9, 0x68, 0x0b, 0x54, + 0x90, 0xc7, 0x11, 0xff, 0x03, 0xd4, 0xbe, 0x71, 0x6e, 0x55, 0xa9, 0x02, 0xee, 0x3b, 0xf5, 0x42, + 0x85, 0x8d, 0xdc, 0xb5, 0x9f, 0xa3, 0x0f, 0x3e, 0x86, 0xff, 0x02, 0x74, 0x0d, 0xd2, 0x07, 0xf4, + 0x30, 0x88, 0x97, 0x0e, 0x4a, 0x70, 0x60, 0x04, 0xf8, 0xc1, 0xe7, 0x31, 0x82, 0x0a, 0x53, 0x39, + 0x47, 0x43, 0xbf, 0x82, 0xcc, 0xf4, 0x34, 0x04, 0x9b, 0x9e, 0x92, 0x93, 0x87, 0x0b, 0xec, 0x3e, + 0xd8, 0xe0, 0x71, 0xbd, 0xbe, 0x20, 0x42, 0xd7, 0x8b, 0x35, 0xb8, 0xf6, 0xe0, 0xc8, 0x79, 0xa8, + 0x7b, 0xc9, 0x05, 0x3c, 0xce, 0x46, 0x36, 0xcb, 0x9e, 0x4e, 0xdf, 0x15, 0x8c, 0xde, 0xf5, 0xac, + 0xb1, 0x06, 0xbe, 0x86, 0xa3, 0x6c, 0x96, 0x21, 0x85, 0xd3, 0x57, 0xbf, 0x93, 0xd7, 0x1d, 0xb6, + 0x1d, 0x7e, 0xcb, 0x7a, 0xf5, 0xef, 0xa5, 0xf8, 0x69, 0x71, 0xb5, 0x32, 0xe2, 0x9e, 0xdf, 0x63, + 0xf5, 0xfa, 0xff, 0x04, 0x50, 0x35, 0x10, 0x62, 0xbc, 0x51, 0x0d, 0x90, 0x43, 0xf2, 0x8a, 0x0b, + 0x67, 0xfd, 0x37, 0xad, 0x51, 0x13, 0xe0, 0x43, 0x89, 0x1a, 0x2b, 0xbf, 0x6d, 0xee, 0x1e, 0xb6, + 0xf2, 0x80, 0xe8, 0x9c, 0xc2, 0x15, 0x39, 0x24, 0xd7, 0xad, 0xe4, 0x08, 0xf7, 0xf9, 0x87, 0x70, + 0x56, 0xae, 0xc2, 0x16, 0x08, 0xff, 0xc0, 0xfe, 0x05, 0x0d, 0xe2, 0xc3, 0x1e, 0x91, 0x09, 0x00, + 0x00 +}; // Autogenerated from wled00/data/settings_wifi.htm, do not edit!! -const char PAGE_settings_wifi[] PROGMEM = R"=====(WiFi Settings")); + if (subPage == SUBPAGE_UPDATE) // update + { + sappends('m',SET_F("(\"sip\")[0]"),(char*)F("WLED ")); + olen -= 2; //delete "; + oappend(versionString); + oappend(SET_F("
(")); + #if defined(ARDUINO_ARCH_ESP32) + oappend(ESP.getChipModel()); + #else + oappend("esp8266"); + #endif + oappend(SET_F(" build ")); + oappendi(VERSION); + oappend(SET_F(")\";")); + } + + if (subPage == SUBPAGE_2D) // 2D matrices + { + sappend('v',SET_F("SOMP"),strip.isMatrix); + #ifndef WLED_DISABLE_2D + oappend(SET_F("maxPanels=")); oappendi(WLED_MAX_PANELS); oappend(SET_F(";")); + oappend(SET_F("resetPanels();")); + if (strip.isMatrix) { + if(strip.panels>0){ + sappend('v',SET_F("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience + sappend('v',SET_F("PH"),strip.panel[0].height); + } + sappend('v',SET_F("MPC"),strip.panels); + // panels + for (uint8_t i=0; i