diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..d09c8a60 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile + +# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 +ARG VARIANT="3" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +# [Option] Install Node.js +ARG INSTALL_NODE="true" +ARG NODE_VERSION="lts/*" +RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..f77dc4de --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +{ + "name": "Python 3", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 + "VARIANT": "3", + // Options + "INSTALL_NODE": "true", + "NODE_VERSION": "lts/*" + } + }, + + // Set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "platformio.platformio-ide" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} + diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..83dab5d1 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +layout python-venv python3 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..6f5a5be8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [Aircoookie,blazoncek] +custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek'] diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index c3536b2c..00000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Bug -about: Noticed an issue with your lights? -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. Please quickly search existing issues first! - -**To Reproduce** -Steps to reproduce the behavior, if consistently possible - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**WLED version** - - Board: [e.g. Wemos D1, ESP32 dev] - - Version [e.g. 0.10.0, dev200603] - - Format [e.g. Binary, self-compiled] - -**Additional context** -Anything else you'd like to say about the problem? - -Thank you for your help! diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..285ad419 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,86 @@ +name: Bug Report +description: File a bug report +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Please quickly search existing issues first before submitting a bug. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear and concise description of what the bug is. + placeholder: Tell us what the problem is. + validations: + required: true + - type: textarea + id: how-to-reproduce + attributes: + label: To Reproduce Bug + description: Steps to reproduce the behavior, if consistently possible. + placeholder: Tell us how to make the bug appear. + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + placeholder: Tell us what you expected to happen. + validations: + required: true + - type: dropdown + id: install_format + attributes: + label: Install Method + description: How did you install WLED? + options: + - Binary from WLED.me + - Self-Compiled + validations: + required: true + - type: input + id: version + attributes: + label: What version of WLED? + description: You can find this in by going to Config -> Security & Updates -> Scroll to Bottom. Copy and paste the entire line after "Server message" + placeholder: "e.g. WLED 0.13.1 (build 2203150)" + validations: + required: true + - type: dropdown + id: Board + attributes: + label: Which microcontroller/board are you seeing the problem on? + multiple: true + options: + - ESP8266 + - ESP32 + - ESP32-S3 + - ESP32-S2 + - ESP32-C3 + - Other + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log/trace output + description: Please copy and paste any relevant log output if you have it. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + 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 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..29a2f1b5 --- /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 94f92a61..00000000 --- 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/workflows/wled-ci.yml b/.github/workflows/wled-ci.yml new file mode 100644 index 00000000..2b599e6f --- /dev/null +++ b/.github/workflows/wled-ci.yml @@ -0,0 +1,91 @@ +name: PlatformIO CI + +on: [push, pull_request] + +jobs: + + get_default_envs: + name: Gather Environments + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - 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 "environments=$(pio project config --json-output | jq -cr '.[0][1][0][1]')" >> $GITHUB_OUTPUT + outputs: + environments: ${{ steps.envs.outputs.environments }} + + + build: + name: Build Enviornments + 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@v3 + - name: Cache pip + 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@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Install PlatformIO + run: pip install -r requirements.txt + - name: Build firmware + env: + WLED_RELEASE: True + run: pio run -e ${{ matrix.environment }} + - uses: actions/upload-artifact@v2 + with: + name: firmware-${{ matrix.environment }} + path: | + build_output/firmware/*.bin + build_output/firmware/*.gz + - uses: actions/upload-artifact@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + name: firmware-release + path: build_output/release/*.bin + release: + name: Create Release + runs-on: ubuntu-latest + needs: [get_default_envs, build] + if: startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/download-artifact@v2 + with: + name: firmware-release + - name: Create draft release + uses: softprops/action-gh-release@v1 + with: + draft: True + files: | + *.bin + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index db3138f5..789de0a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,21 @@ .pio +.cache .pioenvs .piolibdeps .vscode -!.vscode/extensions.json /wled00/Release /wled00/extLibs /platformio_override.ini +/wled00/my_config.h +/build_output .DS_Store .gitignore .clang-format node_modules +.idea +.direnv +wled-update.sh +esp01-update.sh +/wled00/LittleFS +replace_fs.py +wled00/wled00.ino.cpp diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile new file mode 100644 index 00000000..cab85e35 --- /dev/null +++ b/.gitpod.Dockerfile @@ -0,0 +1,3 @@ +FROM gitpod/workspace-full + +USER gitpod diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 00000000..8452f08b --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,11 @@ +tasks: + - command: pip3 install -U platformio && platformio run + +image: + file: .gitpod.Dockerfile + +vscode: + extensions: + - Atishay-Jain.All-Autocomplete + - esbenp.prettier-vscode + - shardulm94.trailing-spaces diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 16b4f2c1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Continuous Integration (CI) is the practice, in software -# engineering, of merging all developer working copies with a shared mainline -# several times a day < https://docs.platformio.org/page/ci/index.html > -# -# Documentation: -# -# * Travis CI Embedded Builds with PlatformIO -# < https://docs.travis-ci.com/user/integration/platformio/ > -# -# * PlatformIO integration with Travis CI -# < https://docs.platformio.org/page/ci/travis.html > -# -# * User Guide for `platformio ci` command -# < https://docs.platformio.org/page/userguide/cmd_ci.html > -# -# -# Please choose one of the following templates (proposed below) and uncomment -# it (remove "# " before each line) or use own configuration according to the -# Travis CI documentation (see above). -# -# * Test the Travis config here: -# < https://config.travis-ci.com/explore > -# - -language: python -python: - # - "2.7" - - "3.5" -os: linux -cache: - bundler: true - ccache: true - directories: - - "~/.platformio" - - "~/.buildcache" -env: - - PLATFORMIO_CI_SRC=wled00 -install: - - pip install -U platformio - - platformio update -script: - # - platformio ci --project-conf=./platformio.ini - - platformio run \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0f0d7401..080e70d0 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,10 @@ -{ - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "platformio.platformio-ide" - ] -} +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..2ee772ce --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build: HTML and binary", + "dependsOn": [ + "Build: HTML only", + "Build: binary only" + ], + "dependsOrder": "sequence", + "problemMatcher": [ + "$platformio", + ], + }, + { + "type": "PlatformIO", + "label": "Build: binary only", + "task": "Build", + "group": { + "kind": "build", + "isDefault": true, + }, + "problemMatcher": [ + "$platformio" + ], + "presentation": { + "panel": "shared" + } + }, + { + "type": "npm", + "script": "build", + "group": "build", + "problemMatcher": [], + "label": "Build: HTML only", + "detail": "npm run build", + "presentation": { + "panel": "shared" + } + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c9e0ee4..3d792e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,1045 @@ ## WLED changelog -### Development versions after 0.10.0 release +#### 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 unknow 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 initilaisation 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 facilitiate 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 + +- Version bump to v0.13.1 "Toki" +- Fix persistent preset bug, preventing save of new presets + +### WLED release 0.13.0 + +#### Build 2203142 + +- Release of WLED v0.13.0 "Toki" +- Reduce APA102 hardware SPI frequency to 5Mhz +- Remove `persistent` parameter in `savePreset()` + +### Builds between releases 0.12.0 and 0.13.0 + +#### Build 2203140 + +- Added factory reset by pressing button 0 for >10 seconds +- Added ability to set presets from DMX Effect mode +- Simplified label hiding JS in user interface +- Fixed JSON `{"live":true}` indefinite realtime mode + +#### Build 2203080 + +- Disabled auto white mode in segments with no RGB bus +- Fixed hostname string not 0-terminated +- Fixed Popcorn mode not lighting first LED on pop + +#### Build 2203060 + +- Dynamic hiding of unused color controls in UI (PR #2567) +- Removed native Cronixie support and added Cronixie usermod +- Fixed disabled timed preset expanding calendar +- Fixed Color Order setting shown for analog busses +- Fixed incorrect operator (#2566) + +#### Build 2203011 + +- IR rewrite (PR #2561), supports CCT +- Added locate button to Time settings +- CSS fixes and adjustments +- Consistent Tab indentation in index JS and CSS +- Added initial contribution style guideline + +#### Build 2202222 + +- Version bump to 0.13.0-b7 "Toki" +- Fixed HTTP API commands not applying to all selected segments in some conditions +- Blynk support is not compiled in by default on ESP32 builds + +#### Build 2202210 + +- Fixed HTTP API commands not applying to all selected segments if called from JSON +- Improved Stream effects, no longer rely on LED state and won't fade out at low brightness + +#### Build 2202200 + +- Added `info.leds.seglc` per-segment light capability info (PR #2552) +- Fixed `info.leds.rgbw` behavior +- Segment bounds sync (PR #2547) +- WebSockets auto reconnection and error handling +- Disable relay pin by default (PR #2531) +- Various fixes (ESP32 touch pin 33, floats, PR #2530, #2534, #2538) +- Deprecated `info.leds.cct`, `info.leds.wv` and `info.leds.rgbw` +- Deprecated `/url` endpoint + +#### Build 2202030 + +- Switched to binary format for WebSockets peek (PR #2516) +- Playlist bugfix +- Added `extractModeName()` utility function +- Added serial out (PR #2517) +- Added configurable baud rate + +#### Build 2201260 + +- Initial ESP32-C3 and ESP32-S2 support (PRs #2452, #2454, #2502) +- Full segment sync (PR #2427) +- Allow overriding of color order by ranges (PR #2463) +- Added white channel to Peek + +#### Build 2112080 + +- Version bump to 0.13.0-b6 "Toki" +- Added "ESP02" (ESP8266 with 2M of flash) to PIO/release binaries + +#### Build 2112070 + +- Added new effect "Fairy", replacing "Police All" +- Added new effect "Fairytwinkle", replacing "Two Areas" +- Static single JSON buffer (performance and stability improvement) (PR #2336) + +#### Build 2112030 + +- Fixed ESP32 crash on Colortwinkles brightness change +- Fixed setting picker to black resetting hue and saturation +- Fixed auto white mode not saved to config + +#### Build 2111300 + +- Added CCT and white balance correction support (PR #2285) +- Unified UI slider style +- Added LED settings config template upload + +#### Build 2111220 + +- Fixed preset cycle not working from preset called by UI +- Reintroduced permanent min. and max. cycle bounds + +#### Build 2111190 + +- Changed default ESP32 LED pin from 16 to 2 +- Renamed "Running 2" to "Chase 2" +- Renamed "Tri Chase" to "Chase 3" + +#### Build 2111170 + +- Version bump to 0.13.0-b5 "Toki" +- Improv Serial support (PR #2334) +- Button improvements (PR #2284) +- Added two time zones (PR #2264, 2311) +- JSON in/decrementing support for brightness and presets +- Fixed no gamma correction for JSON individual LED control +- Preset cycle bugfix +- Removed ledCount +- LED settings buffer bugfix +- Network pin conflict bugfix +- Changed default ESP32 partition layout to 4M, 1M FS + +#### Build 2110110 + +- Version bump to 0.13.0-b4 "Toki" +- Added option for bus refresh if off (PR #2259) +- New auto segment logic +- Fixed current calculations for virtual or non-linear configs (PR #2262) + +#### Build 2110060 + +- Added virtual network DDP busses (PR #2245) +- Allow playlist as end preset in playlist +- Improved bus start field UX +- Pin reservations improvements (PR #2214) + +#### Build 2109220 + +- Version bump to 0.13.0-b3 "Toki" +- Added segment names (PR #2184) +- Improved Police and other effects (PR #2184) +- Reverted PR #1902 (Live color correction - will be implemented as usermod) (PR #2175) +- Added transitions for segment on/off +- Improved number of sparks/stars in Fireworks effect with low number of segments +- Fixed segment name edit pencil disappearing with request +- Fixed color transition active even if the segment is off +- Disallowed file upload with OTA lock active +- Fixed analog invert option missing (PR #2219) + +#### Build 2109100 + +- Added an auto create segments per bus setting +- Added 15 new palettes from SR branch (PR #2134) +- Fixed segment runtime not reset on FX change via HTTP API +- Changed AsyncTCP dependency to pbolduc fork v1.2.0 + +#### Build 2108250 + +- Added Sync groups (PR #2150) +- Added JSON API over Serial support +- Live color correction (PR #1902) + +#### Build 2108180 + +- Fixed JSON IR remote not working with codes greater than 0xFFFFFF (fixes #2135) +- Fixed transition 0 edge case + +#### Build 2108170 + +- 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 + +#### Build 2108050 + +- Fixed undesirable color transition from Orange to boot preset color on first boot +- Removed misleading Delete button on new playlist with one entry +- Updated NeoPixelBus to 2.6.7 and AsyncTCP to 1.1.1 + +#### Build 2107230 + +- Added skinning (extra custom CSS) (PR #2084) +- Added presets/config backup/restore (PR #2084) +- Added option for using length instead of Stop LED in UI (PR #2048) +- Added custom `holidays.json` holiday list (PR #2048) + +#### Build 2107100 + +- Version bump to 0.13.0-b2 "Toki" +- Accept hex color strings in individual LED API +- Fixed transition property not applying unless power/bri/color changed next +- Moved transition field below segments (temporarily) +- Reduced unneeded websockets pushes + +#### Build 2107091 + +- Fixed presets using wrong call mode (e.g. causing buttons to send UDP under direct change type) +- Increased hue buffer +- Renamed `NOTIFIER_CALL_MODE_` to `CALL_MODE_` + +#### Build 2107090 + +- Busses extend total configured LEDs if required +- Fixed extra button pins defaulting to 0 on first boot + +#### Build 2107080 + +- Made Peek use the main websocket connection instead of opening a second one +- Temperature usermod fix (from @blazoncek's dev branch) + +#### Build 2107070 + +- More robust initial resource loading in UI +- Added `getJsonValue()` for usermod config parsing (PR #2061) +- Fixed preset saving over websocket +- Alpha ESP32 S2 support (filesystem does not work) (PR #2067) + +#### Build 2107042 + +- Updated ArduinoJson to 6.18.1 +- Improved Twinkleup effect +- Fixed preset immediately deselecting when set via HTTP API `PL=` + +#### Build 2107041 + +- Restored support for "PL=~" mistakenly removed in 2106300 +- JSON IR improvements + +#### Build 2107040 + +- Playlist entries are now more compact +- Added the possibility to enter negative numbers for segment offset + +#### Build 2107021 + +- Added WebSockets support to UI + +#### Build 2107020 + +- Send websockets on every state change +- Improved Aurora effect + +#### Build 2107011 + +- Added MQTT button feedback option (PR #2011) + +#### Build 2107010 + +- Added JSON IR codes (PR #1941) +- Adjusted the width of WiFi and LED settings input fields +- Fixed a minor visual issue with slider trail not reaching thumb on low values + +#### Build 2106302 + +- Fixed settings page broken by using "%" in input fields + +#### Build 2106301 + +- Fixed a problem with disabled buttons reverting to pin 0 causing conflict + +#### Build 2106300 + +- Version bump to 0.13.0-b0 "Toki" +- BREAKING: Removed preset cycle (use playlists) +- BREAKING: Removed `nl.fade`, `leds.pin` and `ccnf` from JSON API +- Added playlist editor UI +- Reordered segment UI and added offset field +- Raised maximum MQTT password length to 64 (closes #1373) + +#### Build 2106290 + +- Added Offset to segments, allows shifting the LED considered first within a segment +- Added `of` property to seg object in JSON API to set offset +- Usermod settings improvements (PR #2043, PR #2045) + +#### Build 2106250 + +- Fixed preset only disabling on second effect/color change + +#### Build 2106241 + +- BREAKING: Added ability for usermods to force a config save if config incomplete. `readFromConfig()` needs to return a `bool` to indicate if the config is complete +- Updated usermods implementing `readFromConfig()` +- Auto-create segments based on configured busses + +#### Build 2106200 + +- Added 2 Ethernet boards and split Ethernet configs into separate file + +#### Build 2106180 + +- Fixed DOS on Chrome tab restore causing reboot + +#### Build 2106170 + +- Optimized JSON buffer usage (pre-serialized color arrays) + +#### Build 2106140 + +- Updated main logo +- Reduced flash usage by 0.8kB by using 8-bit instead of 32-bit PNGs for welcome and 404 pages +- Added a check to stop Alexa reporting an error if state set by macro differs from the expected state + +#### Build 2106100 + +- Added support for multiple buttons with various types (PR #1977) +- Fixed infinite playlists (PR #2020) +- Added `r` to playlist object, allows for shuffle regardless of the `repeat` value +- Improved accuracy of NTP time sync +- Added possibility for WLED UDP sync to sync system time +- Improved UDP sync accuracy, if both sender and receiver are NTP synced +- Fixed a cache issue with restored tabs +- Cache CORS request +- Disable WiFi sleep by default on ESP32 + +#### Build 2105230 + +- No longer retain MQTT `/v` topic to alleviate storage loads on MQTT broker +- Fixed Sunrise calculation (atan_t approx. used outside of value range) + +#### Build 2105200 + +- Fixed WS281x output on ESP32 +- Fixed potential out-of-bounds write in MQTT +- Fixed IR pin not changeable if IR disabled +- Fixed XML API containing -1 on Manual only RGBW mode (see #888, #1783) + +#### Build 2105171 + +- Always copy MQTT payloads to prevent non-0-terminated strings +- Updated ArduinoJson to 6.18.0 +- Added experimental support for `{"on":"t"}` to toggle on/off state via JSON + +#### Build 2105120 + +- Fixed possibility of non-0-terminated MQTT payloads +- Fixed two warnings regarding integer comparison + +#### Build 2105112 + +- Usermod settings page no usermods message +- Lowered min speed for Drip effect + +#### Build 2105111 + +- Fixed various Codacy code style and logic issues + +#### Build 2105110 + +- Added Usermod settings page and configurable usermods (PR #1951) +- Added experimental `/json/cfg` endpoint for changing settings from JSON (see #1944, not part of official API) + +#### Build 2105070 + +- Fixed not turning on after pressing "Off" on IR remote twice (#1950) +- Fixed OTA update file selection from Android app (TODO: file type verification in JS, since android can't deal with accept='.bin' attribute) + +#### Build 2104220 + +- Version bump to 0.12.1-b1 "Hikari" +- Release and build script improvements (PR #1844) + +#### Build 2104211 + +- Replace default TV simulator effect with the version that saves 18k of flash and appears visually identical + +#### Build 2104210 + +- Added `tb` to JSON state, allowing setting the timebase (set tb=0 to start e.g. wipe effect from the beginning). Receive only. +- Slightly raised Solid mode refresh rate to work with LEDs (TM1814) that require refresh rates of at least 2fps +- Added sunrise and sunset calculation to the backup JSON time source + +#### Build 2104151 + +- `NUM_STRIPS` no longer required with compile-time strip defaults +- Further optimizations in wled_math.h + +#### Build 2104150 + +- Added ability to add multiple busses as compile time defaults using the esp32_multistrip usermod define syntax + +#### Build 2104141 + +- Reduced memory usage by 540b by switching to a different trigonometric approximation + +#### Build 2104140 + +- Added dynamic location-based Sunrise/Sunset macros (PR #1889) +- Improved seasonal background handling (PR #1890) +- Fixed instance discovery not working if MQTT not compiled in +- Fixed Button, IR, Relay pin not assigned by default (resolves #1891) + +#### Build 2104120 + +- Added switch support (button macro is switch closing action, long press macro switch opening) +- Replaced Circus effect with new Running Dual effect (Circus is Tricolor Chase with Red/White/Black) +- Fixed ledmap with multiple segments (PR #1864) + +#### Build 2104030 + +- Fixed ESP32 crash on Drip effect with reversed segment (#1854) +- Added flag `WLED_DISABLE_BROWNOUT_DET` to disable ESP32 brownout detector (off by default) + +### WLED release 0.12.0 + +#### Build 2104020 + +- Allow clearing button/IR/relay pin on platforms that don't support negative numbers +- Removed AUX pin +- Hid some easter eggs, only to be found at easter + +### Development versions between 0.11.1 and 0.12.0 releases + +#### Build 2103310 + +- Version bump to 0.12.0 "Hikari" +- Fixed LED settings submission in iOS app + +#### Build 2103300 + +- Version bump to 0.12.0-b5 "Hikari" +- Update to core espressif32@3.2 +- Fixed IR pin not configurable + +#### Build 2103290 + +- Version bump to 0.12.0-b4 "Hikari" +- Experimental use of espressif32@3.1.1 +- Fixed RGBW mode disabled after LED settings saved +- Fixed infrared support not compiled in if IRPIN is not defined + +#### Build 2103230 + +- Fixed current estimation + +#### Build 2103220 + +- Version bump to 0.12.0-b2 "Hikari" +- Worked around an issue causing a critical decrease in framerate (wled.cpp l.240 block) +- Bump to Espalexa v2.7.0, fixing discovery + +#### Build 2103210 + +- Version bump to 0.12.0-b1 "Hikari" +- More colors visible on Palette preview +- Fixed chevron icon not included +- Fixed color order override +- Cleanup + +#### Build 2103200 + +- Version bump to 0.12.0-b0 "Hikari" +- Added palette preview and search (PR #1637) +- Added Reverse checkbox for PWM busses - reverses logic level for on +- Fixed various problems with the Playlist feature (PR #1724) +- Replaced "Layer" icon with "i" icon for Info button +- Chunchun effect more fitting for various segment lengths (PR #1804) +- Removed global reverse (in favor of individual bus reverse) +- Removed some unused icons from UI icon font + +#### Build 2103130 + +- Added options for Auto Node discovery +- Optimized strings (no string both F() and raw) + +#### Build 2103090 + +- Added Auto Node discovery (PR #1683) +- Added tooltips to quick color selectors for accessibility + +#### Build 2103060 + +- Auto start field population in bus config + +#### Build 2103050 + +- Fixed incorrect over-memory indication in LED settings on ESP32 + +#### Build 2103041 + +- Added destructor for BusPwm (fixes #1789) + +#### Build 2103040 + +- Fixed relay mode inverted when upgrading from 0.11.0 +- Fixed no more than 2 pins per bus configurable in UI +- Changed to non-linear IR brightness steps (PR #1742) +- Fixed various warnings (PR #1744) +- Added UDP DNRGBW Mode (PR #1704) +- Added dynamic LED mapping with ledmap.json file (PR #1738) +- Added support for QuinLED-ESP32-Ethernet board +- Added support for WESP32 ethernet board (PR #1764) +- Added Caching for main UI (PR #1704) +- Added Tetrix mode (PR #1729) +- Removed Merry Christmas mode (use "Chase 2" - called Running 2 before 0.13.0) +- Added memory check on Bus creation + +#### Build 2102050 + +- Version bump to 0.12.0-a0 "Hikari" +- Added FPS indication in info +- Bumped max outputs from 7 to 10 busses for ESP32 + +#### Build 2101310 + +- First alpha configurable multipin + +#### Build 2101130 + +- Added color transitions for all segments and slots and for segment brightness +- Fixed bug that prevented setting a boot preset higher than 25 + +#### Build 2101040 + +- Replaced Red & Blue effect with Aurora effect (PR #1589) +- Fixed HTTP changing segments uncommanded (#1618) +- Updated copyright year and contributor page link + +#### Build 2012311 + +- Fixed Countdown mode + +#### Build 2012310 + +- (Hopefully actually) fixed display of usermod values in info screen + +#### Build 2012240 + +- Fixed display of usermod values in info screen +- 4 more effects now use FRAMETIME +- Remove unsupported environments from platformio.ini + +#### Build 2012210 + +- Split index.htm in separate CSS + JS files (PR #1542) +- Minify UI HTML, saving >1.5kB flash +- Fixed JShint warnings + +#### Build 2012180 + +- Boot brightness 0 will now use the brightness from preset +- Add iOS scrolling momentum (from PR #1528) + +### WLED release 0.11.1 + +#### Build 2012180 + +- Release of WLED 0.11.1 "Mirai" +- Fixed AP hide not saving (fixes #1520) +- Fixed MQTT password re-transmitted to HTML +- Hide Update buttons while uploading, accept .bin +- Make sure AP password is at least 8 characters long + +### Development versions after 0.11.0 release + +#### Build 2012160 + +- Bump Espalexa to 2.5.0, fixing discovery (PR Espalexa/#152, originally PR #1497) + +#### Build 2012150 + +- Added Blends FX (PR #1491) +- Fixed an issue that made it impossible to deactivate timed presets + +#### Build 2012140 + +- Added Preset ID quick display option (PR #1462) +- Fixed LEDs not turning on when using gamma correct brightness and LEDPIN 2 (default) +- Fixed notifier applying main segment to selected segments on notification with FX/Col disabled + +#### Build 2012130 + +- Fixed RGBW mode not saved between reboots (fixes #1457) +- Added brightness scaling in palette function for default (PR #1484) + +#### Build 2012101 + +- Fixed preset cycle default duration rounded down to nearest 10sec interval (#1458) +- Enabled E1.31/DDP/Art-Net in AP mode + +#### Build 2012100 + +- Fixed multi-segment preset cycle +- Fixed EEPROM (pre-0.11 settings) not cleared on factory reset +- Fixed an issue with intermittent crashes on FX change (PR #1465) +- Added function to know if strip is updating (PR #1466) +- Fixed using colorwheel sliding the UI (PR #1459) +- Fixed analog clock settings not saving (PR #1448) +- Added Temperature palette (PR #1430) +- Added Candy cane FX (PR #1445) + +#### Build 2012020 + +- UDP `parsePacket()` with sync disabled (#1390) +- Added Multi RGBW DMX mode (PR #1383) + +#### Build 2012010 + +- Fixed compilation for analog (PWM) LEDs + +### WLED version 0.11.0 + +#### Build 2011290 + +- Release of WLED 0.11.0 "Mirai" +- Workaround for weird empty %f Espalexa issue +- Fixed crash on saving preset with HTTP API `PS` +- Improved performance for color changes in non-main segment + +#### Build 2011270 + +- Added tooltips for speed and intensity sliders (PR #1378) +- Moved color order to NpbWrapper.h +- Added compile time define to override the color order for a specific range + +#### Build 2011260 + +- Add `live` property to state, allowing toggling of realtime (not incl. in state resp.) +- PIO environment changes + +#### Build 2011230 + +- Version bump to 0.11.0 "Mirai" +- Improved preset name sorting +- Fixed Preset cycle not working beyond preset 16 + +### Development versions between 0.10.2 and 0.11.0 releases + +#### Build 2011220 + +- Fixed invalid save when modifying preset before refresh (might be related to #1361) +- Fixed brightness factor ignored on realtime timeout (fixes #1363) +- Fixed Phase and Chase effects with LED counts >256 (PR #1366) + +#### Build 2011210 + +- Fixed Brightness slider beneath color wheel not working (fixes #1360) +- Fixed invalid UI state after saving modified preset + +#### Build 2011200 + +- Added HEX color receiving to JSON API with `"col":["RRGGBBWW"]` format +- Moved Kelvin color receiving in JSON API from `"col":[[val]]` to `"col":[val]` format + _Notice:_ This is technically a breaking change. Since no release was made since the introduction and the Kelvin property was not previously documented in the wiki, + impact should be minimal. +- BTNPIN can now be disabled by setting to -1 (fixes #1237) + +#### Build 2011180 + +- Platformio.ini updates and streamlining (PR #1266) +- my_config.h custom compile settings system (not yet used for much, adapted from PR #1266) +- Added Hawaii timezone (HST) +- Linebreak after 5 quick select buttons + +#### Build 2011154 + +- Fixed RGBW saved incorrectly +- Fixed pmt caching requesting /presets.json too often +- Fixed deEEP not copying the first segment of EEPROM preset 16 + +#### Build 2011153 + +- Fixed an ESP32 end-of-file issue +- Fixed strip.isRgbw not read from cfg.json + +#### Build 2011152 + +- Version bump to 0.11.0p "Mirai" +- Increased max. num of segments to 12 (ESP8266) / 16 (ESP32) +- Up to 250 presets stored in the `presets.json` file in filesystem +- Complete overhaul of the Presets UI tab +- Updated iro.js to v5 (fixes black color wheel) +- Added white temperature slider to color wheel +- Add JSON settings serialization/deserialization to cfg.json and wsec.json +- Added deEEP to convert the EEPROM settings and presets to files +- Playlist support - JSON only for now +- New v2 usermod methods `addToConfig()` and `readFromConfig()` (see EXAMPLE_v2 for doc) +- Added Ethernet support for ESP32 (PR #1316) +- IP addresses are now handled by the `Network` class +- New `esp32_poe` PIO environment +- Use EspAsyncWebserver Aircoookie fork v.2.0.0 (hiding wsec.json) +- Removed `WLED_DISABLE_FILESYSTEM` and `WLED_ENABLE_FS_SERVING` defines as they are now required +- Added pin manager +- UI performance improvements (no drop shadows) +- More explanatory error messages in UI +- Improved candle brightness +- Return remaining nightlight time `nl.rem` in JSON API (PR #1302) +- UI sends timestamp with every command, allowing for timed presets without using NTP +- Added gamma calculation (yet unused) +- Added LED type definitions to const.h (yet unused) +- Added nicer 404 page +- Removed `NP` and `MS=` macro HTTP API commands +- Removed macros from Time settings + +#### Build 2011120 + +- Added the ability for the /api MQTT topic to receive JSON API payloads + +#### Build 2011040 + +- Inversed Rain direction (fixes #1147) + +#### Build 2011010 + +- Re-added previous C9 palette +- Renamed new C9 palette + +#### Build 2010290 + +- Colorful effect now supports palettes +- Added C9 2 palette (#1291) +- Improved C9 palette brightness by 12% +- Disable onboard LED if LEDs are off (PR #1245) +- Added optional status LED (PR #1264) +- Realtime max. brightness now honors brightness factor (fixes #1271) +- Updated ArduinoJSON to 6.17.0 + +#### Build 2010020 + +- Fixed interaction of `T` and `NL` HTTP API commands (#1214) +- Fixed an issue where Sunrise mode nightlight does not activate if toggled on simultaneously + +#### Build 2009291 + +- Fixed MQTT bootloop (no F() macro, #1199) + +#### Build 2009290 + +- Added basic DDP protocol support +- Added Washing Machine effect (PR #1208) + +#### Build 2009260 + +- Added Loxone parser (PR #1185) +- Added support for kelvin input via `K=` HTTP and `"col":[[val]]` JSON API calls + _Notice:_ `"col":[[val]]` removed in build 2011200, use `"col":[val]` +- Added supplementary UDP socket (#1205) +- TMP2.net receivable by default +- UDP sockets accept HTTP and JSON API commands +- Fixed missing timezones (#1201) + +#### Build 2009202 + +- Fixed LPD8806 compilation + +#### Build 2009201 + +- Added support for preset cycle toggling using CY=2 +- Added ESP32 touch pin support (#1190) +- Fixed modem sleep on ESP8266 (#1184) + +#### Build 2009200 + +- Increased available heap memory by 4kB +- Use F() macro for the majority of strings +- Restructure timezone code +- Restructured settings saved code +- Updated ArduinoJSON to 6.16.1 + +#### Build 2009170 + +- New WLED logo on Welcome screen (#1164) +- Fixed 170th pixel dark in E1.31 + +#### Build 2009100 + +- Fixed sunrise mode not reinitializing +- Fixed passwords not clearable + +#### Build 2009070 + +- New Segments are now initialized with default speed and intensity + +#### Build 2009030 + +- Fixed bootloop if mDNS is used on builds without OTA support + +### WLED version 0.10.2 + +#### Build 2008310 + +- Added new logo +- Maximum GZIP compression (#1126) +- Enable WebSockets by default + +### Development versions between 0.10.0 and 0.10.2 releases + +#### Build 2008300 + +- Added new UI customization options to UI settings +- Added Dancing Shadows effect (#1108) +- Preset cycle is now paused if lights turned off or nightlight active +- Removed `esp01` and `esp01_ota` envs from travis build (need too much flash) + +#### Build 2008290 + +- Added individual LED control support to JSON API +- Added internal Segment Freeze/Pause option + +#### Build 2008250 + +- Made `platformio_override.ini` example easier to use by including the `default_envs` property +- FastLED uses `now` as timer, so effects using e.g. `beatsin88()` will sync correctly +- Extended the speed range of Pacifica effect +- Improved TPM2.net receiving (#1100) +- Fixed exception on empty MQTT payload (#1101) + +#### Build 2008200 + +- Added segment mirroring to web UI +- Fixed segment mirroring when in reverse mode + +#### Build 2008140 + +- Removed verbose live mode info from `` in HTTP API response + +#### Build 2008100 + +- Fixed Auto White mode setting (fixes #1088) + +#### Build 2008070 + +- Added segment mirroring (`mi` property) (#1017) +- Fixed DMX settings page not displayed (#1070) +- Fixed ArtNet multi universe and improve code style (#1076) +- Renamed global var `local` to `localTime` (#1078) + +#### Build 2007190 + +- Fixed hostname containing illegal characters (#1035) #### Build 2006251 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..560a7097 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +## Thank you for making WLED better! + +Here are a few suggestions to make it easier for you to contribute! + +### Code style + +When in doubt, it is easiest to replicate the code style you find in the files you want to edit :) +Below are the guidelines we use in the WLED repository. + +#### Indentation + +We use tabs for Indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files. +You are all set if you have enabled `Editor: Detect Indentation` in VS Code. + +#### Blocks + +Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block braches is acceptable. + +Good: +```cpp +if (a == b) { + doStuff(a); +} +``` + +```cpp +if (a == b) +{ + doStuff(a); +} +``` + +```cpp +if (a == b) doStuff(a); +``` + +There should always be a space between a keyword and its condition and between the condition and brace. +Within the condition, no space should be between the paranthesis and variables. +Spaces between variables and operators are up to the authors discretion. +There should be no space between function names and their argument parenthesis. + +Good: +```cpp +if (a == b) { + doStuff(a); +} +``` + +Not good: +```cpp +if( a==b ){ + doStuff ( a); +} +``` + +#### Comments + +Comments should have a space between the delimiting characters (e.g. `//`) and the comment text. +Note: This is a recent change, the majority of the codebase still has comments without spaces. + +Good: +``` +// This is a comment. + +/* This is a CSS inline comment */ + +/* + * This is a comment + * wrapping over multiple lines, + * used in WLED for file headers and function explanations + */ + + +``` + +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 diff --git a/images/Readme.md b/images/Readme.md new file mode 100644 index 00000000..738a84f6 --- /dev/null +++ b/images/Readme.md @@ -0,0 +1,5 @@ +### Additional Logos + +Additional awesome logos for WLED can be found here [Aircoookie/Akemi](https://github.com/Aircoookie/Akemi). + + diff --git a/images/macbook-pro-space-gray-on-the-wooden-table.jpg b/images/macbook-pro-space-gray-on-the-wooden-table.jpg index 64f645b3..25f46cc1 100644 Binary files a/images/macbook-pro-space-gray-on-the-wooden-table.jpg and b/images/macbook-pro-space-gray-on-the-wooden-table.jpg differ diff --git a/images/wled_logo.png b/images/wled_logo.png deleted file mode 100644 index 905c793f..00000000 Binary files a/images/wled_logo.png and /dev/null differ diff --git a/images/wled_logo_akemi.png b/images/wled_logo_akemi.png new file mode 100644 index 00000000..87a2d485 Binary files /dev/null and b/images/wled_logo_akemi.png differ diff --git a/images/wled_logo_old.png b/images/wled_logo_old.png new file mode 100644 index 00000000..116eda0a Binary files /dev/null and b/images/wled_logo_old.png differ diff --git a/package-lock.json b/package-lock.json index d44c4c77..c4fe9b1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,36 +1,18 @@ { "name": "wled", - "version": "0.10.0", + "version": "0.14.0-b3", "lockfileVersion": 1, "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" - } - }, - "@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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "ajv": { - "version": "6.12.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", - "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -48,39 +30,6 @@ "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" - } - } - } - }, "ansi-escapes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", @@ -97,9 +46,9 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -147,9 +96,9 @@ "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=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -160,68 +109,15 @@ } }, "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==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "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": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -239,43 +135,10 @@ "fill-range": "^7.0.1" } }, - "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==" - } - } - }, - "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } + "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==" }, "camelcase": { "version": "1.2.1", @@ -326,25 +189,20 @@ } }, "chokidar": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", - "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "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" } }, - "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": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", @@ -368,11 +226,6 @@ } } }, - "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==" - }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", @@ -383,14 +236,6 @@ "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": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", @@ -399,19 +244,6 @@ "q": "^1.1.2" } }, - "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", @@ -433,7 +265,7 @@ "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": { "version": "1.4.0", @@ -462,11 +294,6 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", @@ -513,24 +340,11 @@ "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" - } - }, "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==" - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -566,19 +380,34 @@ "domelementtype": "1" } }, - "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==", + "dot-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz", + "integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==", "requires": { - "is-obj": "^2.0.0" + "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" + } + } } }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" - }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -633,11 +462,6 @@ "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": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -656,11 +480,6 @@ "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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -715,19 +534,11 @@ } }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "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" - } - }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -737,21 +548,13 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "requires": { "is-glob": "^4.0.1" } }, - "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": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/got/-/got-3.3.1.tgz", @@ -806,38 +609,48 @@ "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==" }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, - "html-minifier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", - "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "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": { - "camel-case": "^3.0.0", - "clean-css": "^4.2.1", - "commander": "^2.19.0", + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", "he": "^1.2.0", - "param-case": "^2.1.1", + "param-case": "^3.0.3", "relateurl": "^0.2.7", - "uglify-js": "^3.5.1" + "terser": "^4.6.3" }, "dependencies": { - "uglify-js": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.3.tgz", - "integrity": "sha512-r5ImcL6QyzQGVimQoov3aL2ZScywrOgBXGndbWrdehKoSvGe/RmiE5Jpw/v+GvxODt6l2tpBXwA7n+qZVlHBMA==", + "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": { - "commander": "~2.20.3" + "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" } } } @@ -870,11 +683,6 @@ } } }, - "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==" - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -896,12 +704,7 @@ "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": { "version": "0.1.4", @@ -919,9 +722,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "inliner": { "version": "1.13.1", @@ -962,46 +765,24 @@ "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, "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==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "requires": { "is-extglob": "^2.1.1" } }, - "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" - } - }, "is-npm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", @@ -1012,16 +793,6 @@ "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==" - }, "is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", @@ -1037,11 +808,6 @@ "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": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -1071,11 +837,6 @@ "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=" - }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -1102,14 +863,6 @@ "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": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -1248,31 +1001,11 @@ "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" - }, "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==" - } - } - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -1291,23 +1024,18 @@ "mime-db": "1.44.0" } }, - "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==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "mkdirp": { "version": "0.5.5", @@ -1330,161 +1058,35 @@ "inherits": "~2.0.1" } }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "requires": { - "lower-case": "^1.1.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==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", "requires": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", + "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" + "undefsafe": "^2.0.5" }, "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==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "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==" - } - } + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "supports-color": { "version": "5.5.0", @@ -1493,49 +1095,13 @@ "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==" } } }, "nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", "requires": { "abbrev": "1" } @@ -1545,11 +1111,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, - "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" - }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -1595,11 +1156,6 @@ "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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/package-json/-/package-json-1.2.0.tgz", @@ -1609,12 +1165,32 @@ "registry-url": "^3.0.0" } }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "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": { - "no-case": "^2.2.0" + "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" + } + } } }, "performance-now": { @@ -1623,9 +1199,9 @@ "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==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, "pinkie": { "version": "2.0.4", @@ -1668,37 +1244,20 @@ "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": { "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" - } - }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" }, "rc": { "version": "1.2.8", @@ -1766,21 +1325,13 @@ } }, "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "requires": { "picomatch": "^2.2.1" } }, - "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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", @@ -1834,14 +1385,6 @@ "uuid": "^3.3.2" } }, - "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": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", @@ -1866,9 +1409,9 @@ "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==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" }, "semver-diff": { "version": "2.1.0", @@ -1878,10 +1421,20 @@ "semver": "^5.0.3" } }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + "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==", + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==" + } + } }, "slide": { "version": "1.1.6", @@ -1893,6 +1446,22 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, + "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": { + "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -1927,41 +1496,6 @@ "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" - } - } - } - }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -1999,10 +1533,22 @@ "whet.extend": "~0.9.9" } }, - "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.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "requires": { + "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==" + } + } }, "then-fs": { "version": "2.0.0", @@ -2017,11 +1563,6 @@ "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==" - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2047,6 +1588,11 @@ "punycode": "^2.1.1" } }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2060,19 +1606,6 @@ "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": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", @@ -2090,20 +1623,9 @@ "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" - } - }, - "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" - } + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" }, "update-notifier": { "version": "0.5.0", @@ -2119,11 +1641,6 @@ "string-length": "^1.0.0" } }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" - }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -2132,21 +1649,6 @@ "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=" - } - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2172,14 +1674,6 @@ "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" - } - }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", diff --git a/package.json b/package.json index a932fcd2..d57c87d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.10.1", + "version": "0.14.0-b3", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { @@ -23,9 +23,9 @@ "homepage": "https://github.com/Aircoookie/WLED#readme", "dependencies": { "clean-css": "^4.2.3", - "html-minifier": "^4.0.0", + "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/pio-scripts/obj-dump.py b/pio-scripts/obj-dump.py new file mode 100644 index 00000000..91bc3de5 --- /dev/null +++ b/pio-scripts/obj-dump.py @@ -0,0 +1,9 @@ +# Little convenience script to get an object dump + +Import('env') + +def obj_dump_after_elf(source, target, env): + print("Create firmware.asm") + env.Execute("xtensa-lx106-elf-objdump "+ "-D " + str(target[0]) + " > "+ "${PROGNAME}.asm") + +env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", [obj_dump_after_elf]) diff --git a/pio-scripts/output_bins.py b/pio-scripts/output_bins.py new file mode 100644 index 00000000..01223e93 --- /dev/null +++ b/pio-scripts/output_bins.py @@ -0,0 +1,69 @@ +Import('env') +import os +import shutil +import gzip + +OUTPUT_DIR = "build_output{}".format(os.path.sep) + +def _get_cpp_define_value(env, define): + define_list = [item[-1] for item in env["CPPDEFINES"] if item[0] == define] + + if define_list: + return define_list[0] + + return None + +def _create_dirs(dirs=["firmware", "map"]): + # check if output directories exist and create if necessary + if not os.path.isdir(OUTPUT_DIR): + os.mkdir(OUTPUT_DIR) + + for d in dirs: + if not os.path.isdir("{}{}".format(OUTPUT_DIR, d)): + os.mkdir("{}{}".format(OUTPUT_DIR, d)) + +def bin_rename_copy(source, target, env): + _create_dirs() + variant = env["PIOENV"] + + # create string with location and file names based on variant + map_file = "{}map{}{}.map".format(OUTPUT_DIR, os.path.sep, variant) + bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) + + release_name = _get_cpp_define_value(env, "WLED_RELEASE_NAME") + + if release_name: + _create_dirs(["release"]) + version = _get_cpp_define_value(env, "WLED_VERSION") + release_file = "{}release{}WLED_{}_{}.bin".format(OUTPUT_DIR, os.path.sep, version, release_name) + shutil.copy(str(target[0]), release_file) + + # check if new target files exist and remove if necessary + for f in [map_file, bin_file]: + if os.path.isfile(f): + os.remove(f) + + # copy firmware.bin to firmware/.bin + shutil.copy(str(target[0]), bin_file) + + # copy firmware.map to map/.map + if os.path.isfile("firmware.map"): + shutil.move("firmware.map", map_file) + +def bin_gzip(source, target, env): + _create_dirs() + variant = env["PIOENV"] + + # create string with location and file names based on variant + bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) + gzip_file = "{}firmware{}{}.bin.gz".format(OUTPUT_DIR, os.path.sep, variant) + + # check if new target files exist and remove if necessary + if os.path.isfile(gzip_file): os.remove(gzip_file) + + # write gzip firmware file + with open(bin_file,"rb") as fp: + with gzip.open(gzip_file, "wb", compresslevel = 9) as f: + shutil.copyfileobj(fp, f) + +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_rename_copy, bin_gzip]) diff --git a/pio-scripts/set_version.py b/pio-scripts/set_version.py new file mode 100644 index 00000000..1d8e076e --- /dev/null +++ b/pio-scripts/set_version.py @@ -0,0 +1,8 @@ +Import('env') +import json + +PACKAGE_FILE = "package.json" + +with open(PACKAGE_FILE, "r") as package: + version = json.load(package)["version"] + env.Append(BUILD_FLAGS=[f"-DWLED_VERSION={version}"]) diff --git a/pio-scripts/strip-floats.py b/pio-scripts/strip-floats.py new file mode 100644 index 00000000..da916ebe --- /dev/null +++ b/pio-scripts/strip-floats.py @@ -0,0 +1,15 @@ +Import('env') + +# +# Dump build environment (for debug) +#print env.Dump() +# + +flags = " ".join(env['LINKFLAGS']) +flags = flags.replace("-u _printf_float", "") +flags = flags.replace("-u _scanf_float", "") +newflags = flags.split() + +env.Replace( + LINKFLAGS=newflags +) \ No newline at end of file diff --git a/pio-scripts/user_config_copy.py b/pio-scripts/user_config_copy.py new file mode 100644 index 00000000..1251ca17 --- /dev/null +++ b/pio-scripts/user_config_copy.py @@ -0,0 +1,9 @@ +Import('env') +import os +import shutil + +# copy WLED00/my_config_sample.h to WLED00/my_config.h +if os.path.isfile("wled00/my_config.h"): + print ("*** use existing my_config.h ***") +else: + shutil.copy("wled00/my_config_sample.h", "wled00/my_config.h") diff --git a/platformio.ini b/platformio.ini index 3a5383be..73da6250 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,29 +2,27 @@ ; Please visit documentation: https://docs.platformio.org/page/projectconf.html [platformio] -src_dir = ./wled00 -data_dir = ./wled00/data -lib_dir = ./wled00/src -build_cache_dir = ~/.buildcache -extra_configs = - platformio_override.ini - # ------------------------------------------------------------------------------ # 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 -default_envs = travis_esp8266, esp01, esp01_1m_ota, 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, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi # Release binaries -; default_envs = nodemcuv2, esp01, esp01_1m_ota, esp01_1m_full, esp32dev, custom_WS2801, custom_APA102, custom_LEDPIN_16, custom_LEDPIN_4, custom32_LEDPIN_16 +; 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, 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 ; default_envs = nodemcuv2 -; default_envs = esp01 -; default_envs = esp01_1m_ota +; default_envs = esp8266_2m ; default_envs = esp01_1m_full ; default_envs = esp07 ; default_envs = d1_mini @@ -34,49 +32,58 @@ default_envs = travis_esp8266, esp01, esp01_1m_ota, travis_esp32 ; default_envs = d1_mini_ota ; default_envs = esp32dev ; default_envs = esp8285_4CH_MagicHome -; default_envs = esp8285_4CH_H801 -; default_envs = esp8285_5CH_H801 +; default_envs = esp8285_H801 ; default_envs = d1_mini_5CH_Shojo_PCB ; 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 + +src_dir = ./wled00 +data_dir = ./wled00/data +build_cache_dir = ~/.buildcache +extra_configs = + platformio_override.ini [common] # ------------------------------------------------------------------------------ # PLATFORM: # !! DO NOT confuse platformio's ESP8266 development platform with Arduino core for ESP8266 # -# arduino core 2.3.0 = platformIO 1.5.0 -# arduino core 2.4.0 = platformIO 1.6.0 -# arduino core 2.4.1 = platformIO 1.7.3 -# arduino core 2.4.2 = platformIO 1.8.0 -# arduino core 2.5.0 = platformIO 2.0.4 -# arduino core 2.5.1 = platformIO 2.1.1 -# arduino core 2.5.2 = platformIO 2.2.3 -# arduino core 2.6.1 = platformIO 2.3.0 -# arduino core 2.6.2 = platformIO 2.3.1 # arduino core 2.6.3 = platformIO 2.3.2 # arduino core 2.7.0 = platformIO 2.5.0 # ------------------------------------------------------------------------------ -arduino_core_2_3_0 = espressif8266@1.5.0 -arduino_core_2_4_0 = espressif8266@1.6.0 -arduino_core_2_4_1 = espressif8266@1.7.3 -arduino_core_2_4_2 = espressif8266@1.8.0 -arduino_core_2_5_0 = espressif8266@2.0.4 -arduino_core_2_5_1 = espressif8266@2.1.1 -arduino_core_2_5_2 = espressif8266@2.2.3 -arduino_core_2_6_1 = espressif8266@2.3.0 -arduino_core_2_6_2 = espressif8266@2.3.1 arduino_core_2_6_3 = espressif8266@2.3.3 -arduino_core_2_7_1 = espressif8266@2.5.1 +arduino_core_2_7_4 = espressif8266@2.6.2 +arduino_core_3_0_0 = espressif8266@3.0.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.0 # 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_1} -# We use 2.7.0+ on analog boards because of PWM flicker fix -platform_latest = ${common.arduino_core_2_7_1} +platform_wled_default = ${common.arduino_core_3_1_2} +# We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization +#platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 +platform_packages = platformio/framework-arduinoespressif8266 + 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 @@ -84,20 +91,18 @@ platform_latest = ${common.arduino_core_2_7_1} # ------------------------------------------------------------------------------ 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 +#-DDEBUG_ESP_CORE is not working right now # ------------------------------------------------------------------------------ -# FLAGS: ldscript -# ldscript_512k ( 512 KB) = 487 KB sketch, 4 KB eeprom, no spiffs, 16 KB reserved -# ldscript_1m0m (1024 KB) = 999 KB sketch, 4 KB eeprom, no spiffs, 16 KB reserved +# FLAGS: ldscript (available ldscripts at https://github.com/esp8266/Arduino/tree/master/tools/sdk/ld) # ldscript_2m1m (2048 KB) = 1019 KB sketch, 4 KB eeprom, 1004 KB spiffs, 16 KB reserved # ldscript_4m1m (4096 KB) = 1019 KB sketch, 4 KB eeprom, 1002 KB spiffs, 16 KB reserved, 2048 KB empty/ota? -# ldscript_4m3m (4096 KB) = 1019 KB sketch, 4 KB eeprom, 3040 KB spiffs, 16 KB reserved # # Available lwIP variants (macros): # -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH = v1.4 Higher Bandwidth (default) # -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY = v2 Lower Memory # -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH = v2 Higher Bandwidth +# -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH # # BearSSL performance: # When building with -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL, please add `board_build.f_cpu = 160000000` to the environment configuration @@ -110,35 +115,44 @@ debug_flags = -D DEBUG=1 -D WLED_DEBUG -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT # TLS_RSA_WITH_AES_256_CBC_SHA / AES256-SHA # This reduces the OTA size with ~45KB, so it's especially useful on low memory boards (512k/1m). # ------------------------------------------------------------------------------ -build_flags = -g -w -DMQTT_MAX_PACKET_SIZE=1024 -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH - -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL -DBEARSSL_SSL_BASIC +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 + -D _IR_ENABLE_DEFAULT_=false + -D DECODE_HASH=true -D DECODE_NEC=true - -D DECODE_SONY=true + -D DECODE_SONY=true -D DECODE_SAMSUNG=true -D DECODE_LG=true - -build_flags_esp8266 = ${common.build_flags} -DESP8266 -build_flags_esp32 = ${common.build_flags} -DARDUINO_ARCH_ESP32 + ;-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 -# enables all features for travis CI -build_flags_all_features = - -D WLED_USE_ANALOG_LED - -D WLED_USE_H801 - -D WLED_ENABLE_5CH_LEDS - -D WLED_ENABLE_ADALIGHT - -D WLED_ENABLE_DMX - -D WLED_ENABLE_MQTT +build_unflags = -ldscript_512k = eagle.flash.512k.ld ;for older versions change this to eagle.flash.512k0.ld -ldscript_1m0m = eagle.flash.1m.ld ;for older versions change this to eagle.flash.1m0.ld +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 ldscript_2m1m = eagle.flash.2m1m.ld ldscript_4m1m = eagle.flash.4m1m.ld -ldscript_4m3m = eagle.flash.4m3m.ld -shared_libdeps_dir = ./wled00/src +[scripts_defaults] +extra_scripts = + pre:pio-scripts/set_version.py + post:pio-scripts/output_bins.py + post:pio-scripts/strip-floats.py + pre:pio-scripts/user_config_copy.py # ------------------------------------------------------------------------------ # COMMON SETTINGS: @@ -147,36 +161,163 @@ shared_libdeps_dir = ./wled00/src framework = arduino board_build.flash_mode = dout monitor_speed = 115200 +# slow upload speed (comment this out with a ';' when building for development use) upload_speed = 115200 -lib_extra_dirs = - ${common.shared_libdeps_dir} +# fast upload speed (remove ';' when building for development use) +; upload_speed = 921600 # ------------------------------------------------------------------------------ # 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@3.3.2 - NeoPixelBus@2.5.7 - ESPAsyncTCP@1.2.0 - ESPAsyncUDP@697c75a025 - AsyncTCP@1.0.3 - Esp Async WebServer@1.2.0 - IRremoteESP8266@2.7.3 - #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line + fastled/FastLED @ 3.6.0 + IRremoteESP8266 @ 2.8.2 + makuna/NeoPixelBus @ 2.7.5 + https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.7 + #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 + #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 -lib_ignore = - AsyncTCP + #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} + +[esp8266] +build_flags = + -DESP8266 + -DFP_IN_IROM + ;-Wno-deprecated-declarations + ;-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 + -DVTABLES_IN_FLASH + ; restrict to minimal mime-types + -DMIMETYPE_MINIMAL + +lib_deps = + #https://github.com/lorol/LITTLEFS.git + ESPAsyncTCP @ 1.2.2 + ESPAsyncUDP + ${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 + -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 = + https://github.com/lorol/LITTLEFS.git + https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${env.lib_deps} + + +[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=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 = + 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=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} + +[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 @@ -185,84 +326,218 @@ lib_ignore = [env:nodemcuv2] board = nodemcuv2 platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} +build_unflags = ${common.build_unflags} +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:esp01] -board = esp01 +[env:esp8266_2m] +board = esp_wroom_02 platform = ${common.platform_wled_default} -board_build.ldscript = ${common.ldscript_512k} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA -D WLED_DISABLE_ALEXA -D WLED_DISABLE_BLYNK - -D WLED_DISABLE_CRONIXIE -D WLED_DISABLE_HUESYNC -D WLED_DISABLE_INFRARED -D WLED_DISABLE_MQTT - -[env:esp01_1m_ota] -board = esp01_1m -platform = ${common.platform_wled_default} -board_build.ldscript = ${common.ldscript_1m0m} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_ALEXA -D WLED_DISABLE_BLYNK -D WLED_DISABLE_CRONIXIE -D WLED_DISABLE_HUESYNC -D WLED_DISABLE_INFRARED -D WLED_DISABLE_MQTT +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=ESP02 +lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full] board = esp01_1m platform = ${common.platform_wled_default} -board_build.ldscript = ${common.ldscript_1m0m} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA +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 +lib_deps = ${esp8266.lib_deps} [env:esp07] board = esp07 platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} +lib_deps = ${esp8266.lib_deps} [env:d1_mini] board = d1_mini platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} upload_speed = 921600 board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} +lib_deps = ${esp8266.lib_deps} +monitor_filters = esp8266_exception_decoder [env:heltec_wifi_kit_8] board = d1_mini 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} +lib_deps = ${esp8266.lib_deps} [env:h803wf] board = d1_mini 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 LEDPIN=1 -D WLED_DISABLE_INFRARED +lib_deps = ${esp8266.lib_deps} [env:esp32dev] board = esp32dev -platform = espressif32@1.11.2 -build_flags = ${common.build_flags_esp32} -lib_ignore = - ESPAsyncTCP - ESPAsyncUDP +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_BROWNOUT_DET +lib_deps = ${esp32.lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.default_partitions} + +[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 +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 +lib_deps = ${esp32.lib_deps} +board_build.partitions = ${esp32.default_partitions} + +[env:esp32s2_saola] +board = esp32-s2-saola-1 +platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.2/platform-tasmota-espressif32-2.0.2.zip +platform_packages = +framework = arduino +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: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 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_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_latest} -board_build.ldscript = ${common.ldscript_1m0m} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_HUESYNC -D WLED_USE_ANALOG_LEDS +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_1m128k} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA +lib_deps = ${esp8266.lib_deps} -[env:esp8285_4CH_H801] +[env:esp8285_H801] board = esp8285 -platform = ${common.platform_latest} -board_build.ldscript = ${common.ldscript_1m0m} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_HUESYNC -D WLED_USE_ANALOG_LEDS -D WLED_USE_H801 - -[env:esp8285_5CH_H801] -board = esp8285 -platform = ${common.platform_latest} -board_build.ldscript = ${common.ldscript_1m0m} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_HUESYNC -D WLED_USE_ANALOG_LEDS -D WLED_USE_H801 -D WLED_ENABLE_5CH_LEDS +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_1m128k} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA +lib_deps = ${esp8266.lib_deps} [env:d1_mini_5CH_Shojo_PCB] board = d1_mini -platform = ${common.platform_latest} +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} -D WLED_USE_ANALOG_LEDS -D WLED_USE_SHOJO_PCB -D WLED_ENABLE_5CH_LEDS +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D WLED_USE_SHOJO_PCB +lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ # DEVELOPMENT BOARDS @@ -272,8 +547,11 @@ build_flags = ${common.build_flags_esp8266} -D WLED_USE_ANALOG_LEDS -D WLED_USE_ board = d1_mini build_type = debug 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} ${common.debug_flags} +lib_deps = ${esp8266.lib_deps} [env:d1_mini_ota] board = d1_mini @@ -281,74 +559,229 @@ upload_protocol = espota # exchange for your WLED IP upload_port = "10.10.1.27" platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} +lib_deps = ${esp8266.lib_deps} + +[env:anavi_miracle_controller] +board = d1_mini +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 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=LolinS2 + -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_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:custom_LEDPIN_4] -board = d1_mini -platform = ${common.platform_latest} -board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=4 -D IRPIN=5 - -[env:custom_LEDPIN_16] -board = d1_mini -platform = ${common.platform_latest} -board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=16 - -[env:custom_APA102] -board = d1_mini -platform = ${common.platform_latest} -board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} -D USE_APA102 - -[env:custom_WS2801] -board = d1_mini -platform = ${common.platform_latest} -board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} -D USE_WS2801 - -[env:custom32_LEDPIN_16] -board = esp32dev -platform = espressif32@1.11.2 -build_flags = ${common.build_flags_esp32} -D LEDPIN=16 -lib_ignore = - ESPAsyncTCP - ESPAsyncUDP +[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 [env:wemos_shield_esp32] board = esp32dev -platform = espressif32@1.11.2 -upload_port = /dev/cu.SLAB_USBtoUART -monitor_port = /dev/cu.SLAB_USBtoUART +platform = ${esp32.platform} +platform_packages = ${esp32.platform_packages} upload_speed = 460800 -build_flags = ${common.build_flags_esp32} -D LEDPIN=16 -D RLYPIN=19 -D BTNPIN=17 -lib_ignore = - ESPAsyncTCP - ESPAsyncUDP +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp32} + -D LEDPIN=16 + -D RLYPIN=19 + -D BTNPIN=17 + -D IRPIN=18 + -D UWLED_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] board = esp32dev +build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp32} -D LEDPIN=27 -D BTNPIN=39 -lib_ignore = - ESPAsyncTCP - ESPAsyncUDP -platform = espressif32@1.11.2 +lib_deps = ${esp32.lib_deps} +platform = ${esp32.platform} +platform_packages = ${esp32.platform_packages} +board_build.partitions = ${esp32.default_partitions} + +[env:sp501e] +board = esp_wroom_02 +platform = ${common.platform_wled_default} +board_build.ldscript = ${common.ldscript_2m512k} +build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=1 +lib_deps = ${esp8266.lib_deps} + +[env:sp511e] +board = esp_wroom_02 +platform = ${common.platform_wled_default} +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: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_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: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_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} + + +[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} -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: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_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA -D USERMOD_MY9291 +lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ -# travis test board configurations +# codm pixel controller board configurations +# codm-controller-0_6 can also be used for the TYWE3S controller # ------------------------------------------------------------------------------ -[env:travis_esp8266] -extends = env:d1_mini -build_type = debug -build_flags = ${common.build_flags_esp8266} ${common.debug_flags} ${common.build_flags_all_features} +[env:codm-controller-0_6] +board = esp_wroom_02 +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_2m512k} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} +lib_deps = ${esp8266.lib_deps} -[env:travis_esp32] -extends = env:esp32dev -build_type = debug -build_flags = ${common.build_flags_esp32} ${common.debug_flags} ${common.build_flags_all_features} +[env:codm-controller-0_6-rev2] +board = esp_wroom_02 +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} +lib_deps = ${esp8266.lib_deps} + +# ------------------------------------------------------------------------------ +# EleksTube-IPS +# ------------------------------------------------------------------------------ +[env:elekstube_ips] +board = esp32dev +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 + -D USERMOD_ELEKSTUBE_IPS + -D LEDPIN=12 + -D RLYPIN=27 + -D BTNPIN=34 + -D DEFAULT_LED_COUNT=6 + # Display config + -D ST7789_DRIVER + -D TFT_WIDTH=135 + -D TFT_HEIGHT=240 + -D CGRAM_OFFSET + -D TFT_SDA_READ + -D TFT_MOSI=23 + -D TFT_SCLK=18 + -D TFT_DC=25 + -D TFT_RST=26 + -D SPI_FREQUENCY=40000000 + -D USER_SETUP_LOADED +monitor_filters = esp32_exception_decoder +lib_deps = + ${esp32.lib_deps} + TFT_eSPI @ ^2.3.70 +board_build.partitions = ${esp32.default_partitions} diff --git a/platformio_override.ini.example b/platformio_override.ini.example deleted file mode 100644 index ba829aa1..00000000 --- a/platformio_override.ini.example +++ /dev/null @@ -1,36 +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 - -[env:esp8266_1m_custom] -board = esp01_1m -platform = ${common.arduino_core_2_4_2} -board_build.ldscript = ${common.ldscript_1m0m} -build_flags = ${common.build_flags_esp8266} - -D WLED_DISABLE_OTA - -D WLED_DISABLE_ALEXA - -D WLED_DISABLE_BLYNK - -D WLED_DISABLE_CRONIXIE - -D WLED_DISABLE_HUESYNC - -D WLED_DISABLE_INFRARED -; PIN defines - uncomment and change, if needed: -; -D LEDPIN=2 -; -D BTNPIN=0 -; -D IR_PIN=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 -; to drive analog LED strips (aka 5050), uncomment the following -; PWM pins 5,12,13,15 are used with Magic Home LED Controller (default) -; -D WLED_USE_ANALOG_LEDS -; for the H801 controller (PINs 15,13,12,14 (W2 = 04)) uncomment this -; -D WLED_USE_H801 -; for the BW-LT11 controller (PINs 12,4,14,5 ) uncomment this -; -D WLED_USE_BWLT11 -; and to enable channel 5 for RGBW-CT led strips this -; -D WLED_USE_5CH_LEDS diff --git a/platformio_override.ini.sample b/platformio_override.ini.sample new file mode 100644 index 00000000..d6ea5d96 --- /dev/null +++ b/platformio_override.ini.sample @@ -0,0 +1,65 @@ +# 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_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 +; +; set default color order of your led strip +; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB diff --git a/readme.md b/readme.md index fa1446b2..dda6634a 100644 --- a/readme.md +++ b/readme.md @@ -1,95 +1,84 @@

- + + - + + +

- + # Welcome to my project WLED! ✨ -A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control NeoPixel (WS2812B, WS2811, SK6812, APA102) LEDs or also SPI based chipsets like the WS2801! +A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102! ## ⚙️ Features -- WS2812FX library integrated for over 100 special effects +- 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 -- 16 user presets to save and load colors/effects easily, supports cycling through them. -- Macro functions to automatically execute API calls +- 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 -- Configurable analog clock + support for the Cronixie kit by Diamex -- Configurable Auto Brightness limit for safer operation +- Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods) +- Configurable Auto Brightness limit for safe operation +- Filesystem-based config for easier backup of presets and settings ## 💡 Supported light control interfaces -- WLED app for Android and iOS +- 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 -- E1.31 -- Hyperion +- MQTT +- E1.31, Art-Net, DDP and TPM2.net +- [diyHue](https://github.com/diyhue/diyHue) (Wled is supported by diyHue, including Hue Sync Entertainment under udp. Thanks to [Gregory Mallios](https://github.com/gmallios)) +- [Hyperion](https://github.com/hyperion-project/hyperion.ng) - UDP realtime - Alexa voice control (including dimming and color) - Sync to Philips hue lights -- Adalight (PC ambilight via serial) +- Adalight (PC ambilight via serial) and TPM2 - Sync color of multiple WLED devices (UDP notifier) - Infrared remotes (24-key RGB, receiver required) - Simple timers/schedules (time from NTP, timezones/DST supported) ## 📲 Quick start guide and documentation -See the [wiki](https://github.com/Aircoookie/WLED/wiki)! +See the [documentation on our official site](https://kno.wled.ge)! -DrZzs has made some excellent video guides: -[Introduction, hardware and installation](https://www.youtube.com/watch?v=tXvtxwK3jRk) -[Settings, tips and tricks](https://www.youtube.com/watch?v=6eCE2BpLaUQ) +[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! -If you'd rather read, here is a very [detailed step-by-step beginner tutorial](https://tynick.com/blog/11-03-2019/getting-started-with-wled-on-esp8266/) by tynick! - -Russian speakers, check out the videos by Room31: -[WLED Firmware Overview: Interface and Settings](https://youtu.be/h7lKsczEI7E) -[ESP8266 based LED controller for WS2812b strip. WLED Firmware + OpenHAB](https://youtu.be/K4ioTt3XvGc) - -## 🖼️ Images +## 🖼️ User interface -## 💾 Compatible LED Strips -Type | Voltage | Comments -|---|---|---| -WS2812B | 5v | -WS2813 | 5v | -SK6812 | 5v | RGBW -APA102 | 5v | C/D -WS2801 | 5v | C/D -LPD8806 | 5v | C/D -TM1814 | 12v | RGBW -WS2811 | 12v | 3-LED segments -WS2815 | 12v | -GS8208 | 12v | +## 💾 Compatible hardware + +See [here](https://kno.wled.ge/basics/compatible-hardware)! ## ✌️ Other Licensed under the MIT license -Credits [here](https://github.com/Aircoookie/WLED/wiki/Contributors-&-About)! - -Uses Linearicons by Perxis! +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 photoeleptic seizures 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. -As per the MIT license, i assume no liability for any damage to you or any other person or equipment. + +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.in b/requirements.in new file mode 100644 index 00000000..7c715125 --- /dev/null +++ b/requirements.in @@ -0,0 +1 @@ +platformio diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..17eca159 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,58 @@ +# +# This file is autogenerated by pip-compile with python 3.8 +# To update, run: +# +# pip-compile +# +aiofiles==22.1.0 + # via platformio +ajsonrpc==1.2.0 + # via platformio +anyio==3.6.2 + # via starlette +bottle==0.12.25 + # via platformio +certifi==2023.7.22 + # via requests +charset-normalizer==3.1.0 + # via requests +click==8.1.3 + # via + # platformio + # uvicorn +colorama==0.4.6 + # via platformio +h11==0.14.0 + # via + # uvicorn + # wsproto +idna==3.4 + # via + # anyio + # requests +marshmallow==3.19.0 + # via platformio +packaging==23.1 + # via marshmallow +platformio==6.1.6 + # via -r requirements.in +pyelftools==0.29 + # via platformio +pyserial==3.5 + # via platformio +requests==2.31.0 + # via platformio +semantic-version==2.10.0 + # via platformio +sniffio==1.3.0 + # via anyio +starlette==0.23.1 + # via platformio +tabulate==0.9.0 + # via platformio +urllib3==1.26.15 + # via requests +uvicorn==0.20.0 + # via platformio +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 00000000..a179a89d --- /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.csv b/tools/WLED_ESP32_16MB.csv new file mode 100644 index 00000000..de78209d --- /dev/null +++ b/tools/WLED_ESP32_16MB.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, 0x200000, +app1, app, ota_1, 0x210000,0x200000, +spiffs, data, spiffs, 0x410000,0xBE0000, \ No newline at end of file diff --git a/tools/WLED_ESP32_16MB_9MB_FS.csv b/tools/WLED_ESP32_16MB_9MB_FS.csv new file mode 100644 index 00000000..f2f3f778 --- /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 00000000..7a1cf15f --- /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_4MB_1MB_FS.csv b/tools/WLED_ESP32_4MB_1MB_FS.csv new file mode 100644 index 00000000..5dec3c06 --- /dev/null +++ b/tools/WLED_ESP32_4MB_1MB_FS.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, \ No newline at end of file diff --git a/tools/WLED_ESP32_8MB.csv b/tools/WLED_ESP32_8MB.csv new file mode 100644 index 00000000..3cf3afc3 --- /dev/null +++ b/tools/WLED_ESP32_8MB.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x200000, +app1, app, ota_1, 0x210000,0x200000, +spiffs, data, spiffs, 0x410000,0x3E0000, +coredump, data, coredump,,64K diff --git a/tools/cdata.js b/tools/cdata.js index 4bf2bba3..90619ba6 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,19 +64,57 @@ 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"); + html = filter(html, "html-minify-ui"); + console.info("Minified to " + html.length + " characters"); if (error) { console.warn(error); @@ -76,7 +122,7 @@ function writeHtmlGzipped(sourceFile, resultFile) { } html = adoptVersionAndRepo(html); - zlib.gzip(html, function (error, result) { + zlib.gzip(html, { level: zlib.constants.Z_BEST_COMPRESSION }, function (error, result) { if (error) { console.warn(error); throw error; @@ -88,13 +134,13 @@ function writeHtmlGzipped(sourceFile, resultFile) { * Binary array for the Web UI. * gzip is used for smaller size and improved speeds. * - * Please see https://github.com/Aircoookie/WLED/wiki/Add-own-functionality#web-ui + * 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 ${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} }; `; @@ -104,35 +150,10 @@ ${array} }); } -const CleanCSS = require("clean-css"); -const MinifyHTML = require("html-minifier").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 { - console.warn("Unknown filter: " + type); - return str; - } -} - function specToChunk(srcDir, s) { if (s.method == "plaintext") { const buf = fs.readFileSync(srcDir + "/" + s.file); - const str = buf.toString("ascii"); + const str = buf.toString("utf-8"); const chunk = ` // Autogenerated from ${srcDir}/${s.file}, do not edit!! const char ${s.name}[] PROGMEM = R"${s.prepend || ""}${filter(str, s.filter)}${ @@ -141,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); @@ -152,7 +188,7 @@ ${result} }; `; - return s.mangle ? s.mangle(chunk) : chunk; + return chunk; } else { console.warn("Unknown method: " + s.method); return undefined; @@ -163,7 +199,7 @@ function writeChunks(srcDir, specs, resultFile) { let src = `/* * More web UI HTML source arrays. * This file is auto generated, please don't make any changes manually. - * Instead, see https://github.com/Aircoookie/WLED/wiki/Add-own-functionality#web-ui + * Instead, see https://kno.wled.ge/advanced/custom-features/#changing-web-ui * to find out how to easily modify the web UI source! */ `; @@ -182,141 +218,115 @@ 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", }, { 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", + method: "gzip", + filter: "html-minify", + }, + { + 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" ); @@ -327,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'), @@ -361,25 +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: "liveviewws2D.htm", + name: "PAGE_liveviewws2D", + method: "gzip", + filter: "html-minify", + }, + { + file: "404.htm", + name: "PAGE_404", + method: "gzip", filter: "html-minify", }, { @@ -387,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/tools/fps_test.htm b/tools/fps_test.htm new file mode 100644 index 00000000..5858e8ad --- /dev/null +++ b/tools/fps_test.htm @@ -0,0 +1,232 @@ + + + + WLED frame rate test tool + + + + +

Starship monitoring dashboard

+ (or rather just a WLED frame rate tester lol)

+ IP:
+ Time per effect: s
+ Effects to test: + + + + +
+ Extra JSON:
+ +
+ LEDs: -, Seg: -, Bri: -
+ FPS min: -, max: -, avg: -

+
+

+ + + + + \ No newline at end of file diff --git a/wled00/data/jsontest.htm b/tools/json_test.htm similarity index 100% rename from wled00/data/jsontest.htm rename to tools/json_test.htm diff --git a/tools/multi-update.cmd b/tools/multi-update.cmd new file mode 100644 index 00000000..7fd1cf44 --- /dev/null +++ b/tools/multi-update.cmd @@ -0,0 +1,16 @@ +@echo off +SETLOCAL +SET FWPATH=c:\path\to\your\WLED\build_output\firmware +GOTO ESPS + +:UPDATEONE +IF NOT EXIST %FWPATH%\%2 GOTO SKIP + ping -w 1000 -n 1 %1 | find "TTL=" || GOTO SKIP + ECHO Updating %1 + curl -s -F "update=@%FWPATH%/%2" %1/update >nul +:SKIP +GOTO:EOF + +:ESPS +call :UPDATEONE 192.168.x.x firmware.bin +call :UPDATEONE .... diff --git a/tools/multi-update.sh b/tools/multi-update.sh new file mode 100644 index 00000000..40e26221 --- /dev/null +++ b/tools/multi-update.sh @@ -0,0 +1,19 @@ +#!/bin/bash +FWPATH=/path/to/your/WLED/build_output/firmware + +update_one() { +if [ -f $FWPATH/$2 ]; then + ping -c 1 $1 >/dev/null + PINGRESULT=$? + if [ $PINGRESULT -eq 0 ]; then + echo Updating $1 + curl -s -F "update=@${FWPATH}/$2" $1/update >/dev/null + return 0 + fi + return 1 +fi +} + +update_one 192.168.x.x firmware.bin +update_one 192.168.x.x firmware.bin +# ... \ No newline at end of file diff --git a/usermods/ADS1115_v2/ChannelSettings.h b/usermods/ADS1115_v2/ChannelSettings.h new file mode 100644 index 00000000..26538a14 --- /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 00000000..44092bc8 --- /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 00000000..5e2b4b27 --- /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 00000000..596f0acb --- /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 new file mode 100644 index 00000000..151cf1d4 --- /dev/null +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -0,0 +1,553 @@ +/* + * Usermod for detecting people entering/leaving a staircase and switching the + * staircase on/off. + * + * Edit the Animated_Staircase_config.h file to compile this usermod for your + * specific configuration. + * + * See the accompanying README.md file for more info. + */ +#pragma once +#include "wled.h" + +class Animated_Staircase : public Usermod { + private: + + /* configuration (available in API and stored in flash) */ + bool enabled = false; // Enable this usermod + unsigned long segment_delay_ms = 150; // Time between switching each segment + unsigned long on_time_ms = 30000; // The time for the light to stay on + int8_t topPIRorTriggerPin = -1; // disabled + int8_t bottomPIRorTriggerPin = -1; // disabled + int8_t topEchoPin = -1; // disabled + int8_t bottomEchoPin = -1; // disabled + bool useUSSensorTop = false; // using PIR or UltraSound sensor? + 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 + + /* runtime variables */ + bool initDone = false; + + // Time between checking of the sensors + const unsigned int scanDelay = 100; + + // Lights on or off. + // Flipping this will start a transition. + bool on = false; + + // Swipe direction for current transition + #define SWIPE_UP true + #define SWIPE_DOWN false + bool swipe = SWIPE_UP; + + // Indicates which Sensor was seen last (to determine + // the direction when swiping off) + #define LOWER false + #define UPPER true + bool lastSensor = LOWER; + + // Time of the last transition action + unsigned long lastTime = 0; + + // Time of the last sensor check + unsigned long lastScanTime = 0; + + // Last time the lights were switched on or off + unsigned long lastSwitchTime = 0; + + // segment id between onIndex and offIndex are on. + // controll the swipe by setting/moving these indices around. + // onIndex must be less than or equal to offIndex + byte onIndex = 0; + byte offIndex = 0; + + // The maximum number of configured segments. + // Dynamically updated based on user configuration. + byte maxSegmentId = 1; + byte minSegmentId = 0; + + // These values are used by the API to read the + // last sensor state, or trigger a sensor + // through the API + bool topSensorRead = false; + bool topSensorWrite = false; + bool bottomSensorRead = false; + bool bottomSensorWrite = false; + bool topSensorState = false; + bool bottomSensorState = false; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _segmentDelay[]; + static const char _onTime[]; + static const char _useTopUltrasoundSensor[]; + static const char _topPIRorTrigger_pin[]; + static const char _topEcho_pin[]; + static const char _useBottomUltrasoundSensor[]; + static const char _bottomPIRorTrigger_pin[]; + static const char _bottomEcho_pin[]; + static const char _topEchoCm[]; + static const char _bottomEchoCm[]; + + 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() { + for (int i = minSegmentId; i < maxSegmentId; i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; // skip gaps + if (i >= onIndex && i < offIndex) { + 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 + // seg.setMode(mainsegment.mode); + // seg.setColor(0, mainsegment.colors[0]); + } else { + seg.setOption(SEG_OPTION_ON, false); + } + // Always mark segments as "transitional", we are animating the staircase + //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); + } + + /* + * Detects if an object is within ultrasound range. + * signalPin: The pin where the pulse is sent + * echoPin: The pin where the echo is received + * maxTimeUs: Detection timeout in microseconds. If an echo is + * 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. + * Since the sound has to travel back and forth, the detection + * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2. + * + * For practical reasons, here are some useful distances: + * + * Distance = maxtime + * 5 cm = 292 uS + * 10 cm = 583 uS + * 20 cm = 1166 uS + * 30 cm = 1749 uS + * 50 cm = 2915 uS + * 100 cm = 5831 uS + */ + bool ultrasoundRead(int8_t signalPin, int8_t echoPin, unsigned int maxTimeUs) { + if (signalPin<0 || echoPin<0) return false; + digitalWrite(signalPin, LOW); + delayMicroseconds(2); + digitalWrite(signalPin, HIGH); + delayMicroseconds(10); + digitalWrite(signalPin, LOW); + return pulseIn(echoPin, HIGH, maxTimeUs) > 0; + } + + bool checkSensors() { + bool sensorChanged = false; + + if ((millis() - lastScanTime) > scanDelay) { + lastScanTime = millis(); + + bottomSensorRead = bottomSensorWrite || + (!useUSSensorBottom ? + (bottomPIRorTriggerPin<0 ? false : digitalRead(bottomPIRorTriggerPin)) : + ultrasoundRead(bottomPIRorTriggerPin, bottomEchoPin, bottomMaxDist*59) // cm to us + ); + topSensorRead = topSensorWrite || + (!useUSSensorTop ? + (topPIRorTriggerPin<0 ? false : digitalRead(topPIRorTriggerPin)) : + ultrasoundRead(topPIRorTriggerPin, topEchoPin, topMaxDist*59) // cm to us + ); + + if (bottomSensorRead != bottomSensorState) { + bottomSensorState = bottomSensorRead; // change previous state + sensorChanged = true; + publishMqtt(true, bottomSensorState ? "on" : "off"); + DEBUG_PRINTLN(F("Bottom sensor changed.")); + } + + if (topSensorRead != topSensorState) { + topSensorState = topSensorRead; // change previous state + sensorChanged = true; + publishMqtt(false, topSensorState ? "on" : "off"); + DEBUG_PRINTLN(F("Top sensor changed.")); + } + + // Values read, reset the flags for next API call + topSensorWrite = false; + bottomSensorWrite = false; + + if (topSensorRead != bottomSensorRead) { + lastSwitchTime = millis(); + + if (on) { + lastSensor = topSensorRead; + } else { + // If the bottom sensor triggered, we need to swipe up, ON + swipe = bottomSensorRead; + + DEBUG_PRINT(F("ON -> Swipe ")); + DEBUG_PRINTLN(swipe ? F("up.") : F("down.")); + + if (onIndex == offIndex) { + // Position the indices for a correct on-swipe + if (swipe == SWIPE_UP) { + onIndex = minSegmentId; + } else { + onIndex = maxSegmentId; + } + offIndex = onIndex; + } + on = true; + } + } + } + return sensorChanged; + } + + void autoPowerOff() { + if ((millis() - lastSwitchTime) > on_time_ms) { + // if sensors are still on, do nothing + if (bottomSensorState || topSensorState) return; + + // Swipe OFF in the direction of the last sensor detection + swipe = lastSensor; + on = false; + + DEBUG_PRINT(F("OFF -> Swipe ")); + DEBUG_PRINTLN(swipe ? F("up.") : F("down.")); + } + } + + void updateSwipe() { + if ((millis() - lastTime) > segment_delay_ms) { + lastTime = millis(); + + byte oldOn = onIndex; + byte oldOff = offIndex; + if (on) { + // Turn on all segments + onIndex = MAX(minSegmentId, onIndex - 1); + offIndex = MIN(maxSegmentId, offIndex + 1); + } else { + if (swipe == SWIPE_UP) { + onIndex = MIN(offIndex, onIndex + 1); + } else { + offIndex = MAX(onIndex, offIndex - 1); + } + } + if (oldOn != onIndex || oldOff != offIndex) updateSegments(); // reduce the number of updates to necessary ones + } + } + + // send sesnor values to JSON API + void writeSensorsToJson(JsonObject& staircase) { + staircase[F("top-sensor")] = topSensorRead; + staircase[F("bottom-sensor")] = bottomSensorRead; + } + + // allow overrides from JSON API + void readSensorsFromJson(JsonObject& staircase) { + bottomSensorWrite = bottomSensorState || (staircase[F("bottom-sensor")].as()); + topSensorWrite = topSensorState || (staircase[F("top-sensor")].as()); + } + + void enable(bool enable) { + if (enable) { + DEBUG_PRINTLN(F("Animated Staircase enabled.")); + DEBUG_PRINT(F("Delay between steps: ")); + DEBUG_PRINT(segment_delay_ms); + DEBUG_PRINT(F(" milliseconds.\nStairs switch off after: ")); + DEBUG_PRINT(on_time_ms / 1000); + DEBUG_PRINTLN(F(" seconds.")); + + if (!useUSSensorBottom) + pinMode(bottomPIRorTriggerPin, INPUT_PULLUP); + else { + pinMode(bottomPIRorTriggerPin, OUTPUT); + pinMode(bottomEchoPin, INPUT); + } + + if (!useUSSensorTop) + pinMode(topPIRorTriggerPin, INPUT_PULLUP); + else { + 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 + transitionDelayTemp = transitionDelay = segment_delay_ms; + strip.setTransition(segment_delay_ms/100); + strip.trigger(); + } else { + // Restore segment options + 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 dvices/UI of change + colorUpdated(CALL_MODE_DIRECT_CHANGE); + DEBUG_PRINTLN(F("Animated Staircase disabled.")); + } + enabled = enable; + } + + public: + void setup() { + // standardize invalid pin numbers to -1 + if (topPIRorTriggerPin < 0) topPIRorTriggerPin = -1; + if (topEchoPin < 0) topEchoPin = -1; + if (bottomPIRorTriggerPin < 0) bottomPIRorTriggerPin = -1; + if (bottomEchoPin < 0) bottomEchoPin = -1; + // allocate pins + PinManagerPinType pins[4] = { + { topPIRorTriggerPin, useUSSensorTop }, + { topEchoPin, false }, + { bottomPIRorTriggerPin, useUSSensorBottom }, + { bottomEchoPin, false }, + }; + // NOTE: this *WILL* return TRUE if all the pins are set to -1. + // this is *BY DESIGN*. + if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) { + topPIRorTriggerPin = -1; + topEchoPin = -1; + bottomPIRorTriggerPin = -1; + bottomEchoPin = -1; + enabled = false; + } + enable(enabled); + initDone = true; + } + + 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(); + 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) + * topic should look like: /swipe with amessage of [up|down] + */ + bool onMqttMessage(char* topic, char* payload) { + if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/swipe"), 6) == 0) { + String action = payload; + if (action == "up") { + bottomSensorWrite = true; + return true; + } else if (action == "down") { + topSensorWrite = true; + return true; + } else 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) { + //(re)subscribe to required topics + char subuf[64]; + if (mqttDeviceTopic[0] != 0) { + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/swipe")); + mqtt->subscribe(subuf, 0); + } + } +#endif + + void addToJsonState(JsonObject& root) { + JsonObject staircase = root[FPSTR(_name)]; + if (staircase.isNull()) { + staircase = root.createNestedObject(FPSTR(_name)); + } + writeSensorsToJson(staircase); + DEBUG_PRINTLN(F("Staircase sensor state exposed in API.")); + } + + /* + * Reads configuration settings from the json API. + * See void addToJsonState(JsonObject& root) + */ + 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()) { + en = staircase[FPSTR(_enabled)].as(); + } else { + String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on + 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. + */ + void addToConfig(JsonObject& root) { + JsonObject staircase = root[FPSTR(_name)]; + if (staircase.isNull()) { + staircase = root.createNestedObject(FPSTR(_name)); + } + staircase[FPSTR(_enabled)] = enabled; + staircase[FPSTR(_segmentDelay)] = segment_delay_ms; + staircase[FPSTR(_onTime)] = on_time_ms / 1000; + staircase[FPSTR(_useTopUltrasoundSensor)] = useUSSensorTop; + staircase[FPSTR(_topPIRorTrigger_pin)] = topPIRorTriggerPin; + staircase[FPSTR(_topEcho_pin)] = useUSSensorTop ? topEchoPin : -1; + staircase[FPSTR(_useBottomUltrasoundSensor)] = useUSSensorBottom; + staircase[FPSTR(_bottomPIRorTrigger_pin)] = bottomPIRorTriggerPin; + staircase[FPSTR(_bottomEcho_pin)] = useUSSensorBottom ? bottomEchoPin : -1; + staircase[FPSTR(_topEchoCm)] = topMaxDist; + staircase[FPSTR(_bottomEchoCm)] = bottomMaxDist; + DEBUG_PRINTLN(F("Staircase config saved.")); + } + + /* + * Reads the configuration to internal flash memory before setup() is called. + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject& root) { + bool oldUseUSSensorTop = useUSSensorTop; + bool oldUseUSSensorBottom = useUSSensorBottom; + int8_t oldTopAPin = topPIRorTriggerPin; + int8_t oldTopBPin = topEchoPin; + int8_t oldBottomAPin = bottomPIRorTriggerPin; + int8_t oldBottomBPin = bottomEchoPin; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_enabled)] | enabled; + + segment_delay_ms = top[FPSTR(_segmentDelay)] | segment_delay_ms; + segment_delay_ms = (unsigned long) min((unsigned long)10000,max((unsigned long)10,(unsigned long)segment_delay_ms)); // max delay 10s + + on_time_ms = top[FPSTR(_onTime)] | on_time_ms/1000; + on_time_ms = min(900,max(10,(int)on_time_ms)) * 1000; // min 10s, max 15min + + useUSSensorTop = top[FPSTR(_useTopUltrasoundSensor)] | useUSSensorTop; + topPIRorTriggerPin = top[FPSTR(_topPIRorTrigger_pin)] | topPIRorTriggerPin; + topEchoPin = top[FPSTR(_topEcho_pin)] | topEchoPin; + + useUSSensorBottom = top[FPSTR(_useBottomUltrasoundSensor)] | useUSSensorBottom; + bottomPIRorTriggerPin = top[FPSTR(_bottomPIRorTrigger_pin)] | bottomPIRorTriggerPin; + 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) + bottomMaxDist = top[FPSTR(_bottomEchoCm)] | bottomMaxDist; + bottomMaxDist = min(150,max(30,(int)bottomMaxDist)); // max distance ~1.5m (a lag of 9ms may be expected) + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + DEBUG_PRINTLN(F(" config loaded.")); + } else { + // changing parameters from settings page + DEBUG_PRINTLN(F(" config (re)loaded.")); + bool changed = false; + if ((oldUseUSSensorTop != useUSSensorTop) || + (oldUseUSSensorBottom != useUSSensorBottom) || + (oldTopAPin != topPIRorTriggerPin) || + (oldTopBPin != topEchoPin) || + (oldBottomAPin != bottomPIRorTriggerPin) || + (oldBottomBPin != bottomEchoPin)) { + changed = true; + pinManager.deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase); + pinManager.deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase); + pinManager.deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase); + pinManager.deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase); + } + if (changed) setup(); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return true; + } + + /* + * Shows the delay between steps and power-off time in the "info" + * tab of the web-UI. + */ + 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); + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char Animated_Staircase::_name[] PROGMEM = "staircase"; +const char Animated_Staircase::_enabled[] PROGMEM = "enabled"; +const char Animated_Staircase::_segmentDelay[] PROGMEM = "segment-delay-ms"; +const char Animated_Staircase::_onTime[] PROGMEM = "on-time-s"; +const char Animated_Staircase::_useTopUltrasoundSensor[] PROGMEM = "useTopUltrasoundSensor"; +const char Animated_Staircase::_topPIRorTrigger_pin[] PROGMEM = "topPIRorTrigger_pin"; +const char Animated_Staircase::_topEcho_pin[] PROGMEM = "topEcho_pin"; +const char Animated_Staircase::_useBottomUltrasoundSensor[] PROGMEM = "useBottomUltrasoundSensor"; +const char Animated_Staircase::_bottomPIRorTrigger_pin[] PROGMEM = "bottomPIRorTrigger_pin"; +const char Animated_Staircase::_bottomEcho_pin[] PROGMEM = "bottomEcho_pin"; +const char Animated_Staircase::_topEchoCm[] PROGMEM = "top-dist-cm"; +const char Animated_Staircase::_bottomEchoCm[] PROGMEM = "bottom-dist-cm"; diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md new file mode 100644 index 00000000..61c1cb2d --- /dev/null +++ b/usermods/Animated_Staircase/README.md @@ -0,0 +1,130 @@ +# Usermod Animated Staircase +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 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 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 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). + +Before compiling, you have to make the following modifications: + +Edit `usermods_list.cpp`: +1. Open `wled00/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 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 the time needed for an echo (see below). + +## Hardware installation +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 + 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 + do for the datacable! + +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 + lowest segment id. +2. Save your segments into a preset. +3. Ideally, add the preset in the config > LED setup menu to the "apply + preset **n** at boot" setting. + +## Changing behavior through API +The Staircase settings can be changed through the WLED JSON api. + +**NOTE:** We are using [curl](https://curl.se/) to send HTTP POSTs to the WLED API. +If you're using Windows and want to use the curl commands, replace the `\` with a `^` +or remove them and put everything on one line. + + +| Setting | Description | Default | +|------------------|---------------------------------------------------------------|---------| +| enabled | Enable or disable the usermod | true | +| bottom-sensor | Manually trigger a down to up animation via API | false | +| top-sensor | Manually trigger an up to down animation via API | false | + + +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 "state" element: + +```json +{ + "state": { + "staircase": { + "enabled": true, + "bottom-sensor": 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 you to play with the lights without the usermod switching them on or off. + +To disable the usermod: + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d {"staircase":{"enabled":false}} \ + xxx.xxx.xxx.xxx/json/state +``` + +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 etc. + +When an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors. + +**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 +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" \ + -d '{"staircase":{"bottom-sensor":true}}' \ + xxx.xxx.xxx.xxx/json/state +``` + +Likewise, to trigger the top sensor: + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"staircase":{"top-sensor":true}}' \ + xxx.xxx.xxx.xxx/json/state +``` +**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 the usermod. + +Have fun with this usermod.
+www.rolfje.com + +Modifications @blazoncek + +## Change log +2021-04 +* Adaptation for runtime configuration. diff --git a/usermods/Artemis_reciever/readme.md b/usermods/Artemis_reciever/readme.md new file mode 100644 index 00000000..11b94908 --- /dev/null +++ b/usermods/Artemis_reciever/readme.md @@ -0,0 +1,5 @@ +Usermod to allow WLED to receive via UDP port from RGB.NET (and therefore add as a device to be controlled within artemis on PC) + +This is only a very simple code to support a single led strip, it does not support the full function of the RGB.NET sketch for esp8266 only what is needed to be used with Artemis. It will show as a ws281x device in artemis when you provide the correct hostname or ip. Artemis queries the number of LEDs via the web interface (/config) but communication to set the LEDs is all done via the UDP interface. + +To install, copy the usermod.cpp file to wled00 folder and recompile \ No newline at end of file diff --git a/usermods/Artemis_reciever/usermod.cpp b/usermods/Artemis_reciever/usermod.cpp new file mode 100644 index 00000000..5f230dfd --- /dev/null +++ b/usermods/Artemis_reciever/usermod.cpp @@ -0,0 +1,93 @@ +/* + * RGB.NET (artemis) receiver + * + * This works via the UDP, http is not supported apart from reporting LED count + * + * + */ +#include "wled.h" +#include + +WiFiUDP UDP; +const unsigned int RGBNET_localUdpPort = 1872; // local port to listen on +unsigned char RGBNET_packet[770]; +long lastTime = 0; +int delayMs = 10; +bool isRGBNETUDPEnabled; + +void RGBNET_readValues() { + + int RGBNET_packetSize = UDP.parsePacket(); + if (RGBNET_packetSize) { + // receive incoming UDP packets + int sequenceNumber = UDP.read(); + int channel = UDP.read(); + + //channel data is not used we only supports one channel + int len = UDP.read(RGBNET_packet, strip.getLengthTotal()*3); + if(len==0){ + return; + } + + for (int i = 0; i < len; i=i+3) { + strip.setPixelColor(i/3, RGBNET_packet[i], RGBNET_packet[i+1], RGBNET_packet[i+2], 0); + } + //strip.show(); + } +} + +//update LED strip +void RGBNET_show() { + strip.show(); + lastTime = millis(); +} + +//This function provides a json with info on the number of LEDs connected +// it is needed by artemis to know how many LEDs to display on the surface +void handleConfig(AsyncWebServerRequest *request) +{ + String config = (String)"{\ + \"channels\": [\ + {\ + \"channel\": 1,\ + \"leds\": " + strip.getLengthTotal() + "\ + },\ + {\ + \"channel\": 2,\ + \"leds\": " + "0" + "\ + },\ + {\ + \"channel\": 3,\ + \"leds\": " + "0" + "\ + },\ + {\ + \"channel\": 4,\ + \"leds\": " + "0" + "\ + }\ + ]\ +}"; + request->send(200, "application/json", config); +} + + +void userSetup() +{ + server.on("/config", HTTP_GET, [](AsyncWebServerRequest *request){ + handleConfig(request); + }); +} + +void userConnected() +{ + // new wifi, who dis? + UDP.begin(RGBNET_localUdpPort); + isRGBNETUDPEnabled = true; +} + +void userLoop() +{ + RGBNET_readValues(); + if (millis()-lastTime > delayMs) { + RGBNET_show(); + } +} \ No newline at end of file diff --git a/usermods/BH1750_v2/readme.md b/usermods/BH1750_v2/readme.md new file mode 100644 index 00000000..05a033cf --- /dev/null +++ b/usermods/BH1750_v2/readme.md @@ -0,0 +1,49 @@ +# BH1750 usermod + +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. + +## Compiliation + +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 new file mode 100644 index 00000000..5e597d01 --- /dev/null +++ b/usermods/BH1750_v2/usermod_bh1750.h @@ -0,0 +1,252 @@ +// 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 + +// the max frequency to check photoresistor, 10 seconds +#ifndef USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL +#define USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL 10000 +#endif + +// the min frequency to check photoresistor, 500 ms +#ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL +#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 +#endif + +// how many seconds after boot to take first measurement, 10 seconds +#ifndef USERMOD_BH1750_FIRST_MEASUREMENT_AT +#define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 +#endif + +// only report if differance grater than offset value +#ifndef USERMOD_BH1750_OFFSET_VALUE +#define USERMOD_BH1750_OFFSET_VALUE 1 +#endif + +class Usermod_BH1750 : public Usermod +{ +private: + int8_t offset = USERMOD_BH1750_OFFSET_VALUE; + + unsigned long maxReadingInterval = USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL; + unsigned long minReadingInterval = USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL; + unsigned long lastMeasurement = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); + unsigned long lastSend = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); + // flag to indicate we have finished the first readLightLevel call + // allows this library to report to the user how long until the first + // measurement + bool getLuminanceComplete = false; + + // flag set at startup + bool enabled = true; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + 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; + + bool checkBoundSensor(float newValue, float prevValue, float maxDiff) + { + 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() + { + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } + sensorFound = lightMeter.begin(); + initDone = true; + } + + void loop() + { + if ((!enabled) || strip.isUpdating()) + return; + + 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 < minReadingInterval) + { + return; + } + + bool shouldUpdate = now - lastSend > maxReadingInterval; + + float lux = lightMeter.readLightLevel(); + lastMeasurement = millis(); + getLuminanceComplete = true; + + if (shouldUpdate || checkBoundSensor(lux, lastLux, offset)) + { + lastLux = lux; + lastSend = millis(); +#ifndef WLED_DISABLE_MQTT + if (WLED_MQTT_CONNECTED) + { + 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(F("Missing MQTT connection. Not publishing data")); + } +#endif + } + } + + inline float getIlluminance() { + return (float)lastLux; + } + + void addToJsonInfo(JsonObject &root) + { + JsonObject user = root[F("u")]; + if (user.isNull()) + user = root.createNestedObject(F("u")); + + JsonArray lux_json = user.createNestedArray(F("Luminance")); + 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; + } else { + lux_json.add(lastLux); + lux_json.add(F(" lx")); + } + } + + // (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)] = enabled; + top[FPSTR(_maxReadInterval)] = maxReadingInterval; + top[FPSTR(_minReadInterval)] = minReadingInterval; + top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; + top[FPSTR(_offset)] = offset; + + DEBUG_PRINTLN(F("BH1750 config saved.")); + } + + // called before setup() to populate properties from values stored in cfg.json + bool readFromConfig(JsonObject &root) + { + // we look for JSON object. + JsonObject top = root[FPSTR(_name)]; + 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); + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + } + + return configComplete; + + } + + uint16_t getId() + { + return USERMOD_ID_BH1750; + } + +}; + +// strings to reduce flash memory usage (used more than twice) +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/BME280_v2/README.md b/usermods/BME280_v2/README.md new file mode 100644 index 00000000..0a4afbf1 --- /dev/null +++ b/usermods/BME280_v2/README.md @@ -0,0 +1,90 @@ +# 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) + +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 + +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 +``` + + +# 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` + +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 new file mode 100644 index 00000000..c7d25ec1 --- /dev/null +++ b/usermods/BME280_v2/usermod_bme280.h @@ -0,0 +1,456 @@ +// 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 // BME280 sensor +#include // BME280 extended measurements + +class UsermodBME280 : public Usermod +{ +private: + + // 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; + + // set the default pins based on the architecture, these get overridden by Usermod menu settings + #ifdef ESP8266 + //uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 + #endif + bool initDone = false; + + // BME280 sensor settings + BME280I2C::Settings settings{ + BME280::OSR_X16, // Temperature oversampling x16 + BME280::OSR_X16, // Humidity oversampling x16 + BME280::OSR_X16, // Pressure oversampling x16 + // Defaults + BME280::Mode_Forced, + BME280::StandbyTime_1000ms, + BME280::Filter_Off, + BME280::SpiEnable_False, + BME280I2C::I2CAddr_0x76 // I2C address. I2C specific. Default 0x76 + }; + + BME280I2C bme{settings}; + + uint8_t sensorType; + + // Measurement timers + long timer; + long lastTemperatureMeasure = 0; + long lastPressureMeasure = 0; + + // Current sensor values + float sensorTemperature; + float sensorHumidity; + float sensorHeatIndex; + float sensorDewPoint; + float sensorPressure; + String tempScale; + // Track previous sensor values + float lastTemperature; + float lastHumidity; + float lastHeatIndex; + float lastDewPoint; + float lastPressure; + + // 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 Farenheit being set in Usermod Menu) + void UpdateBME280Data(int SensorType) + { + float _temperature, _humidity, _pressure; + + if (UseCelsius) { + BME280::TempUnit tempUnit(BME280::TempUnit_Celsius); + EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius); + 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); + BME280::PresUnit presUnit(BME280::PresUnit_hPa); + + bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit); + + 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() + { + if (i2c_scl<0 || i2c_sda<0) { enabled = false; sensorType = 0; return; } + + if (!bme.begin()) + { + sensorType = 0; + DEBUG_PRINTLN(F("Could not find BME280 I2C sensor!")); + } + else + { + switch (bme.chipModel()) + { + case BME280::ChipModel_BME280: + sensorType = 1; + DEBUG_PRINTLN(F("Found BME280 sensor! Success.")); + break; + case BME280::ChipModel_BMP280: + sensorType = 2; + DEBUG_PRINTLN(F("Found BMP280 sensor! No Humidity available.")); + break; + default: + sensorType = 0; + 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 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) + { + lastTemperatureMeasure = timer; + + UpdateBME280Data(sensorType); + + 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) + { + 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 * 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) + { + publishMqtt("humidity", String(humidity, HumidityDecimals).c_str()); + } + + if (heatIndex != lastHeatIndex || PublishAlways) + { + publishMqtt("heat_index", String(heatIndex, TemperatureDecimals).c_str()); + } + + if (dewPoint != lastDewPoint || PublishAlways) + { + publishMqtt("dew_point", String(dewPoint, TemperatureDecimals).c_str()); + } + + lastHumidity = humidity; + lastHeatIndex = heatIndex; + lastDewPoint = dewPoint; + } + } + + if (timer - lastPressureMeasure >= PressureInterval * 1000) + { + lastPressureMeasure = timer; + + float pressure = roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals); + + if (pressure != lastPressure || PublishAlways) + { + publishMqtt("pressure", String(pressure, PressureDecimals).c_str()); + } + + lastPressure = pressure; + } + } + } + + 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/assets/battery_connection_schematic_01.png b/usermods/Battery/assets/battery_connection_schematic_01.png new file mode 100644 index 00000000..5ce01de6 Binary files /dev/null and b/usermods/Battery/assets/battery_connection_schematic_01.png differ diff --git a/usermods/Battery/assets/battery_connection_schematic_02.png b/usermods/Battery/assets/battery_connection_schematic_02.png new file mode 100644 index 00000000..03f41ca0 Binary files /dev/null and b/usermods/Battery/assets/battery_connection_schematic_02.png differ diff --git a/usermods/Battery/assets/battery_info_screen.png b/usermods/Battery/assets/battery_info_screen.png new file mode 100644 index 00000000..5aa60a03 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 00000000..b1134eb3 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 00000000..958bfe52 --- /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 00000000..d55573ab --- /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 posibilities + +## 🎈 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 parralel sumed 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 00000000..be3d8748 --- /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/readme.md b/usermods/Cronixie/readme.md new file mode 100644 index 00000000..1eeac8ed --- /dev/null +++ b/usermods/Cronixie/readme.md @@ -0,0 +1,8 @@ +# Cronixie clock usermod + +This usermod supports driving the Cronixie M and L clock kits by Diamex. + +## Installation + +Compile and upload after adding `-D USERMOD_CRONIXIE` to `build_flags` of your PlatformIO environment. +Make sure the Auto Brightness Limiter is enabled at 420mA (!) and configure 60 WS281x LEDs. \ No newline at end of file diff --git a/usermods/Cronixie/usermod_cronixie.h b/usermods/Cronixie/usermod_cronixie.h new file mode 100644 index 00000000..534fd3a7 --- /dev/null +++ b/usermods/Cronixie/usermod_cronixie.h @@ -0,0 +1,302 @@ +#pragma once + +#include "wled.h" + +class UsermodCronixie : public Usermod { + private: + unsigned long lastTime = 0; + char cronixieDisplay[7] = "HHMMSS"; + byte _digitOut[6] = {10,10,10,10,10,10}; + byte dP[6] = {255, 255, 255, 255, 255, 255}; + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + bool backlight = true; + + public: + void initCronixie() + { + if (dP[0] == 255) // if dP[0] is 255, cronixie is not yet init'ed + { + setCronixie(); + strip.getSegment(0).grouping = 10; // 10 LEDs per digit + } + } + + void setup() { + + } + + void loop() { + if (!toki.isTick()) return; + initCronixie(); + _overlayCronixie(); + strip.trigger(); + } + + byte getSameCodeLength(char code, int index, char const cronixieDisplay[]) + { + byte counter = 0; + + for (int i = index+1; i < 6; i++) + { + if (cronixieDisplay[i] == code) + { + counter++; + } else { + return counter; + } + } + return counter; + } + + void setCronixie() + { + /* + * digit purpose index + * 0-9 | 0-9 (incl. random) + * 10 | blank + * 11 | blank, bg off + * 12 | test upw. + * 13 | test dnw. + * 14 | binary AM/PM + * 15 | BB upper +50 for no trailing 0 + * 16 | BBB + * 17 | BBBB + * 18 | BBBBB + * 19 | BBBBBB + * 20 | H + * 21 | HH + * 22 | HHH + * 23 | HHHH + * 24 | M + * 25 | MM + * 26 | MMM + * 27 | MMMM + * 28 | MMMMM + * 29 | MMMMMM + * 30 | S + * 31 | SS + * 32 | SSS + * 33 | SSSS + * 34 | SSSSS + * 35 | SSSSSS + * 36 | Y + * 37 | YY + * 38 | YYYY + * 39 | I + * 40 | II + * 41 | W + * 42 | WW + * 43 | D + * 44 | DD + * 45 | DDD + * 46 | V + * 47 | VV + * 48 | VVV + * 49 | VVVV + * 50 | VVVVV + * 51 | VVVVVV + * 52 | v + * 53 | vv + * 54 | vvv + * 55 | vvvv + * 56 | vvvvv + * 57 | vvvvvv + */ + + //H HourLower | HH - Hour 24. | AH - Hour 12. | HHH Hour of Month | HHHH Hour of Year + //M MinuteUpper | MM Minute of Hour | MMM Minute of 12h | MMMM Minute of Day | MMMMM Minute of Month | MMMMMM Minute of Year + //S SecondUpper | SS Second of Minute | SSS Second of 10 Minute | SSSS Second of Hour | SSSSS Second of Day | SSSSSS Second of Week + //B AM/PM | BB 0-6/6-12/12-18/18-24 | BBB 0-3... | BBBB 0-1.5... | BBBBB 0-1 | BBBBBB 0-0.5 + + //Y YearLower | YY - Year LU | YYYY - Std. + //I MonthLower | II - Month of Year + //W Week of Month | WW Week of Year + //D Day of Week | DD Day Of Month | DDD Day Of Year + + DEBUG_PRINT("cset "); + DEBUG_PRINTLN(cronixieDisplay); + + for (int i = 0; i < 6; i++) + { + dP[i] = 10; + switch (cronixieDisplay[i]) + { + case '_': dP[i] = 10; break; + case '-': dP[i] = 11; break; + case 'r': dP[i] = random(1,7); break; //random btw. 1-6 + case 'R': dP[i] = random(0,10); break; //random btw. 0-9 + //case 't': break; //Test upw. + //case 'T': break; //Test dnw. + case 'b': dP[i] = 14 + getSameCodeLength('b',i,cronixieDisplay); i = i+dP[i]-14; break; + case 'B': dP[i] = 14 + getSameCodeLength('B',i,cronixieDisplay); i = i+dP[i]-14; break; + case 'h': dP[i] = 70 + getSameCodeLength('h',i,cronixieDisplay); i = i+dP[i]-70; break; + case 'H': dP[i] = 20 + getSameCodeLength('H',i,cronixieDisplay); i = i+dP[i]-20; break; + case 'A': dP[i] = 108; i++; break; + case 'a': dP[i] = 58; i++; break; + case 'm': dP[i] = 74 + getSameCodeLength('m',i,cronixieDisplay); i = i+dP[i]-74; break; + case 'M': dP[i] = 24 + getSameCodeLength('M',i,cronixieDisplay); i = i+dP[i]-24; break; + case 's': dP[i] = 80 + getSameCodeLength('s',i,cronixieDisplay); i = i+dP[i]-80; break; //refresh more often bc. of secs + case 'S': dP[i] = 30 + getSameCodeLength('S',i,cronixieDisplay); i = i+dP[i]-30; break; + case 'Y': dP[i] = 36 + getSameCodeLength('Y',i,cronixieDisplay); i = i+dP[i]-36; break; + case 'y': dP[i] = 86 + getSameCodeLength('y',i,cronixieDisplay); i = i+dP[i]-86; break; + case 'I': dP[i] = 39 + getSameCodeLength('I',i,cronixieDisplay); i = i+dP[i]-39; break; //Month. Don't ask me why month and minute both start with M. + case 'i': dP[i] = 89 + getSameCodeLength('i',i,cronixieDisplay); i = i+dP[i]-89; break; + //case 'W': break; + //case 'w': break; + case 'D': dP[i] = 43 + getSameCodeLength('D',i,cronixieDisplay); i = i+dP[i]-43; break; + case 'd': dP[i] = 93 + getSameCodeLength('d',i,cronixieDisplay); i = i+dP[i]-93; break; + case '0': dP[i] = 0; break; + case '1': dP[i] = 1; break; + case '2': dP[i] = 2; break; + case '3': dP[i] = 3; break; + case '4': dP[i] = 4; break; + case '5': dP[i] = 5; break; + case '6': dP[i] = 6; break; + case '7': dP[i] = 7; break; + case '8': dP[i] = 8; break; + case '9': dP[i] = 9; break; + //case 'V': break; //user var0 + //case 'v': break; //user var1 + } + } + DEBUG_PRINT("result "); + for (int i = 0; i < 5; i++) + { + DEBUG_PRINT((int)dP[i]); + DEBUG_PRINT(" "); + } + DEBUG_PRINTLN((int)dP[5]); + + _overlayCronixie(); // refresh + } + + void _overlayCronixie() + { + byte h = hour(localTime); + byte h0 = h; + byte m = minute(localTime); + byte s = second(localTime); + byte d = day(localTime); + byte mi = month(localTime); + int y = year(localTime); + //this has to be changed in time for 22nd century + y -= 2000; if (y<0) y += 30; //makes countdown work + + if (useAMPM && !countdownMode) + { + if (h>12) h-=12; + else if (h==0) h+=12; + } + for (int i = 0; i < 6; i++) + { + if (dP[i] < 12) _digitOut[i] = dP[i]; + else { + if (dP[i] < 65) + { + switch(dP[i]) + { + case 21: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; i++; break; //HH + case 25: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; i++; break; //MM + case 31: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; i++; break; //SS + + case 20: _digitOut[i] = h- (h/10)*10; break; //H + case 24: _digitOut[i] = m/10; break; //M + case 30: _digitOut[i] = s/10; break; //S + + case 43: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //D + case 44: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; i++; break; //DD + case 40: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; i++; break; //II + case 37: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //YY + case 39: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //YYYY + + //case 16: _digitOut[i+2] = ((h0/3)&1)?1:0; i++; //BBB (BBBB NI) + //case 15: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:0; i++; //BB + case 14: _digitOut[i] = (h0>11)?1:0; break; //B + } + } else + { + switch(dP[i]) + { + case 71: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //hh + case 75: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //mm + case 81: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ss + //case 66: _digitOut[i+2] = ((h0/3)&1)?1:10; i++; //bbb (bbbb NI) + //case 65: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:10; i++; //bb + case 64: _digitOut[i] = (h0>11)?1:10; break; //b + + case 93: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //d + case 94: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //dd + case 90: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ii + case 87: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //yy + case 89: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //yyyy + } + } + } + } + } + + void handleOverlayDraw() + { + byte offsets[] = {5, 0, 6, 1, 7, 2, 8, 3, 9, 4}; + + for (uint16_t i = 0; i < 6; i++) + { + byte o = 10*i; + byte excl = 10; + if(_digitOut[i] < 10) excl = offsets[_digitOut[i]]; + excl += o; + + if (backlight && _digitOut[i] <11) + { + 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); + } + } else + { + for (uint16_t j=o; j< o+10; j++) { + if (j != excl) strip.setPixelColor(j, 0); + } + } + } + } + + void addToJsonState(JsonObject& root) + { + root["nx"] = cronixieDisplay; + } + + void readFromJsonState(JsonObject& root) + { + if (root["nx"].is()) { + strncpy(cronixieDisplay, root["nx"], 6); + setCronixie(); + } + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(F("Cronixie")); + top["backlight"] = backlight; + } + + 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("Cronixie")]; + + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top["backlight"], backlight); + + return configComplete; + } + + uint16_t getId() + { + return USERMOD_ID_CRONIXIE; + } +}; \ No newline at end of file diff --git a/usermods/DHT/platformio_override.ini b/usermods/DHT/platformio_override.ini new file mode 100644 index 00000000..d192f043 --- /dev/null +++ b/usermods/DHT/platformio_override.ini @@ -0,0 +1,23 @@ +; Options +; ------- +; USERMOD_DHT - define this to have this user mod included 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_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:d1_mini.lib_deps} + https://github.com/alwynallan/DHT_nonblocking + +[env:custom32_LEDPIN_16_usermod_dht_C] +extends = env:custom32_LEDPIN_16 +build_flags = ${env:custom32_LEDPIN_16.build_flags} -D USERMOD_DHT -D USERMOD_DHT_CELSIUS -D USERMOD_DHT_STATS +lib_deps = ${env.lib_deps} + https://github.com/alwynallan/DHT_nonblocking + diff --git a/usermods/DHT/readme.md b/usermods/DHT/readme.md new file mode 100644 index 00000000..6089ffbf --- /dev/null +++ b/usermods/DHT/readme.md @@ -0,0 +1,48 @@ +# 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 (and optionally sent to an MQTT broker). + +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 + +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_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 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 + +* [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link + +### 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:d1_mini_usermod_dht_C`. If not, you can add the libraries and dependencies into `platformio.ini` as you see fit. + + +## 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 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 +* Created diff --git a/usermods/DHT/usermod_dht.h b/usermods/DHT/usermod_dht.h new file mode 100644 index 00000000..b6142f43 --- /dev/null +++ b/usermods/DHT/usermod_dht.h @@ -0,0 +1,247 @@ +#pragma once + +#include "wled.h" +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + + +#include + +// USERMOD_DHT_DHTTYPE: +// 11 // DHT 11 +// 21 // DHT 21 +// 22 // DHT 22 (AM2302), AM2321 *** default +#ifndef USERMOD_DHT_DHTTYPE +#define USERMOD_DHT_DHTTYPE 22 +#endif + +#if USERMOD_DHT_DHTTYPE == 11 +#define DHTTYPE DHT_TYPE_11 +#elif USERMOD_DHT_DHTTYPE == 21 +#define DHTTYPE DHT_TYPE_21 +#elif USERMOD_DHT_DHTTYPE == 22 +#define DHTTYPE DHT_TYPE_22 +#endif + +// Connect pin 1 (on the left) of the sensor to +5V +// NOTE: If using a board with 3.3V logic like an Arduino Due connect pin 1 +// to 3.3V instead of 5V! +// Connect pin 2 of the sensor to whatever your DHTPIN is +// NOTE: Pin defaults below are for QuinLed Dig-Uno's Q2 on the board +// Connect pin 4 (on the right) of the sensor to GROUND +// NOTE: If using a bare sensor (AM*), Connect a 10K resistor from pin 2 +// (data) to pin 1 (power) of the sensor. DHT* boards have the pullup already + +#ifdef USERMOD_DHT_PIN +#define DHTPIN USERMOD_DHT_PIN +#else +#ifdef ARDUINO_ARCH_ESP32 +#define DHTPIN 21 +#else //ESP8266 boards +#define DHTPIN 4 +#endif +#endif + +// the frequency to check sensor, 1 minute +#ifndef USERMOD_DHT_MEASUREMENT_INTERVAL +#define USERMOD_DHT_MEASUREMENT_INTERVAL 60000 +#endif + +// how many seconds after boot to take first measurement, 90 seconds +// 90 gives enough time to OTA update firmware if this crashses +#ifndef USERMOD_DHT_FIRST_MEASUREMENT_AT +#define USERMOD_DHT_FIRST_MEASUREMENT_AT 90000 +#endif + +// from COOLDOWN_TIME in dht_nonblocking.cpp +#define DHT_TIMEOUT_TIME 10000 + +DHT_nonblocking dht_sensor(DHTPIN, DHTTYPE); + +class UsermodDHT : public Usermod { + private: + unsigned long nextReadTime = 0; + unsigned long lastReadTime = 0; + 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; + uint16_t clean_updates = 0; + uint16_t errors = 0; + unsigned long maxDelay = 0; + unsigned long currentIteration = 0; + unsigned long maxIteration = 0; + #endif + + public: + 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 + } + + void loop() { + if (disabled) { + return; + } + if (millis() < nextReadTime) { + return; + } + + #ifdef USERMOD_DHT_STATS + if (millis() >= nextResetStatsTime) { + nextResetStatsTime += 60*60*1000; + errors = 0; + updates = 0; + clean_updates = 0; + } + unsigned long dcalc = millis(); + if (currentIteration == 0) { + currentIteration = millis(); + } + #endif + + float tempC; + if (dht_sensor.measure(&tempC, &humidity)) { + #ifdef USERMOD_DHT_CELSIUS + temperature = tempC; + #else + 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) { + maxIteration = icalc; + } + if (icalc > DHT_TIMEOUT_TIME) { + errors += icalc/DHT_TIMEOUT_TIME; + } else { + clean_updates += 1; + } + updates += 1; + currentIteration = 0; + + #endif + } + + #ifdef USERMOD_DHT_STATS + dcalc = millis() - dcalc; + if (dcalc > maxDelay) { + maxDelay = dcalc; + } + #endif + + if (((millis() - lastReadTime) > 10*USERMOD_DHT_MEASUREMENT_INTERVAL)) { + disabled = true; + } + } + + void addToJsonInfo(JsonObject& root) { + if (disabled) { + return; + } + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray temp = user.createNestedArray("Temperature"); + JsonArray hum = user.createNestedArray("Humidity"); + + #ifdef USERMOD_DHT_STATS + JsonArray next = user.createNestedArray("next"); + if (nextReadTime >= millis()) { + next.add((nextReadTime - millis()) / 1000); + next.add(" sec until read"); + } else { + next.add((millis() - nextReadTime) / 1000); + next.add(" sec active reading"); + } + + JsonArray last = user.createNestedArray("last"); + last.add((millis() - lastReadTime) / 60000); + last.add(" min since read"); + + JsonArray err = user.createNestedArray("errors"); + err.add(errors); + err.add(" Errors"); + + JsonArray upd = user.createNestedArray("updates"); + upd.add(updates); + upd.add(" Updates"); + + JsonArray cupd = user.createNestedArray("cleanUpdates"); + cupd.add(clean_updates); + cupd.add(" Updates"); + + JsonArray iter = user.createNestedArray("maxIter"); + iter.add(maxIteration); + iter.add(" ms"); + + JsonArray delay = user.createNestedArray("maxDelay"); + delay.add(maxDelay); + delay.add(" ms"); + #endif + + if (initializing) { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + temp.add((nextReadTime - millis()) / 1000); + temp.add(" sec until read"); + hum.add((nextReadTime - millis()) / 1000); + hum.add(" sec until read"); + return; + } + + hum.add(humidity); + hum.add("%"); + + temp.add(temperature); + #ifdef USERMOD_DHT_CELSIUS + temp.add("°C"); + #else + temp.add("°F"); + #endif + } + + uint16_t getId() + { + return USERMOD_ID_DHT; + } + +}; diff --git a/usermods/EXAMPLE_v2/readme.md b/usermods/EXAMPLE_v2/readme.md index 09a8e553..8917a1fb 100644 --- a/usermods/EXAMPLE_v2/readme.md +++ b/usermods/EXAMPLE_v2/readme.md @@ -5,6 +5,6 @@ In this usermod file you can find the documentation on how to take advantage of ## Installation Copy `usermod_v2_example.h` to the wled00 directory. -Uncomment the corresponding lines in `usermods_list.h` and compile! +Uncomment the corresponding lines in `usermods_list.cpp` and compile! _(You shouldn't need to actually install this, it does nothing useful)_ diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index e67ef8da..43648b58 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -22,18 +22,75 @@ //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) + bool testBool = false; + unsigned long testULong = 42424242; + float testFloat = 42.42; + String testString = "Forty-Two"; + + // These config variables have defaults set inside readFromConfig() + int testInt; + 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 methosd 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; } @@ -57,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(); @@ -69,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")); } - */ /* @@ -90,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; } @@ -100,11 +173,204 @@ 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!")); } - - + + + /* + * 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; + //save these vars persistently whenever settings are saved + top["great"] = userVar0; + top["testBool"] = testBool; + top["testInt"] = testInt; + top["testLong"] = testLong; + top["testULong"] = testULong; + top["testFloat"] = testFloat; + top["testString"] = testString; + JsonArray pinArray = top.createNestedArray("pin"); + pinArray.add(testPins[0]); + pinArray.add(testPins[1]); + } + + + /* + * 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[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top["great"], userVar0); + configComplete &= getJsonValue(top["testBool"], testBool); + configComplete &= getJsonValue(top["testULong"], testULong); + configComplete &= getJsonValue(top["testFloat"], testFloat); + configComplete &= getJsonValue(top["testString"], testString); + + // 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. @@ -116,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/ChipSelect.h b/usermods/EleksTube_IPS/ChipSelect.h new file mode 100644 index 00000000..ced5eb8e --- /dev/null +++ b/usermods/EleksTube_IPS/ChipSelect.h @@ -0,0 +1,70 @@ +#ifndef CHIP_SELECT_H +#define CHIP_SELECT_H + +#include "Hardware.h" + +/* + * `digit`s are as defined in Hardware.h, 0 == seconds ones, 5 == hours tens. + */ + +class ChipSelect { +private: + uint8_t digits_map; + const uint8_t all_on = 0x3F; + const uint8_t all_off = 0x00; +public: + ChipSelect() : digits_map(all_off) {} + + void update() { + // Documented in README.md. Q7 and Q6 are unused. Q5 is Seconds Ones, Q0 is Hours Tens. + // Q7 is the first bit written, Q0 is the last. So we push two dummy bits, then start with + // Seconds Ones and end with Hours Tens. + // CS is Active Low, but digits_map is 1 for enable, 0 for disable. So we bit-wise NOT first. + + uint8_t to_shift = (~digits_map) << 2; + + digitalWrite(CSSR_LATCH_PIN, LOW); + shiftOut(CSSR_DATA_PIN, CSSR_CLOCK_PIN, LSBFIRST, to_shift); + digitalWrite(CSSR_LATCH_PIN, HIGH); + } + + void begin() + { + pinMode(CSSR_LATCH_PIN, OUTPUT); + pinMode(CSSR_DATA_PIN, OUTPUT); + pinMode(CSSR_CLOCK_PIN, OUTPUT); + + digitalWrite(CSSR_DATA_PIN, LOW); + digitalWrite(CSSR_CLOCK_PIN, LOW); + digitalWrite(CSSR_LATCH_PIN, LOW); + update(); + } + + // These speak the indexes defined in Hardware.h. + // So 0 is disabled, 1 is enabled (even though CS is active low, this gets mapped.) + // So bit 0 (LSB), is index 0, is SECONDS_ONES + // Translation to what the 74HC595 uses is done in update() + void setDigitMap(uint8_t map, bool update_=true) { digits_map = map; if (update_) update(); } + uint8_t getDigitMap() { return digits_map; } + + // Helper functions + // Sets just the one digit by digit number + void setDigit(uint8_t digit, bool update_=true) { setDigitMap(0x01 << digit, update_); } + void setAll(bool update_=true) { setDigitMap(all_on, update_); } + void clear(bool update_=true) { setDigitMap(all_off, update_); } + void setSecondsOnes() { setDigit(SECONDS_ONES); } + void setSecondsTens() { setDigit(SECONDS_TENS); } + void setMinutesOnes() { setDigit(MINUTES_ONES); } + void setMinutesTens() { setDigit(MINUTES_TENS); } + void setHoursOnes() { setDigit(HOURS_ONES); } + void setHoursTens() { setDigit(HOURS_TENS); } + bool isSecondsOnes() { return ((digits_map & SECONDS_ONES_MAP) > 0); } + bool isSecondsTens() { return ((digits_map & SECONDS_TENS_MAP) > 0); } + bool isMinutesOnes() { return ((digits_map & MINUTES_ONES_MAP) > 0); } + bool isMinutesTens() { return ((digits_map & MINUTES_TENS_MAP) > 0); } + bool isHoursOnes() { return ((digits_map & HOURS_ONES_MAP) > 0); } + bool isHoursTens() { return ((digits_map & HOURS_TENS_MAP) > 0); } +}; + + +#endif // CHIP_SELECT_H diff --git a/usermods/EleksTube_IPS/Hardware.h b/usermods/EleksTube_IPS/Hardware.h new file mode 100644 index 00000000..e4f79305 --- /dev/null +++ b/usermods/EleksTube_IPS/Hardware.h @@ -0,0 +1,52 @@ +/* + * Define the hardware for the EleksTube IPS clock. Mostly pin definitions + */ +#ifndef ELEKSTUBEHAX_HARDWARE_H +#define ELEKSTUBEHAX_HARDWARE_H + +#include +#include // for HIGH and LOW + +// Common indexing scheme, used to identify the digit +#define SECONDS_ONES (0) +#define SECONDS_TENS (1) +#define MINUTES_ONES (2) +#define MINUTES_TENS (3) +#define HOURS_ONES (4) +#define HOURS_TENS (5) +#define NUM_DIGITS (6) + +#define SECONDS_ONES_MAP (0x01 << SECONDS_ONES) +#define SECONDS_TENS_MAP (0x01 << SECONDS_TENS) +#define MINUTES_ONES_MAP (0x01 << MINUTES_ONES) +#define MINUTES_TENS_MAP (0x01 << MINUTES_TENS) +#define HOURS_ONES_MAP (0x01 << HOURS_ONES) +#define HOURS_TENS_MAP (0x01 << HOURS_TENS) + +// WS2812 (or compatible) LEDs on the back of the display modules. +#define BACKLIGHTS_PIN (12) + +// Buttons, active low, externally pulled up (with actual resistors!) +#define BUTTON_LEFT_PIN (33) +#define BUTTON_MODE_PIN (32) +#define BUTTON_RIGHT_PIN (35) +#define BUTTON_POWER_PIN (34) + +// I2C to DS3231 RTC. +#define RTC_SCL_PIN (22) +#define RTC_SDA_PIN (21) + +// Chip Select shift register, to select the display +#define CSSR_DATA_PIN (14) +#define CSSR_CLOCK_PIN (16) +#define CSSR_LATCH_PIN (17) + +// SPI to displays +// DEFINED IN User_Setup.h +// Look for: TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, and TFT_RST + +// Power for all TFT displays are grounded through a MOSFET so they can all be turned off. +// Active HIGH. +#define TFT_ENABLE_PIN (27) + +#endif // ELEKSTUBEHAX_HARDWARE_H diff --git a/usermods/EleksTube_IPS/TFTs.h b/usermods/EleksTube_IPS/TFTs.h new file mode 100644 index 00000000..030ec23a --- /dev/null +++ b/usermods/EleksTube_IPS/TFTs.h @@ -0,0 +1,379 @@ +#ifndef TFTS_H +#define TFTS_H + +#include "wled.h" +#include + +#include +#include "Hardware.h" +#include "ChipSelect.h" + +class TFTs : public TFT_eSPI { +private: + uint8_t digits[NUM_DIGITS]; + + + // These read 16- and 32-bit types from the SD card file. + // BMP data is stored little-endian, Arduino is little-endian too. + // May need to reverse subscript order if porting elsewhere. + + uint16_t read16(fs::File &f) { + uint16_t result; + ((uint8_t *)&result)[0] = f.read(); // LSB + ((uint8_t *)&result)[1] = f.read(); // MSB + return result; + } + + uint32_t read32(fs::File &f) { + uint32_t result; + ((uint8_t *)&result)[0] = f.read(); // LSB + ((uint8_t *)&result)[1] = f.read(); + ((uint8_t *)&result)[2] = f.read(); + ((uint8_t *)&result)[3] = f.read(); // MSB + return result; + } + + uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH]; + int16_t w = 135, h = 240, x = 0, y = 0, bufferedDigit = 255; + uint16_t digitR, digitG, digitB, dimming = 255; + uint32_t digitColor = 0; + + void drawBuffer() { + bool oldSwapBytes = getSwapBytes(); + setSwapBytes(true); + pushImage(x, y, w, h, (uint16_t *)output_buffer); + setSwapBytes(oldSwapBytes); + } + + // These BMP functions are stolen directly from the TFT_SPIFFS_BMP example in the TFT_eSPI library. + // Unfortunately, they aren't part of the library itself, so I had to copy them. + // I've modified drawBmp to buffer the whole image at once instead of doing it line-by-line. + + //// BEGIN STOLEN CODE + + // Draw directly from file stored in RGB565 format. Fastest + bool drawBin(const char *filename) { + fs::File bmpFS; + + // Open requested file on SD card + bmpFS = WLED_FS.open(filename, "r"); + + size_t sz = bmpFS.size(); + if (sz > 64800) { + bmpFS.close(); + return false; + } + + uint16_t r, g, b, dimming = 255; + int16_t row, col; + + //draw img that is shorter than 240pix into the center + w = 135; + h = sz / (w * 2); + x = 0; + y = (height() - h) /2; + + uint8_t lineBuffer[w * 2]; + + if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) strip.service(); + + // 0,0 coordinates are top left + for (row = 0; row < h; row++) { + + bmpFS.read(lineBuffer, sizeof(lineBuffer)); + uint8_t PixM, PixL; + + // Colors are already in 16-bit R5, G6, B5 format + for (col = 0; col < w; col++) + { + if (dimming == 255 && !digitColor) { // not needed, copy directly + output_buffer[row][col] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]); + } else { + // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB + PixM = lineBuffer[col*2+1]; + PixL = lineBuffer[col*2]; + // align to 8-bit value (MSB left aligned) + r = (PixM) & 0xF8; + g = ((PixM << 5) | (PixL >> 3)) & 0xFC; + b = (PixL << 3) & 0xF8; + r *= dimming; g *= dimming; b *= dimming; + r = r >> 8; g = g >> 8; b = b >> 8; + if (digitColor) { // grayscale pixel coloring + uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b); + r = g = b = l; + r *= digitR; g *= digitG; b *= digitB; + r = r >> 8; g = g >> 8; b = b >> 8; + } + output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); + } + } + } + + drawBuffer(); + + bmpFS.close(); + + return true; + } + + bool drawBmp(const char *filename) { + fs::File bmpFS; + + // Open requested file on SD card + bmpFS = WLED_FS.open(filename, "r"); + + uint32_t seekOffset, headerSize, paletteSize = 0; + int16_t row; + uint16_t r, g, b, dimming = 255, bitDepth; + + uint16_t magic = read16(bmpFS); + if (magic != ('B' | ('M' << 8))) { // File not found or not a BMP + Serial.println(F("BMP not found!")); + bmpFS.close(); + return false; + } + + (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 + (void) read16(bmpFS); // color planes (must be 1) + bitDepth = read16(bmpFS); + + if (read32(bmpFS) != 0 || (bitDepth != 24 && bitDepth != 1 && bitDepth != 4 && bitDepth != 8)) { + Serial.println(F("BMP format not recognized.")); + bmpFS.close(); + return false; + } + + uint32_t palette[256]; + if (bitDepth <= 8) // 1,4,8 bit bitmap: read color palette + { + (void) read32(bmpFS); (void) read32(bmpFS); (void) read32(bmpFS); // size, w resolution, h resolution + paletteSize = read32(bmpFS); + 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); + } + } + + // draw img that is shorter than 240pix into the center + x = (width() - w) /2; + y = (height() - h) /2; + + bmpFS.seek(seekOffset); + + uint32_t lineSize = ((bitDepth * w +31) >> 5) * 4; + uint8_t lineBuffer[lineSize]; + + 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 + bmpFS.read(lineBuffer, sizeof(lineBuffer)); + uint8_t* bptr = lineBuffer; + + // Convert 24 to 16 bit colors while copying to output buffer. + for (uint16_t col = 0; col < w; col++) + { + if (bitDepth == 24) { + b = *bptr++; + g = *bptr++; + r = *bptr++; + } else { + uint32_t c = 0; + if (bitDepth == 8) { + c = palette[*bptr++]; + } + else if (bitDepth == 4) { + c = palette[(*bptr >> ((col & 0x01)?0:4)) & 0x0F]; + if (col & 0x01) bptr++; + } + else { // bitDepth == 1 + c = palette[(*bptr >> (7 - (col & 0x07))) & 0x01]; + if ((col & 0x07) == 0x07) bptr++; + } + b = c; g = c >> 8; r = c >> 16; + } + if (dimming != 255) { // only dim when needed + r *= dimming; g *= dimming; b *= dimming; + r = r >> 8; g = g >> 8; b = b >> 8; + } + if (digitColor) { // grayscale pixel coloring + uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b); + r = g = b = l; + + r *= digitR; g *= digitG; b *= digitB; + r = r >> 8; g = g >> 8; b = b >> 8; + } + output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xFF) >> 3); + } + } + + drawBuffer(); + + bmpFS.close(); + return true; + } + + bool drawClk(const char *filename) { + fs::File bmpFS; + + // Open requested file on SD card + bmpFS = WLED_FS.open(filename, "r"); + + if (!bmpFS) + { + Serial.print("File not found: "); + Serial.println(filename); + return false; + } + + uint16_t r, g, b, dimming = 255, magic; + int16_t row, col; + + magic = read16(bmpFS); + if (magic != 0x4B43) { // look for "CK" header + Serial.print(F("File not a CLK. Magic: ")); + Serial.println(magic); + bmpFS.close(); + return false; + } + + w = read16(bmpFS); + h = read16(bmpFS); + x = (width() - w) / 2; + y = (height() - h) / 2; + + uint8_t lineBuffer[w * 2]; + + if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) strip.service(); + + // 0,0 coordinates are top left + for (row = 0; row < h; row++) { + + bmpFS.read(lineBuffer, sizeof(lineBuffer)); + uint8_t PixM, PixL; + + // Colors are already in 16-bit R5, G6, B5 format + for (col = 0; col < w; col++) + { + if (dimming == 255 && !digitColor) { // not needed, copy directly + output_buffer[row][col+x] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]); + } else { + // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB + PixM = lineBuffer[col*2+1]; + PixL = lineBuffer[col*2]; + // align to 8-bit value (MSB left aligned) + r = (PixM) & 0xF8; + g = ((PixM << 5) | (PixL >> 3)) & 0xFC; + b = (PixL << 3) & 0xF8; + r *= dimming; g *= dimming; b *= dimming; + r = r >> 8; g = g >> 8; b = b >> 8; + if (digitColor) { // grayscale pixel coloring + uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b); + r = g = b = l; + r *= digitR; g *= digitG; b *= digitB; + r = r >> 8; g = g >> 8; b = b >> 8; + } + output_buffer[row][col+x] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); + } + } + } + + drawBuffer(); + + bmpFS.close(); + return true; + } + + +public: + TFTs() : TFT_eSPI(), chip_select() + { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) digits[digit] = 0; } + + // no == Do not send to TFT. yes == Send to TFT if changed. force == Send to TFT. + enum show_t { no, yes, force }; + // A digit of 0xFF means blank the screen. + const static uint8_t blanked = 255; + + uint8_t tubeSegment = 1; + uint8_t digitOffset = 0; + + void begin() { + pinMode(TFT_ENABLE_PIN, OUTPUT); + digitalWrite(TFT_ENABLE_PIN, HIGH); //enable displays on boot + + // Start with all displays selected. + chip_select.begin(); + chip_select.setAll(); + + // Initialize the super class. + init(); + } + + void showDigit(uint8_t digit) { + chip_select.setDigit(digit); + uint8_t digitToDraw = digits[digit]; + if (digitToDraw < 10) digitToDraw += digitOffset; + + if (digitToDraw == blanked) { + fillScreen(TFT_BLACK); return; + } + + // if last digit was the same, skip loading from FS to buffer + if (!digitColor && digitToDraw == bufferedDigit) drawBuffer(); + digitR = R(digitColor); digitG = G(digitColor); digitB = B(digitColor); + + // Filenames are no bigger than "254.bmp\0" + char file_name[10]; + // Fastest, raw RGB565 + sprintf(file_name, "/%d.bin", digitToDraw); + if (WLED_FS.exists(file_name)) { + if (drawBin(file_name)) bufferedDigit = digitToDraw; + return; + } + // Fast, raw RGB565, see https://github.com/aly-fly/EleksTubeHAX on how to create this clk format + sprintf(file_name, "/%d.clk", digitToDraw); + if (WLED_FS.exists(file_name)) { + if (drawClk(file_name)) bufferedDigit = digitToDraw; + return; + } + // Slow, regular RGB888 or 1,4,8 bit palette BMP + sprintf(file_name, "/%d.bmp", digitToDraw); + if (drawBmp(file_name)) bufferedDigit = digitToDraw; + return; + } + + void setDigit(uint8_t digit, uint8_t value, show_t show=yes) { + uint8_t old_value = digits[digit]; + digits[digit] = value; + + // 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 + Segment& seg1 = strip.getSegment(tubeSegment); + if (seg1.isActive()) { + digitColor = strip.getPixelColor(seg1.start + digit); + dimming = seg1.opacity; + } else { + digitColor = 0; + dimming = 255; + } + + if (show != no && (old_value != value || show == force)) { + showDigit(digit); + } + } + uint8_t getDigit(uint8_t digit) {return digits[digit];} + + void showAllDigits() {for (uint8_t digit=0; digit < NUM_DIGITS; digit++) showDigit(digit);} + + // Making chip_select public so we don't have to proxy all methods, and the caller can just use it directly. + ChipSelect chip_select; +}; + +#endif // TFTS_H diff --git a/usermods/EleksTube_IPS/User_Setup.h b/usermods/EleksTube_IPS/User_Setup.h new file mode 100644 index 00000000..b4b2edab --- /dev/null +++ b/usermods/EleksTube_IPS/User_Setup.h @@ -0,0 +1,47 @@ +/* + * This is intended to over-ride `User_Setup.h` that comes with the TFT_eSPI library. + * I hate having to modify the library code. + */ + +// ST7789 135 x 240 display with no chip select line + +#define ST7789_DRIVER // Configure all registers + +#define TFT_WIDTH 135 +#define TFT_HEIGHT 240 + +#define CGRAM_OFFSET // Library will add offsets required + +//#define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue +//#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red + +//#define TFT_INVERSION_ON +//#define TFT_INVERSION_OFF + +// EleksTube IPS +#define TFT_SDA_READ // Read and write on the MOSI/SDA pin, no separate MISO pin +#define TFT_MOSI 23 +#define TFT_SCLK 18 +//#define TFT_CS -1 // Not connected +#define TFT_DC 25 // Data Command, aka Register Select or RS +#define TFT_RST 26 // Connect reset to ensure display initialises + +#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH +//#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters +//#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters +//#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm +//#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:. +//#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. +//#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT +//#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts + +//#define SMOOTH_FONT + + +//#define SPI_FREQUENCY 27000000 +#define SPI_FREQUENCY 40000000 + +/* + * To make the Library not over-write all this: + */ +#define USER_SETUP_LOADED diff --git a/usermods/EleksTube_IPS/readme.md b/usermods/EleksTube_IPS/readme.md new file mode 100644 index 00000000..a05c9346 --- /dev/null +++ b/usermods/EleksTube_IPS/readme.md @@ -0,0 +1,45 @@ +# EleksTube IPS Clock usermod + +This usermod allows WLED to run on the EleksTube IPS clock. +It enables running all WLED effects on the background SK6812 lighting, while displaying digit bitmaps on the 6 IPS screens. +Code is largely based on https://github.com/SmittyHalibut/EleksTubeHAX by Mark Smith! + +Supported: +- Display with custom bitmaps (.bmp) or raw RGB565 images (.bin) from filesystem +- Background lighting +- All 4 hardware buttons +- RTC (with RTC usermod) +- Standard WLED time features (NTP, DST, timezones) + +Not supported: +- On-device setup with buttons (WiFi setup only) + +Your images must be 1-135 pixels wide and 1-240 pixels high. +BMP 1, 4, 8, and 24 bits per pixel formats are supported. + +## Installation + +Compile and upload to clock using the `elekstube_ips` PlatformIO environment +Once uploaded (the clock can be flashed like any ESP32 module), go to `[WLED-IP]/edit` and upload the 0-9.bin files from [here](https://github.com/Aircoookie/NixieThemes/tree/master/themes/RealisticNixie/bin). +You can find more clockfaces in the [NixieThemes](https://github.com/Aircoookie/NixieThemes/) repo. +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 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 format will reduce file size even more: + +| Bits per pixel | File size in kB (for 135x240 img) | % of 24 BPP BMP | Max unique colors +| --- | --- | --- | --- | +24 | 98 | 100% | 16M (66K) +16 (.clk) | 64.8 | 66% | 66K +8 | 33.7 | 34% | 256 +4 | 16.4 | 17% | 16 +1 | 4.9 | 5% | 2 + +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) diff --git a/usermods/EleksTube_IPS/usermod_elekstube_ips.h b/usermods/EleksTube_IPS/usermod_elekstube_ips.h new file mode 100644 index 00000000..0f7d92e7 --- /dev/null +++ b/usermods/EleksTube_IPS/usermod_elekstube_ips.h @@ -0,0 +1,158 @@ +#pragma once +#include "TFTs.h" +#include "wled.h" + +//Large parts of the code are from https://github.com/SmittyHalibut/EleksTubeHAX + +class ElekstubeIPSUsermod : public Usermod { + private: + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _tubeSeg[]; + static const char _digitOffset[]; + + char cronixieDisplay[7] = "HHMMSS"; + + TFTs tfts; + void updateClockDisplay(TFTs::show_t show=TFTs::yes) { + bool set[6] = {false}; + for (uint8_t i = 0; i<6; i++) { + char c = cronixieDisplay[i]; + if (c >= '0' && c <= '9') { + tfts.setDigit(5-i, c - '0', show); set[i] = true; + } else if (c >= 'A' && c <= 'G') { + tfts.setDigit(5-i, c - 'A' + 10, show); set[i] = true; //10.bmp to 16.bmp static display + } else if (c == '-' || c == '_' || c == ' ') { + tfts.setDigit(5-i, 255, show); set[i] = true; //blank + } else { + set[i] = false; //display HHMMSS time + } + } + + + uint8_t hr = hour(localTime); + uint8_t hrTens = hr/10; + uint8_t mi = minute(localTime); + uint8_t mittens = mi/10; + uint8_t s = second(localTime); + uint8_t sTens = s/10; + if (!set[0]) tfts.setDigit(HOURS_TENS, hrTens, show); + if (!set[1]) tfts.setDigit(HOURS_ONES, hr - hrTens*10, show); + if (!set[2]) tfts.setDigit(MINUTES_TENS, mittens, show); + if (!set[3]) tfts.setDigit(MINUTES_ONES, mi - mittens*10, show); + if (!set[4]) tfts.setDigit(SECONDS_TENS, sTens, show); + if (!set[5]) tfts.setDigit(SECONDS_ONES, s - sTens*10, show); + } + unsigned long lastTime = 0; + public: + + uint8_t lastBri; + uint32_t lastCols[6]; + TFTs::show_t fshow=TFTs::yes; + + void setup() { + tfts.begin(); + tfts.fillScreen(TFT_BLACK); + + for (int8_t i = 5; i >= 0; i--) { + tfts.setDigit(i, 255, TFTs::force); //turn all off + } + } + + void loop() { + if (!toki.isTick()) return; + updateLocalTime(); + + Segment& seg1 = strip.getSegment(tfts.tubeSegment); + if (seg1.isActive()) { + bool update = false; + if (seg1.opacity != lastBri) update = true; + lastBri = seg1.opacity; + for (uint8_t i = 0; i < 6; i++) { + uint32_t c = strip.getPixelColor(seg1.start + i); + if (c != lastCols[i]) update = true; + lastCols[i] = c; + } + if (update) fshow=TFTs::force; + } else if (lastCols[0] != 0) { // Segment 1 deleted + fshow=TFTs::force; + lastCols[0] = 0; + } + + updateClockDisplay(fshow); + fshow=TFTs::yes; + } + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void addToConfig(JsonObject &root) { + // we add JSON object: {"EleksTubeIPS": {"tubeSegment": 1, "digitOffset": 0}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_tubeSeg)] = tfts.tubeSegment; + top[FPSTR(_digitOffset)] = tfts.digitOffset; + DEBUG_PRINTLN(F("EleksTube 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: {"EleksTubeIPS": {"tubeSegment": 1, "digitOffset": 0}} + DEBUG_PRINT(FPSTR(_name)); + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + tfts.tubeSegment = top[FPSTR(_tubeSeg)] | tfts.tubeSegment; + uint8_t digitOffsetPrev = tfts.digitOffset; + tfts.digitOffset = top[FPSTR(_digitOffset)] | tfts.digitOffset; + if (tfts.digitOffset > 240) tfts.digitOffset = 240; + if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force; + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_digitOffset)].isNull(); + } + + /* + * 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["nx"] = cronixieDisplay; + root[FPSTR(_digitOffset)] = tfts.digitOffset; + } + + + /* + * 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 (root["nx"].is()) { + strncpy(cronixieDisplay, root["nx"], 6); + } + + uint8_t digitOffsetPrev = tfts.digitOffset; + tfts.digitOffset = root[FPSTR(_digitOffset)] | tfts.digitOffset; + if (tfts.digitOffset > 240) tfts.digitOffset = 240; + if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force; + } + + uint16_t getId() + { + return USERMOD_ID_ELEKSTUBE_IPS; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char ElekstubeIPSUsermod::_name[] PROGMEM = "EleksTubeIPS"; +const char ElekstubeIPSUsermod::_tubeSeg[] PROGMEM = "tubeSegment"; +const char ElekstubeIPSUsermod::_digitOffset[] PROGMEM = "digitOffset"; diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/assets/controller.jpg b/usermods/Enclosure_with_OLED_temp_ESP07/assets/controller.jpg index d518ca3e..755bfefb 100644 Binary files a/usermods/Enclosure_with_OLED_temp_ESP07/assets/controller.jpg and b/usermods/Enclosure_with_OLED_temp_ESP07/assets/controller.jpg differ diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/assets/pcb.png b/usermods/Enclosure_with_OLED_temp_ESP07/assets/pcb.png index cf146918..a4b81ad0 100644 Binary files a/usermods/Enclosure_with_OLED_temp_ESP07/assets/pcb.png and b/usermods/Enclosure_with_OLED_temp_ESP07/assets/pcb.png differ diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/readme.md b/usermods/Enclosure_with_OLED_temp_ESP07/readme.md index 94d1c1f2..d612e06e 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 9724a1b7..1ca16050 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/ @@ -100,9 +104,9 @@ void userLoop() { needRedraw = true; } else if (knownBrightness != bri) { needRedraw = true; - } else if (knownMode != strip.getMode()) { + } else if (knownMode != strip.getMainSegment().mode) { needRedraw = true; - } else if (knownPalette != strip.getSegment(0).palette) { + } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } @@ -126,8 +130,8 @@ void userLoop() { #endif knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); knownBrightness = bri; - knownMode = strip.getMode(); - knownPalette = strip.getSegment(0).palette; + knownMode = strip.getMainSegment().mode; + knownPalette = strip.getMainSegment().palette; u8x8.clear(); u8x8.setFont(u8x8_font_chroma48medium8_r); @@ -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; + 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/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp b/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp index c39c74d2..d5fd4a0c 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/ @@ -143,9 +147,9 @@ void userLoop() { needRedraw = true; } else if (knownBrightness != bri) { needRedraw = true; - } else if (knownMode != strip.getMode()) { + } else if (knownMode != strip.getMainSegment().mode) { needRedraw = true; - } else if (knownPalette != strip.getSegment(0).palette) { + } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } @@ -169,8 +173,8 @@ void userLoop() { #endif knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); knownBrightness = bri; - knownMode = strip.getMode(); - knownPalette = strip.getSegment(0).palette; + knownMode = strip.getMainSegment().mode; + knownPalette = strip.getMainSegment().palette; u8x8.clear(); u8x8.setFont(u8x8_font_chroma48medium8_r); @@ -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 f7d2aed6..24d5ff5a 100644 --- a/usermods/Fix_unreachable_netservices_v2/readme.md +++ b/usermods/Fix_unreachable_netservices_v2/readme.md @@ -1,16 +1,32 @@ # Fix unreachable net services V2 -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. +**Attention: This usermod compiles only for ESP8266** + +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 WLAN environments. The modification works with static or DHCP IP address configuration. -**Webinterface**: The number of pings and reconnects is displayed on the info page in the web interface. - _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 + +The number of pings and reconnects is displayed on the info page in the web interface. +The ping delay can be changed. Changes persist after a reboot. + +## JSON API + +The usermod supports the following state changes: + +| JSON key | Value range | Description | +|-------------|------------------|---------------------------------| +| PingDelayMs | 5000 to 18000000 | Deactivdate/activate the sensor | + + Changes also persist after a reboot. ## Installation diff --git a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h index 8ffc821e..3d441e59 100644 --- a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h +++ b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h @@ -1,6 +1,14 @@ #pragma once #include "wled.h" +#if defined(ESP32) +#warning "Usermod FixUnreachableNetServices works only with ESP8266 builds" +class FixUnreachableNetServices : public Usermod +{ +}; +#endif + +#if defined(ESP8266) #include /* @@ -23,116 +31,141 @@ * 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 FixUnreachableNetServices : public Usermod { - private: - //Private class members. You can declare variables and functions only accessible to your usermod here - unsigned long m_lastTime = 0; +class FixUnreachableNetServices : public Usermod +{ +private: + //Private class members. You can declare variables and functions only accessible to your usermod here + unsigned long m_lastTime = 0; - // desclare required variables - const unsigned int PingDelayMs = 60000; - unsigned long m_connectedWiFi = 0; - ping_option m_pingOpt; - unsigned int m_pingCount = 0; + // declare required variables + unsigned long m_pingDelayMs = 60000; + unsigned long m_connectedWiFi = 0; + ping_option m_pingOpt; + unsigned int m_pingCount = 0; + bool m_updateConfig = false; - public: - //Functions called by WLED +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() { - //Serial.println("Hello from my usermod!"); - } + /** + * 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() + { + //Serial.println("Hello from my usermod!"); + } + /** + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + //Serial.println("Connected to WiFi!"); - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() { - //Serial.println("Connected to WiFi!"); + ++m_connectedWiFi; - ++m_connectedWiFi; - - // initialize ping_options structure - memset(&m_pingOpt, 0, sizeof(struct ping_option)); - m_pingOpt.count = 1; - m_pingOpt.ip = WiFi.localIP(); + // initialize ping_options structure + memset(&m_pingOpt, 0, sizeof(struct ping_option)); + m_pingOpt.count = 1; + m_pingOpt.ip = WiFi.localIP(); + } - } - - - /* - * 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 (m_connectedWiFi > 0 && millis()-m_lastTime > PingDelayMs) - { - ping_start(&m_pingOpt); - m_lastTime = millis(); - ++m_pingCount; - } - } - - - /* - * 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) + /** + * loop + */ + void loop() + { + if (m_connectedWiFi > 0 && millis() - m_lastTime > m_pingDelayMs) { - //this code adds "u":{"⚡ Ping fix pings": m_pingCount} to the info object - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray("⚡ Ping fix pings"); //name - infoArr.add(m_pingCount); //value - - //this code adds "u":{"⚡ Reconnects": m_connectedWiFi - 1} to the info object - infoArr = user.createNestedArray("⚡ Reconnects"); //name - infoArr.add(m_connectedWiFi - 1); //value + ping_start(&m_pingOpt); + m_lastTime = millis(); + ++m_pingCount; } - - - /* - * 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 (m_updateConfig) { - //root["user0"] = userVar0; + serializeConfig(); + m_updateConfig = false; } + } + /** + * 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) + { + //this code adds "u":{"⚡ Ping fix pings": m_pingCount} to the info object + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); - /* - * 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) + String uiDomString = "⚡ Ping fix pings\ +Delay sec"; + + JsonArray infoArr = user.createNestedArray(uiDomString); //name + infoArr.add(m_pingCount); //value + + //this code adds "u":{"⚡ Reconnects": m_connectedWiFi - 1} to the info object + infoArr = user.createNestedArray("⚡ Reconnects"); //name + infoArr.add(m_connectedWiFi - 1); //value + } + + /** + * 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["PingDelay"] = (m_pingDelayMs/1000); + } + + /** + * 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 (root["PingDelay"] != nullptr) { - //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!")); - } - - - /* - * 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_FIXNETSERVICES; + m_pingDelayMs = (1000 * max(1UL, min(300UL, root["PingDelay"].as()))); + m_updateConfig = true; } + } - //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! + /** + * provide the changeable values + */ + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject("FixUnreachableNetServices"); + top["PingDelayMs"] = m_pingDelayMs; + } + + /** + * restore the changeable values + */ + bool readFromConfig(JsonObject &root) + { + JsonObject top = root["FixUnreachableNetServices"]; + if (top.isNull()) return false; + m_pingDelayMs = top["PingDelayMs"] | m_pingDelayMs; + m_pingDelayMs = max(5000UL, min(18000000UL, m_pingDelayMs)); + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return true; + } + + /** + * 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_FIXNETSERVICES; + } }; +#endif diff --git a/usermods/Fix_unreachable_webserver/readme.md b/usermods/Fix_unreachable_webserver/readme.md deleted file mode 100644 index 5ed17b87..00000000 --- a/usermods/Fix_unreachable_webserver/readme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Fix unreachable Webserver - -This modification performs a ping request to the local IP address every 60 seconds. By this procedure the web server remains accessible in some problematic WLAN 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. - -With this modification, in the worst case, the network functions are not available for 60 seconds until the next ping request. - -## Installation - -Copy and replace the file `usermod.cpp` in wled00 directory. - - diff --git a/usermods/Fix_unreachable_webserver/usermod.cpp b/usermods/Fix_unreachable_webserver/usermod.cpp deleted file mode 100644 index f1957da2..00000000 --- a/usermods/Fix_unreachable_webserver/usermod.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "wled.h" -/* - * 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 - */ - -#include - -const int PingDelayMs = 60000; -long lastCheckTime = 0; -bool connectedWiFi = false; -ping_option pingOpt; - -//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) - -//gets called once at boot. Do all initialization that doesn't depend on network here -void userSetup() -{ - -} - - -//gets called every time WiFi is (re-)connected. Initialize own network interfaces here -void userConnected() -{ - connectedWiFi = true; - // initialize ping_options structure - memset(&pingOpt, 0, sizeof(struct ping_option)); - pingOpt.count = 1; - pingOpt.ip = WiFi.localIP(); -} - -//loop. You can use "if (WLED_CONNECTED)" to check for successful connection -void userLoop() -{ - if (connectedWiFi && millis()-lastCheckTime > PingDelayMs) - { - ping_start(&pingOpt); - lastCheckTime = millis(); - } -} diff --git a/usermods/JSON_IR_remote/21-key_ir.json b/usermods/JSON_IR_remote/21-key_ir.json new file mode 100644 index 00000000..cc71b14d --- /dev/null +++ b/usermods/JSON_IR_remote/21-key_ir.json @@ -0,0 +1,119 @@ +{ + "desc": "21-key", + "0xFFA25D": { + "label": "On", + "pos": "1x1", + "cmd": "T=1" + }, + "0xFF629D": { + "label": "Off", + "pos": "1x2", + "cmd": "T=0" + }, + "0xFFE21D": { + "label": "Flash", + "pos": "1x3", + "cmnt": "Cycle Effects", + "cmd": "CY=0&FX=~" + }, + "0xFF22DD": { + "label": "Strobe", + "pos": "2x1", + "cmnt": "Sinelon Dual", + "cmd": "CY=0&FX=93" + }, + "0xFF02FD": { + "label": "Fade", + "pos": "2x2", + "cmnt": "Rain", + "cmd": "CY=0&FX=43" + }, + "0xFFC23D": { + "label": "Smooth", + "pos": "2x3", + "cmnt": "Aurora", + "cmd": "CY=0&FX=38" + }, + "0xFFE01F": { + "label": "Bright +", + "pos": "3x1", + "cmd": "A=~16" + }, + "0xFFA857": { + "label": "Bright -", + "pos": "3x2", + "cmd": "A=~-16" + }, + "0xFF906F": { + "label": "White", + "pos": "3x3", + "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" + }, + "0xFF6897": { + "label": "Red", + "pos": "4x1", + "cmnt": "Lava", + "cmd": "FP=8" + }, + "0xFF9867": { + "label": "Green", + "pos": "4x2", + "cmnt": "Forest", + "cmd": "FP=10" + }, + "0xFFB04F": { + "label": "Blue", + "pos": "4x3", + "cmnt": "Breeze", + "cmd": "FP=15" + }, + "0xFF30CF": { + "label": "Tomato", + "pos": "5x1", + "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" + }, + "0xFF18E7": { + "label": "LightGreen", + "pos": "5x2", + "cmnt": "Rivendale", + "cmd": "FP=14" + }, + "0xFF7A85": { + "label": "SkyBlue", + "pos": "5x3", + "cmnt": "Ocean", + "cmd": "FP=9" + }, + "0xFF10EF": { + "label": "Orange", + "pos": "6x1", + "cmnt": "Orangery", + "cmd": "FP=47" + }, + "0xFF38C7": { + "label": "Aqua", + "pos": "6x2", + "cmd": "FP=5&CL=hFFFF&C2=h7FFF&C3=h39A895" + }, + "0xFF5AA5": { + "label": "Purple", + "pos": "6x3", + "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" + }, + "0xFF42BD": { + "label": "Yellow", + "pos": "7x1", + "cmd": "FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE" + }, + "0xFF4AB5": { + "label": "Cyan", + "pos": "7x2", + "cmnt": "Beech", + "cmd": "FP=22" + }, + "0xFF52AD": { + "label": "Pink", + "pos": "7x3", + "cmd": "FP=5&CL=hFFC0CB&C2=hFFD4C0&C3=hA88C96" + } +} \ No newline at end of file diff --git a/usermods/JSON_IR_remote/24-key_ir.json b/usermods/JSON_IR_remote/24-key_ir.json new file mode 100644 index 00000000..48be10b9 --- /dev/null +++ b/usermods/JSON_IR_remote/24-key_ir.json @@ -0,0 +1,147 @@ +{ + "desc": "24-key", + "0xF700FF": { + "label": "+", + "pos": "1x1", + "cmnt": "Speed +", + "cmd": "SX=~16" + }, + "0xF7807F": { + "label": "-", + "pos": "1x2", + "cmnt": "Speed -", + "cmd": "SX=~-16" + }, + "0xF740BF": { + "label": "On/Off", + "pos": "1x3", + "cmnt": "Toggle On/Off", + "cmd": "T=2" + }, + "0xF7C03F": { + "label": "W", + "pos": "1x4", + "cmnt": "Cycle color palette", + "cmd": "FP=~" + }, + "0xF720DF": { + "label": "R", + "pos": "2x1", + "cmnt": "Lava", + "cmd": "FP=8" + }, + "0xF7A05F": { + "label": "G", + "pos": "2x2", + "cmnt": "Forest", + "cmd": "FP=10" + }, + "0xF7609F": { + "label": "B", + "pos": "2x3", + "cmnt": "Breeze", + "cmd": "FP=15" + }, + "0xF7E01F": { + "label": "Bright -", + "pos": "2x4", + "cmnt": "Bright -", + "cmd": "A=~-16" + }, + "0xF710EF": { + "label": "Timer1H", + "pos": "3x1", + "cmnt": "Timer 60 min", + "cmd": "NL=60&NT=0" + }, + "0xF7906F": { + "label": "Timer4H", + "pos": "3x2", + "cmnt": "Timer 30 min", + "cmd": "NL=30&NT=0" + }, + "0xF750AF": { + "label": "Timer8H", + "pos": "3x3", + "cmnt": "Timer 15 min", + "cmd": "NL=15&NT=0" + }, + "0xF7D02F": { + "label": "Bright128", + "pos": "3x4", + "cmnt": "Bright 128", + "cmd": "A=128" + }, + "0xF730CF": { + "label": "Music1", + "pos": "4x1", + "cmnt": "Cycle FX +", + "cmd": "FX=~" + }, + "0xF7B04F": { + "label": "Music2", + "pos": "4x2", + "cmnt": "Cycle FX -", + "cmd": "FX=~-1" + }, + "0xF7708F": { + "label": "Music3", + "pos": "4x3", + "cmnt": "Reset FX and FP", + "cmd": "FX=1&PF=6" + }, + "0xF7F00F": { + "label": "Bright +", + "pos": "4x4", + "cmnt": "Bright +", + "cmd": "A=~16" + }, + "0xF708F7": { + "label": "Mode1", + "pos": "5x1", + "cmnt": "Preset 1", + "cmd": "PL=1" + }, + "0xF78877": { + "label": "Mode2", + "pos": "5x2", + "cmnt": "Preset 2", + "cmd": "PL=2" + }, + "0xF748B7": { + "label": "Mode3", + "pos": "5x3", + "cmnt": "Preset 3", + "cmd": "PL=3" + }, + "0xF7C837": { + "label": "Up", + "pos": "5x4", + "cmnt": "Intensity +", + "cmd": "IX=~16" + }, + "0xF728D7": { + "label": "Mode4", + "pos": "6x1", + "cmnt": "Preset 4", + "cmd": "PL=4" + }, + "0xF7A857": { + "label": "Mode5", + "pos": "6x2", + "cmnt": "Preset 5", + "cmd": "PL=5" + }, + "0xF76897": { + "label": "Cycle", + "pos": "6x3", + "cmnt": "Toggle preset cycle", + "cmd": "CY=1&PT=60000" + }, + "0xF7E817": { + "label": "Down", + "pos": "6x4", + "cmnt": "Intensity -", + "cmd": "IX=~-16" + } +} \ No newline at end of file diff --git a/usermods/JSON_IR_remote/32-key_ir.json b/usermods/JSON_IR_remote/32-key_ir.json new file mode 100644 index 00000000..f58c7795 --- /dev/null +++ b/usermods/JSON_IR_remote/32-key_ir.json @@ -0,0 +1,185 @@ +{ + "desc": "32-key", + "0xFF08F7": { + "label": "On", + "pos": "1x1", + "cmd": "T=1" + }, + "0xFFC03F": { + "label": "Off", + "pos": "1x2", + "cmd": "T=0" + }, + "0xFF807F": { + "label": "Auto", + "pos": "1x3", + "cmnt": "Toggle preset cycle", + "cmd": "CY=2" + }, + "0xFF609F": { + "label": "Mode", + "pos": "1x4", + "cmnt": "Cycle effects", + "cmd": "FX=~&CY=0" + }, + "0xFF906F": { + "label": "4H", + "pos": "2x1", + "cmnt": "Timer 60min", + "cmd": "NL=60&NT=0" + }, + "0xFFB847": { + "label": "6H", + "pos": "2x2", + "cmnt": "Timer 90min", + "cmd": "NL=90&NT=0" + }, + "0xFFF807": { + "label": "8H", + "pos": "2x3", + "cmnt": "Timer 120min", + "cmd": "NL=120&NT=0" + }, + "0xFFB04F": { + "label": "Timer Off", + "pos": "2x4", + "cmd": "NL=0" + }, + "0xFF9867": { + "label": "Red", + "pos": "3x1", + "cmnt": "Lava", + "cmd": "FP=8" + }, + "0xFFD827": { + "label": "Green", + "pos": "3x2", + "cmnt": "Forest", + "cmd": "FP=10" + }, + "0xFF8877": { + "label": "Blue", + "pos": "3x3", + "cmnt": "Breeze", + "cmd": "FP=15" + }, + "0xFFA857": { + "label": "White", + "pos": "3x4", + "cmd": "FP=5&CL=hFFFFFF&C2=hFFE4CD&C3=hE4E4FF" + }, + "0xFFE817": { + "label": "OrangeRed", + "pos": "4x1", + "cmnt": "Sakura", + "cmd": "FP=49" + }, + "0xFF48B7": { + "label": "SeaGreen", + "pos": "4x2", + "cmnt": "Rivendale", + "cmd": "FP=14" + }, + "0xFF6897": { + "label": "RoyalBlue", + "pos": "4x3", + "cmnt": "Ocean", + "cmd": "FP=9" + }, + "0xFFB24D": { + "label": "DarkBlue", + "pos": "4x4", + "cmnt": "Breeze", + "cmd": "FP=15" + }, + "0xFF02FD": { + "label": "Orange", + "pos": "5x1", + "cmnt": "Orangery", + "cmd": "FP=47" + }, + "0xFF32CD": { + "label": "YellowGreen", + "pos": "5x2", + "cmnt": "Aurora", + "cmd": "FP=37" + }, + "0xFF20DF": { + "label": "SkyBlue", + "pos": "5x3", + "cmnt": "Beech", + "cmd": "FP=22" + }, + "0xFF00FF": { + "label": "Orchid", + "pos": "5x4", + "cmd": "FP=5&CL=hDA70D6&C2=hDA70A0&C3=h89618F" + }, + "0xFF50AF": { + "label": "Yellow", + "pos": "6x1", + "cmd": "FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE" + }, + "0xFF7887": { + "label": "DarkGreen", + "pos": "6x2", + "cmnt": "Orange and Teal", + "cmd": "FP=44" + }, + "0xFF708F": { + "label": "RebeccaPurple", + "pos": "6x3", + "cmd": "FP=5&CL=h800080&C2=h800040&C3=h4B1C54" + }, + "0xFF58A7": { + "label": "Plum", + "pos": "6x4", + "cmd": "FP=5&CL=hDDA0DD&C2=hDDA0BE&C3=h8D7791" + }, + "0xFF38C7": { + "label": "Strobe", + "pos": "7x1", + "cmnt": "Dancing Shadows", + "cmd": "FX=112&CY=0" + }, + "0xFF28D7": { + "label": "In Waves", + "pos": "7x2", + "cmnt": "Noise 1", + "cmd": "FX=70&CY=0" + }, + "0xFFF00F": { + "label": "Speed +", + "pos": "7x3", + "cmd": "SX=~16" + }, + "0xFF30CF": { + "label": "Speed -", + "pos": "7x4", + "cmd": "SX=~-16" + }, + "0xFF40BF": { + "label": "Jump", + "pos": "8x1", + "cmnt": "Colortwinkles", + "cmd": "FX=74&CY=0" + }, + "0xFF12ED": { + "label": "Fade", + "pos": "8x2", + "cmnt": "Sunrise", + "cmd": "FX=104&CY=0" + }, + "0xFF2AD5": { + "label": "Flash", + "pos": "8x3", + "cmnt": "Railway", + "cmd": "FX=78&CY=0" + }, + "0xFFA05F": { + "label": "Chase Flash", + "pos": "8x4", + "cmnt": "Washing Machine", + "cmd": "FX=113&CY=0" + } +} \ No newline at end of file diff --git a/usermods/JSON_IR_remote/40-key-black_ir.json b/usermods/JSON_IR_remote/40-key-black_ir.json new file mode 100644 index 00000000..71262b12 --- /dev/null +++ b/usermods/JSON_IR_remote/40-key-black_ir.json @@ -0,0 +1,233 @@ +{ + "desc": "40-key-black", + "0xFF3AC5": { + "label": "Bright +", + "pos": "1x1", + "cmd": "A=~16" + }, + "0xFFBA45": { + "label": "Bright -", + "pos": "1x2", + "cmd": "A=~-16" + }, + "0xFF827D": { + "label": "Off", + "pos": "1x3", + "cmd": "T=0" + }, + "0xFF02FD": { + "label": "On", + "pos": "1x4", + "cmd": "T=1" + }, + "0xFF1AE5": { + "label": "Red", + "pos": "2x1", + "cmnt": "Lava", + "cmd": "FP=8" + }, + "0xFF9A65": { + "label": "Green", + "pos": "2x2", + "cmnt": "Forest", + "cmd": "FP=10" + }, + "0xFFA25D": { + "label": "Blue", + "pos": "2x3", + "cmnt": "Breeze", + "cmd": "FP=15" + }, + "0xFF22DD": { + "label": "White", + "pos": "2x4", + "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" + }, + "0xFF2AD5": { + "label": "Tomato", + "pos": "3x1", + "cmnt": "Yelmag", + "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" + }, + "0xFFAA55": { + "label": "LightGreen", + "pos": "3x2", + "cmnt": "Rivendale", + "cmd": "FP=14" + }, + "0xFF926D": { + "label": "SkyBlue", + "pos": "3x3", + "cmnt": "Ocean", + "cmd": "FP=9" + }, + "0xFF12ED": { + "label": "WarmWhite", + "pos": "3x4", + "cmnt": "Warm White", + "cmd": "FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892" + }, + "0xFF0AF5": { + "label": "OrangeRed", + "pos": "4x1", + "cmnt": "Sakura", + "cmd": "FP=49" + }, + "0xFF8A75": { + "label": "Cyan", + "pos": "4x2", + "cmnt": "Beech", + "cmd": "FP=22" + }, + "0xFFB24D": { + "label": "RebeccaPurple", + "pos": "4x3", + "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" + }, + "0xFF32CD": { + "label": "CoolWhite", + "pos": "4x4", + "cmnt": "Cool White", + "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" + }, + "0xFF38C7": { + "label": "Orange", + "pos": "5x1", + "cmnt": "Orangery", + "cmd": "FP=47" + }, + "0xFFB847": { + "label": "Turquoise", + "pos": "5x2", + "cmd": "FP=5&CL=h40E0D0&C2=h40A0E0&C3=h4E9381" + }, + "0xFF7887": { + "label": "Purple", + "pos": "5x3", + "cmd": "FP=5&CL=h800080&C2=h800040&C3=h4B1C54" + }, + "0xFFF807": { + "label": "MedGray", + "pos": "5x4", + "cmnt": "Cycle palette +", + "cmd": "FP=~" + }, + "0xFF18E7": { + "label": "Yellow", + "pos": "6x1", + "cmd": "FP=5&CL=hFFFF00&C2=h7FFF00&C3=hA89539" + }, + "0xFF9867": { + "label": "DarkCyan", + "pos": "6x2", + "cmd": "FP=5&CL=h8B8B&C2=h458B&C3=h1F5B51" + }, + "0xFF58A7": { + "label": "Plum", + "pos": "6x3", + "cmnt": "Magenta", + "cmd": "FP=40" + }, + "0xFFD827": { + "label": "DarkGray", + "pos": "6x4", + "cmnt": "Cycle palette -", + "cmd": "FP=~-" + }, + "0xFF28D7": { + "label": "Jump3", + "pos": "7x1", + "cmnt": "Colortwinkles", + "cmd": "CY=0&FX=74" + }, + "0xFFA857": { + "label": "Fade3", + "pos": "7x2", + "cmnt": "Rain", + "cmd": "CY=0&FX=43" + }, + "0xFF6897": { + "label": "Flash", + "pos": "7x3", + "cmnt": "Cycle Effects", + "cmd": "CY=0&FX=~" + }, + "0xFFE817": { + "label": "Quick", + "pos": "7x4", + "cmnt": "Fx speed +16", + "cmd": "SX=~16" + }, + "0xFF08F7": { + "label": "Jump7", + "pos": "8x1", + "cmnt": "Sinelon Dual", + "cmd": "CY=0&FX=93" + }, + "0xFF8877": { + "label": "Fade7", + "pos": "8x2", + "cmnt": "Lighthouse", + "cmd": "CY=0&FX=41" + }, + "0xFF48B7": { + "label": "Auto", + "pos": "8x3", + "cmnt": "Toggle preset cycle", + "cmd": "CY=2" + }, + "0xFFC837": { + "label": "Slow", + "pos": "8x4", + "cmnt": "FX speed -16", + "cmd": "SX=~-16" + }, + "0xFF30CF": { + "label": "Custom1", + "pos": "9x1", + "cmnt": "Noise 1", + "cmd": "CY=0&FX=70" + }, + "0xFFB04F": { + "label": "Custom2", + "pos": "9x2", + "cmnt": "Dancing Shadows", + "cmd": "CY=0&FX=112" + }, + "0xFF708F": { + "label": "Music +", + "pos": "9x3", + "cmnt": "FX Intensity +16", + "cmd": "IX=~16" + }, + "0xFFF00F": { + "label": "Timer60", + "pos": "9x4", + "cmnt": "Timer 60 min", + "cmd": "NL=60&NT=0" + }, + "0xFF10EF": { + "label": "Custom3", + "pos": "10x1", + "cmnt": "Twinklefox", + "cmd": "CY=0&FX=80" + }, + "0xFF906F": { + "label": "Custom4", + "pos": "10x2", + "cmnt": "Twinklecat", + "cmd": "CY=0&FX=81" + }, + "0xFF50AF": { + "label": "Music -", + "pos": "10x3", + "cmnt": "FX Intesity -16", + "cmd": "IX=~-16" + }, + "0xFFD02F": { + "label": "Timer120", + "pos": "10x4", + "cmnt": "Timer 120 min", + "cmd": "NL=120&NT=0" + } +} \ No newline at end of file diff --git a/usermods/JSON_IR_remote/40-key-blue_ir.json b/usermods/JSON_IR_remote/40-key-blue_ir.json new file mode 100644 index 00000000..ed25d778 --- /dev/null +++ b/usermods/JSON_IR_remote/40-key-blue_ir.json @@ -0,0 +1,217 @@ +{ + "desc": "40-key-blue", + "0xFF3AC5": { + "label": "Bright +", + "pos": "1x1", + "cmd": "A=~16" + }, + "0xFFBA45": { + "label": "Bright -", + "pos": "1x2", + "cmd": "A=~-16" + }, + "0xFF827D": { + "label": "Off", + "pos": "1x3", + "cmd": "T=0" + }, + "0xFF02FD": { + "label": "On", + "pos": "1x4", + "cmd": "T=1" + }, + "0xFF1AE5": { + "label": "Red", + "pos": "2x1", + "cmnt": "Lava", + "cmd": "FP=8" + }, + "0xFF9A65": { + "label": "Green", + "pos": "2x2", + "cmnt": "Forest", + "cmd": "FP=10" + }, + "0xFFA25D": { + "label": "Blue", + "pos": "2x3", + "cmnt": "Breeze", + "cmd": "FP=15" + }, + "0xFF22DD": { + "label": "White", + "pos": "2x4", + "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" + }, + "0xFF2AD5": { + "label": "Tomato", + "pos": "3x1", + "cmnt": "Yelmag", + "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" + }, + "0xFFAA55": { + "label": "LightGreen", + "pos": "3x2", + "cmnt": "Rivendale", + "cmd": "FP=14" + }, + "0xFF926D": { + "label": "SkyBlue", + "pos": "3x3", + "cmnt": "Ocean", + "cmd": "FP=9" + }, + "0xFF12ED": { + "label": "WarmWhite", + "pos": "3x4", + "cmnt": "Warm White", + "cmd": "FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892" + }, + "0xFF0AF5": { + "label": "OrangeRed", + "pos": "4x1", + "cmnt": "Sakura", + "cmd": "FP=49" + }, + "0xFF8A75": { + "label": "Cyan", + "pos": "4x2", + "cmnt": "Beech", + "cmd": "FP=22" + }, + "0xFFB24D": { + "label": "RebeccaPurple", + "pos": "4x3", + "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" + }, + "0xFF32CD": { + "label": "CoolWhite", + "pos": "4x4", + "cmnt": "Cool White", + "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" + }, + "0xFF38C7": { + "label": "Orange", + "pos": "5x1", + "cmnt": "Orangery", + "cmd": "FP=47" + }, + "0xFFB847": { + "label": "Turquoise", + "pos": "5x2", + "cmd": "FP=5&CL=h40E0D0&C2=h40A0E0&C3=h4E9381" + }, + "0xFF7887": { + "label": "Purple", + "pos": "5x3", + "cmd": "FP=5&CL=h800080&C2=h800040&C3=h4B1C54" + }, + "0xFFF807": { + "label": "MedGray", + "pos": "5x4", + "cmnt": "Cycle palette +", + "cmd": "FP=~" + }, + "0xFF18E7": { + "label": "Yellow", + "pos": "6x1", + "cmd": "FP=5&CL=hFFFF00&C2=h7FFF00&C3=hA89539" + }, + "0xFF9867": { + "label": "DarkCyan", + "pos": "6x2", + "cmd": "FP=5&CL=h8B8B&C2=h458B&C3=h1F5B51" + }, + "0xFF58A7": { + "label": "Plum", + "pos": "6x3", + "cmnt": "Magenta", + "cmd": "FP=40" + }, + "0xFFD827": { + "label": "DarkGray", + "pos": "6x4", + "cmnt": "Cycle palette -", + "cmd": "FP=~-" + }, + "0xFF28D7": { + "label": "W +", + "pos": "7x1" + }, + "0xFFA857": { + "label": "W -", + "pos": "7x2" + }, + "0xFF6897": { + "label": "W On", + "pos": "7x3" + }, + "0xFFE817": { + "label": "W Off", + "pos": "7x4" + }, + "0xFF08F7": { + "label": "W25", + "pos": "8x1" + }, + "0xFF8877": { + "label": "W50", + "pos": "8x2" + }, + "0xFF48B7": { + "label": "W75", + "pos": "8x3" + }, + "0xFFC837": { + "label": "W100", + "pos": "8x4" + }, + "0xFF30CF": { + "label": "Jump3", + "pos": "9x1", + "cmnt": "Colortwinkles", + "cmd": "CY=0&FX=74" + }, + "0xFFB04F": { + "label": "Fade3", + "pos": "9x2", + "cmnt": "Rain", + "cmd": "CY=0&FX=43" + }, + "0xFF708F": { + "label": "Jump7", + "pos": "9x3", + "cmnt": "Sinelon Dual", + "cmd": "CY=0&FX=93" + }, + "0xFFF00F": { + "label": "Quick", + "pos": "9x4", + "cmnt": "Fx speed +16", + "cmd": "SX=~16" + }, + "0xFF10EF": { + "label": "Fade", + "pos": "10x1", + "cmnt": "Lighthouse", + "cmd": "CY=0&FX=41" + }, + "0xFF906F": { + "label": "Flash", + "pos": "10x2", + "cmnt": "Cycle Effects", + "cmd": "CY=0&FX=~" + }, + "0xFF50AF": { + "label": "Auto", + "pos": "10x3", + "cmnt": "Toggle preset cycle", + "cmd": "CY=2" + }, + "0xFFD02F": { + "label": "Slow", + "pos": "10x4", + "cmnt": "Sinelon Dual", + "cmd": "CY=0&FX=93" + } +} \ No newline at end of file diff --git a/usermods/JSON_IR_remote/44-key_ir.json b/usermods/JSON_IR_remote/44-key_ir.json new file mode 100644 index 00000000..bd78e766 --- /dev/null +++ b/usermods/JSON_IR_remote/44-key_ir.json @@ -0,0 +1,241 @@ +{ + "desc": "44-key", + "0xFF3AC5": { + "label": "Bright +", + "pos": "1x1", + "cmd": "A=~16" + }, + "0xFFBA45": { + "label": "Bright -", + "pos": "1x2", + "cmd": "A=~-16" + }, + "0xFF827D": { + "label": "Off", + "pos": "1x3", + "cmd": "T=0" + }, + "0xFF02FD": { + "label": "On", + "pos": "1x4", + "cmd": "T=1" + }, + "0xFF1AE5": { + "label": "Red", + "pos": "2x1", + "cmnt": "Lava", + "cmd": "FP=8" + }, + "0xFF9A65": { + "label": "Green", + "pos": "2x2", + "cmnt": "Forest", + "cmd": "FP=10" + }, + "0xFFA25D": { + "label": "Blue", + "pos": "2x3", + "cmnt": "Breeze", + "cmd": "FP=15" + }, + "0xFF22DD": { + "label": "White", + "pos": "2x4", + "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" + }, + "0xFF2AD5": { + "label": "Tomato", + "pos": "3x1", + "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" + }, + "0xFFAA55": { + "label": "LightGreen", + "pos": "3x2", + "cmnt": "Rivendale", + "cmd": "FP=14" + }, + "0xFF926D": { + "label": "DeepBlue", + "pos": "3x3", + "cmnt": "Ocean", + "cmd": "FP=9" + }, + "0xFF12ED": { + "label": "Warmwhite2", + "pos": "3x4", + "cmnt": "Warm White", + "cmd": "FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892" + }, + "0xFF0AF5": { + "label": "Orange", + "pos": "4x1", + "cmnt": "Sakura", + "cmd": "FP=49" + }, + "0xFF8A75": { + "label": "Turquoise", + "pos": "4x2", + "cmnt": "Beech", + "cmd": "FP=22" + }, + "0xFFB24D": { + "label": "Purple", + "pos": "4x3", + "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" + }, + "0xFF32CD": { + "label": "WarmWhite", + "pos": "4x4", + "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" + }, + "0xFF38C7": { + "label": "Yellowish", + "pos": "5x1", + "cmnt": "Orangery", + "cmd": "FP=47" + }, + "0xFFB847": { + "label": "Cyan", + "pos": "5x2", + "cmnt": "Beech", + "cmd": "FP=22" + }, + "0xFF7887": { + "label": "Magenta", + "pos": "5x3", + "cmd": "FP=5&CL=hFF00FF&C2=hFF007F&C3=h9539A8" + }, + "0xFFF807": { + "label": "ColdWhite", + "pos": "5x4", + "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" + }, + "0xFF18E7": { + "label": "Yellow", + "pos": "6x1", + "cmd": "FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE" + }, + "0xFF9867": { + "label": "Aqua", + "pos": "6x2", + "cmd": "FP=5&CL=hFFFF&C2=h7FFF&C3=h39A895" + }, + "0xFF58A7": { + "label": "Pink", + "pos": "6x3", + "cmd": "FP=5&CL=hFFC0CB&C2=hFFD4C0&C3=hA88C96" + }, + "0xFFD827": { + "label": "ColdWhite2", + "pos": "6x4", + "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" + }, + "0xFF28D7": { + "label": "Red +", + "pos": "7x1", + "cmd": "FP=5&R=~16" + }, + "0xFFA857": { + "label": "Green +", + "pos": "7x2", + "cmd": "FP=5&G=~16" + }, + "0xFF6897": { + "label": "Blue +", + "pos": "7x3", + "cmd": "FP=5&B=~16" + }, + "0xFFE817": { + "label": "Quick", + "pos": "7x4", + "cmnt": "Fx speed +16", + "cmd": "SX=~16" + }, + "0xFF08F7": { + "label": "Red -", + "pos": "8x1", + "cmd": "FP=5&R=~-16" + }, + "0xFF8877": { + "label": "Green -", + "pos": "8x2", + "cmd": "FP=5&G=~-16" + }, + "0xFF48B7": { + "label": "Blue -", + "pos": "8x3", + "cmd": "FP=5&B=~-16" + }, + "0xFFC837": { + "label": "Slow", + "pos": "8x4", + "cmnt": "FX speed -16", + "cmd": "SX=~-16" + }, + "0xFF30CF": { + "label": "Diy1", + "pos": "9x1", + "cmd": "CY=0&PL=1" + }, + "0xFFB04F": { + "label": "Diy2", + "pos": "9x2", + "cmd": "CY=0&PL=2" + }, + "0xFF708F": { + "label": "Diy3", + "pos": "9x3", + "cmd": "CY=0&PL=3" + }, + "0xFFF00F": { + "label": "Auto", + "pos": "9x4", + "cmnt": "Toggle preset cycle", + "cmd": "CY=2" + }, + "0xFF10EF": { + "label": "Diy4", + "pos": "10x1", + "cmd": "CY=0&PL=4" + }, + "0xFF906F": { + "label": "Diy5", + "pos": "10x2", + "cmd": "CY=0&PL=5" + }, + "0xFF50AF": { + "label": "Diy6", + "pos": "10x3", + "cmd": "CY=0&PL=6" + }, + "0xFFD02F": { + "label": "Flash", + "pos": "10x4", + "cmnt": "Cycle Effects", + "cmd": "CY=0&FX=~" + }, + "0xFF20DF": { + "label": "Jump3", + "pos": "11x1", + "cmnt": "Colortwinkles", + "cmd": "CY=0&FX=74" + }, + "0xFFA05F": { + "label": "Jump7", + "pos": "11x2", + "cmnt": "Sinelon Dual", + "cmd": "CY=0&FX=93" + }, + "0xFF609F": { + "label": "Fade3", + "pos": "11x3", + "cmnt": "Rain", + "cmd": "CY=0&FX=43" + }, + "0xFFE01F": { + "label": "Fade7", + "pos": "11x4", + "cmnt": "Lighthouse", + "cmd": "CY=0&FX=41" + } +} \ No newline at end of file diff --git a/usermods/JSON_IR_remote/6-key_ir.json b/usermods/JSON_IR_remote/6-key_ir.json new file mode 100644 index 00000000..d4960a4b --- /dev/null +++ b/usermods/JSON_IR_remote/6-key_ir.json @@ -0,0 +1,38 @@ +{ + "desc": "6-key", + "0xFF0FF0": { + "label": "Power", + "pos": "1x1", + "cmd": "T=2" + }, + "0xFF8F70": { + "label": "Channel +", + "pos": "2x1", + "cmnt": "Cycle palette up", + "cmd": "FP=~" + }, + "0xFF4FB0": { + "label": "Channel -", + "pos": "3x1", + "cmnt": "Cycle palette down", + "cmd": "FP=~-" + }, + "0xFFCF30": { + "label": "Volume +", + "pos": "4x1", + "cmnt": "Brighten", + "cmd": "A=~16" + }, + "0xFF2FD0": { + "label": "Volume -", + "pos": "5x1", + "cmnt": "Dim", + "cmd": "A=~-16" + }, + "0xFFAF50": { + "label": "Mute", + "pos": "6x1", + "cmnt": "Cycle effects", + "cmd": "CY=0&FX=~" + } +} \ No newline at end of file diff --git a/usermods/JSON_IR_remote/9-key_ir.json b/usermods/JSON_IR_remote/9-key_ir.json new file mode 100644 index 00000000..f0bdd8f3 --- /dev/null +++ b/usermods/JSON_IR_remote/9-key_ir.json @@ -0,0 +1,47 @@ +{ + "desc": "9-key", + "0xFF629D": { + "label": "Power", + "cmd": "T=2" + }, + "0xFF22DD": { + "label": "A", + "cmnt": "Preset 1", + "cmd": "PL=1" + }, + "0xFF02FD": { + "label": "B", + "cmnt": "Preset 2", + "cmd": "PL=2" + }, + "0xFFC23D": { + "label": "C", + "cmnt": "Preset 3", + "cmd": "PL=3" + }, + "0xFF30CF": { + "label": "Left", + "cmnt": "Speed -", + "cmd": "SI=~-16" + }, + "0xFF7A85": { + "label": "Right", + "cmnt": "Speed +", + "cmd": "SI=~16" + }, + "0xFF9867": { + "label": "Up", + "cmnt": "Bright +", + "cmd": "A=~16" + }, + "0xFF38C7": { + "label": "Down", + "cmnt": "Bright -", + "cmd": "A=~-16" + }, + "0xFF18E7": { + "label": "Select", + "cmnt": "Cycle effects", + "cmd": "CY=0&FX=~" + } +} \ No newline at end of file diff --git a/usermods/JSON_IR_remote/IR_Remote_Codes.xlsx b/usermods/JSON_IR_remote/IR_Remote_Codes.xlsx new file mode 100644 index 00000000..b1f99d3a Binary files /dev/null and b/usermods/JSON_IR_remote/IR_Remote_Codes.xlsx differ diff --git a/usermods/JSON_IR_remote/ir_json_maker.py b/usermods/JSON_IR_remote/ir_json_maker.py new file mode 100644 index 00000000..a6adcc8c --- /dev/null +++ b/usermods/JSON_IR_remote/ir_json_maker.py @@ -0,0 +1,108 @@ +import colorsys +import json +import openpyxl + +named_colors = {'AliceBlue': '0xF0F8FF', 'AntiqueWhite': '0xFAEBD7', 'Aqua': '0x00FFFF', + 'Aquamarine': '0x7FFFD4', 'Azure': '0xF0FFFF', 'Beige': '0xF5F5DC', 'Bisque': '0xFFE4C4', + 'Black': '0x000000', 'BlanchedAlmond': '0xFFEBCD', 'Blue': '0x0000FF', + 'BlueViolet': '0x8A2BE2', 'Brown': '0xA52A2A', 'BurlyWood': '0xDEB887', + 'CadetBlue': '0x5F9EA0', 'Chartreuse': '0x7FFF00', 'Chocolate': '0xD2691E', + 'Coral': '0xFF7F50', 'CornflowerBlue': '0x6495ED', 'Cornsilk': '0xFFF8DC', + 'Crimson': '0xDC143C', 'Cyan': '0x00FFFF', 'DarkBlue': '0x00008B', 'DarkCyan': '0x008B8B', + 'DarkGoldenRod': '0xB8860B', 'DarkGray': '0xA9A9A9', 'DarkGrey': '0xA9A9A9', + 'DarkGreen': '0x006400', 'DarkKhaki': '0xBDB76B', 'DarkMagenta': '0x8B008B', + 'DarkOliveGreen': '0x556B2F', 'DarkOrange': '0xFF8C00', 'DarkOrchid': '0x9932CC', + 'DarkRed': '0x8B0000', 'DarkSalmon': '0xE9967A', 'DarkSeaGreen': '0x8FBC8F', + 'DarkSlateBlue': '0x483D8B', 'DarkSlateGray': '0x2F4F4F', 'DarkSlateGrey': '0x2F4F4F', + 'DarkTurquoise': '0x00CED1', 'DarkViolet': '0x9400D3', 'DeepPink': '0xFF1493', + 'DeepSkyBlue': '0x00BFFF', 'DimGray': '0x696969', 'DimGrey': '0x696969', + 'DodgerBlue': '0x1E90FF', 'FireBrick': '0xB22222', 'FloralWhite': '0xFFFAF0', + 'ForestGreen': '0x228B22', 'Fuchsia': '0xFF00FF', 'Gainsboro': '0xDCDCDC', + 'GhostWhite': '0xF8F8FF', 'Gold': '0xFFD700', 'GoldenRod': '0xDAA520', 'Gray': '0x808080', + 'Grey': '0x808080', 'Green': '0x008000', 'GreenYellow': '0xADFF2F', 'HoneyDew': '0xF0FFF0', + 'HotPink': '0xFF69B4', 'IndianRed': '0xCD5C5C', 'Indigo': '0x4B0082', 'Ivory': '0xFFFFF0', + 'Khaki': '0xF0E68C', 'Lavender': '0xE6E6FA', 'LavenderBlush': '0xFFF0F5', + 'LawnGreen': '0x7CFC00', 'LemonChiffon': '0xFFFACD', 'LightBlue': '0xADD8E6', + 'LightCoral': '0xF08080', 'LightCyan': '0xE0FFFF', 'LightGoldenRodYellow': '0xFAFAD2', + 'LightGray': '0xD3D3D3', 'LightGrey': '0xD3D3D3', 'LightGreen': '0x90EE90', + 'LightPink': '0xFFB6C1', 'LightSalmon': '0xFFA07A', 'LightSeaGreen': '0x20B2AA', + 'LightSkyBlue': '0x87CEFA', 'LightSlateGray': '0x778899', 'LightSlateGrey': '0x778899', + 'LightSteelBlue': '0xB0C4DE', 'LightYellow': '0xFFFFE0', 'Lime': '0x00FF00', + 'LimeGreen': '0x32CD32', 'Linen': '0xFAF0E6', 'Magenta': '0xFF00FF', 'Maroon': '0x800000', + 'MediumAquaMarine': '0x66CDAA', 'MediumBlue': '0x0000CD', 'MediumOrchid': '0xBA55D3', + 'MediumPurple': '0x9370DB', 'MediumSeaGreen': '0x3CB371', 'MediumSlateBlue': '0x7B68EE', + 'MediumSpringGreen': '0x00FA9A', 'MediumTurquoise': '0x48D1CC', 'MediumVioletRed': '0xC71585', + 'MidnightBlue': '0x191970', 'MintCream': '0xF5FFFA', 'MistyRose': '0xFFE4E1', + 'Moccasin': '0xFFE4B5', 'NavajoWhite': '0xFFDEAD', 'Navy': '0x000080', 'OldLace': '0xFDF5E6', + 'Olive': '0x808000', 'OliveDrab': '0x6B8E23', 'Orange': '0xFFA500', 'OrangeRed': '0xFF4500', + 'Orchid': '0xDA70D6', 'PaleGoldenRod': '0xEEE8AA', 'PaleGreen': '0x98FB98', + 'PaleTurquoise': '0xAFEEEE', 'PaleVioletRed': '0xDB7093', 'PapayaWhip': '0xFFEFD5', + 'PeachPuff': '0xFFDAB9', 'Peru': '0xCD853F', 'Pink': '0xFFC0CB', 'Plum': '0xDDA0DD', + 'PowderBlue': '0xB0E0E6', 'Purple': '0x800080', 'RebeccaPurple': '0x663399', 'Red': '0xFF0000', + 'RosyBrown': '0xBC8F8F', 'RoyalBlue': '0x4169E1', 'SaddleBrown': '0x8B4513', 'Salmon': '0xFA8072', + 'SandyBrown': '0xF4A460', 'SeaGreen': '0x2E8B57', 'SeaShell': '0xFFF5EE', 'Sienna': '0xA0522D', + 'Silver': '0xC0C0C0', 'SkyBlue': '0x87CEEB', 'SlateBlue': '0x6A5ACD', 'SlateGray': '0x708090', + 'SlateGrey': '0x708090', 'Snow': '0xFFFAFA', 'SpringGreen': '0x00FF7F', 'SteelBlue': '0x4682B4', + 'Tan': '0xD2B48C', 'Teal': '0x008080', 'Thistle': '0xD8BFD8', 'Tomato': '0xFF6347', + 'Turquoise': '0x40E0D0', 'Violet': '0xEE82EE', 'Wheat': '0xF5DEB3', 'White': '0xFFFFFF', + 'WhiteSmoke': '0xF5F5F5', 'Yellow': '0xFFFF00', 'YellowGreen': '0x9ACD32'} + +def shift_color(col, shift=30, sat=1.0, val=1.0): + r = (col & (255 << 16)) >> 16 + g = (col & (255 << 8)) >> 8 + b = col & 255 + hsv = colorsys.rgb_to_hsv(r, g, b) + h = (((hsv[0] * 360) + shift) % 360) / 360 + rgb = colorsys.hsv_to_rgb(h, hsv[1] * sat, hsv[2] * val) + return (int(rgb[0]) << 16) + (int(rgb[1]) << 8) + int(rgb[2]) + +def parse_sheet(ws): + print(f'Parsing worksheet {ws.title}') + ir = {"desc": ws.title} + rows = ws.rows + keys = [col.value.lower() for col in next(rows)] + for row in rows: + rec = dict(zip(keys, [col.value for col in row])) + if rec.get('code') is None: + continue + cd = {"label": rec.get('label')} + if rec.get('row'): + cd['pos'] = f'{rec["row"]}x{rec["col"]}' + if rec.get('comment'): + cd['cmnt'] = rec.get('comment') + if rec.get('rpt'): + cd['rpt'] = bool(rec['rpt']) + + if rec.get('cmd'): + cd['cmd'] = rec['cmd'] + elif all((rec.get('primary'), rec.get('secondary'), rec.get('tertiary'))): + c1 = int(rec.get('primary'), 16) + c2 = int(rec.get('secondary'), 16) + c3 = int(rec.get('tertiary'), 16) + cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' + elif all((rec.get('primary'), rec.get('secondary'))): + c1 = int(rec.get('primary'), 16) + c2 = int(rec.get('secondary'), 16) + c3 = shift_color(c1, -1, sat=0.66, val=0.66) + cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' + elif rec.get('primary'): + c1 = int(rec.get('primary'), 16) + c2 = shift_color(c1, 30) + c3 = shift_color(c1, -10, sat=0.66, val=0.66) + cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' + elif rec.get('label') in named_colors: + c1 = int(named_colors[rec.get('label')], 16) + c2 = shift_color(c1, 30) + c3 = shift_color(c1, -10, sat=0.66, val=0.66) + cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' + else: + print(f'Did not find a command or color for {rec["label"]}. Hint use named CSS colors as labels') + ir[rec['code']] = cd + + with open(f'{ws.title}_ir.json', 'w') as fp: + json.dump(ir, fp, indent=2) + +if __name__ == '__main__': + wb = openpyxl.load_workbook('IR_Remote_Codes.xlsx') + for ws in wb.worksheets: + parse_sheet(ws) diff --git a/usermods/JSON_IR_remote/readme.md b/usermods/JSON_IR_remote/readme.md new file mode 100644 index 00000000..43532a6f --- /dev/null +++ b/usermods/JSON_IR_remote/readme.md @@ -0,0 +1,33 @@ +# JSON IR remote + +## Purpose + +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 + +* Upload the IR config file, named _ir.json_ to your board using the [ip address]/edit url. Pick from one of the included files or create your own. +* On the config > LED settings page, set the correct IR pin. +* On the config > Sync Interfaces page, select "JSON Remote" as the Infrared remote. + +## Modification + +* See if there is a json file with the same number of buttons as your remote. Many remotes will have the same internals and emit the same codes but have different labels. +* In the ir.json file, each key will be the hex encoded IR code. +* The "cmd" property will be the HTTP Request API or JSON API to execute when that button is pressed. +* A limited number of c functions are supported (!incBrightness, !decBrightness, !presetFallback) +* When using !presetFallback, include properties PL (preset to load), FX (effect to fall back to) and FP (palette to fall back to) +* If the command is _repeatable_ and does not contain the "~" character, add a "rpt": true property. +* Other properties are ignored, but having a label property may help when editing. + + +Sample: +{ + "0xFF629D": {"cmd": "T=2", "rpt": true, "label": "Toggle on/off"}, // HTTP command + "0xFF9867": {"cmd": "A=~16", "label": "Inc brightness"}, // HTTP command with incrementing + "0xFF38C7": {"cmd": {"bri": 10}, "label": "Dim to 10"}, // JSON command + "0xFF22DD": {"cmd": "!presetFallback", "PL": 1, "FX": 16, "FP": 6, + "label": "Preset 1 or fallback to Saw - Party"}, // c function +} diff --git a/usermods/MY9291/MY92xx.h b/usermods/MY9291/MY92xx.h new file mode 100644 index 00000000..658852b4 --- /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 00000000..66bbc34c --- /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/PIR_Highlight_Standby b/usermods/PIR_sensor_switch/PIR_Highlight_Standby new file mode 100644 index 00000000..152388e8 --- /dev/null +++ b/usermods/PIR_sensor_switch/PIR_Highlight_Standby @@ -0,0 +1,347 @@ +#pragma once + +#include "wled.h" + +/* + * -------------------- + * Rawframe edit: + * - TESTED ON WLED VS.0.10.1 - WHERE ONLY PRESET 16 SAVES SEGMENTS - some macros may not be needed if this changes. + * - Code has been modified as my usage changed, as such it has poor use of functions vs if thens, but feel free to change it for me :) + * + * Edited to SWITCH between two lighting scenes/modes : STANDBY and HIGHLIGHT + * + * Usage: + * - Standby is the default mode and Highlight is activated when the PIR detects activity. + * - PIR delay now set to same value as Nightlight feature on boot but otherwise controlled as normal. + * - Standby and Highlight brightness can be set on the fly (default values set on boot via macros calling presets). + * - Macros are used to set Standby and Highlight states (macros can load saved presets etc). + * + * - Macro short button press = Highlight state default (used on boot only and sets default brightness). + * - Macro double button press = Standby state default (used on boot only and sets default brightness). + * - Macro long button press = Highlight state (after boot). + * - Macro 16 = Standby state (after boot). + * + * ! It is advised not to set 'Apply preset at boot' or a boot macro (that activates a preset) as we will call our own macros on boot. + * + * - When the strip is off before PIR activates the strip will return to off for Standby mode, and vice versa. + * - When the strip is turned off while in Highlight mode, it will return to standby mode. (This behaviour could be changed easily if for some reason you wanted the lights to go out when the pir is activated). + * - Macros can be chained so you could do almost anything, such as have standby mode also turn on the nightlight function with a new time delay. + * + * Segment Notes: + * - It's easier to save the segment selections in preset than apply via macro while we a limited to preset 16. (Ie, instead of selecting sections at the point of activating standby/highlight modes). + * - Because only preset 16 saves segments, for now we are having to use addiotional macros to control segments where they are involved. Macros can be chained so this works but it would be better if macros also accepted json-api commands. (Testing http api segement behaviour of SS with SB left me a little confused). + * + * Future: + * - Maybe a second timer/timetable that turns on/off standby mode also after set inactivity period / date & times. For now this can be achieved others ways so may not be worth eating more processing power. + * + * -------------------- + * + * 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 { + private: + // PIR sensor pin + const uint8_t PIRsensorPin = 13; // D7 on D1 mini + // notification mode for stateUpdated() + const byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // CALL_MODE_DIRECT_CHANGE + // 1 min delay before switch off after the sensor state goes LOW + uint32_t m_switchOffDelay = 60000; + // off timer start time + uint32_t m_offTimerStart = 0; + // current PIR sensor pin state + byte m_PIRsensorPinState = LOW; + // PIR sensor enabled - ISR attached + bool m_PIRenabled = true; + // temp standby brightness store. initial value set as nightlight default target brightness + byte briStandby _INIT(nightlightTargetBri); + // temp hightlight brightness store. initial value set as current brightness + byte briHighlight _INIT(bri); + // highlight active/deactive monitor + bool highlightActive = false; + // wled on/off state in standby mode + bool standbyoff = false; + + /* + * return or change if new PIR sensor state is available + */ + static volatile bool newPIRsensorState(bool changeState = false, bool newState = false) { + static volatile bool s_PIRsensorState = false; + if (changeState) { + s_PIRsensorState = newState; + } + return s_PIRsensorState; + } + + /* + * PIR sensor state has changed + */ + static void IRAM_ATTR ISR_PIRstateChange() { + newPIRsensorState(true, true); + } + + /* + * switch strip on/off + */ + // now allowing adjustable standby and highlight brightness + void switchStrip(bool switchOn) { + //if (switchOn && bri == 0) { + if (switchOn) { // **pir sensor is on and activated** + //bri = briLast; + if (bri != 0) { // is WLED currently on + if (highlightActive) { // and is Highlight already on + briHighlight = bri; // then update highlight brightness with current brightness + } + else { + briStandby = bri; // else update standby brightness with current brightness + } + } + else { // WLED is currently off + if (!highlightActive) { // and Highlight is not already on + briStandby = briLast; // then update standby brightness with last active brightness (before turned off) + standbyoff = true; + } + else { // and Highlight is already on + briHighlight = briLast; // then set hightlight brightness to last active brightness (before turned off) + } + } + applyMacro(16); // apply highlight lighting without brightness + if (bri != briHighlight) { + bri = briHighlight; // set current highlight brightness to last set highlight brightness + } + stateUpdated(NotifyUpdateMode); + highlightActive = true; // flag highlight is on + } + else { // **pir timer has elapsed** + //briLast = bri; + //bri = 0; + if (bri != 0) { // is WLED currently on + briHighlight = bri; // update highlight brightness with current brightness + if (!standbyoff) { // + bri = briStandby; // set standby brightness to last set standby brightness + } + else { // + briLast = briStandby; // set standby off brightness + bri = 0; // set power off in standby + standbyoff = false; // turn off flag + } + applyMacro(macroLongPress); // apply standby lighting without brightness + } + else { // WLED is currently off + briHighlight = briLast; // set last active brightness (before turned off) to highlight lighting brightness + if (!standbyoff) { // + bri = briStandby; // set standby brightness to last set standby brightness + } + else { // + briLast = briStandby; // set standby off brightness + bri = 0; // set power off in standby + standbyoff = false; // turn off flag + } + applyMacro(macroLongPress); // apply standby lighting without brightness + } + stateUpdated(NotifyUpdateMode); + highlightActive = false; // flag highlight is off + } + } + + /* + * Read and update PIR sensor state. + * Initilize/reset switch off timer + */ + bool updatePIRsensorState() { + if (newPIRsensorState()) { + m_PIRsensorPinState = digitalRead(PIRsensorPin); + + if (m_PIRsensorPinState == HIGH) { + m_offTimerStart = 0; + switchStrip(true); + } + else if (bri != 0) { + // start switch off timer + m_offTimerStart = millis(); + } + newPIRsensorState(true, false); + return true; + } + return false; + } + + /* + * switch off the strip if the delay has elapsed + */ + bool handleOffTimer() { + if (m_offTimerStart > 0) { + if ((millis() - m_offTimerStart > m_switchOffDelay) || bri == 0 ) { // now also checking for manual power off during highlight mode + switchStrip(false); + m_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() { + // PIR Sensor mode INPUT_PULLUP + pinMode(PIRsensorPin, INPUT_PULLUP); + // assign interrupt function and set CHANGE mode + attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); + // set delay to nightlight default duration on boot (after which json PIRoffSec overides if needed) + m_switchOffDelay = (nightlightDelayMins*60000); + applyMacro(macroButton); // apply default highlight lighting + briHighlight = bri; + applyMacro(macroDoublePress); // apply default standby lighting with brightness + briStandby = bri; + } + + + /* + * 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() { + 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) + { + //this code adds "u":{"⏲ PIR sensor state":uiDomString} to the info object + // the value contains a button to toggle the sensor enabled/disabled + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray("⏲ PIR sensor state"); //name + String uiDomString = ""; + infoArr.add(uiDomString); //value + + //this code adds "u":{"⏲ switch off timer":uiDomString} to the info object + infoArr = user.createNestedArray("⏲ switch off timer"); //name + + // off timer + if (m_offTimerStart > 0) { + uiDomString = ""; + unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; + if (offSeconds >= 3600) { + uiDomString += (offSeconds / 3600); + uiDomString += " hours "; + offSeconds %= 3600; + } + if (offSeconds >= 60) { + uiDomString += (offSeconds / 60); + offSeconds %= 60; + } else if (uiDomString.length() > 0){ + uiDomString += 0; + } + if (uiDomString.length() > 0){ + uiDomString += " min "; + } + uiDomString += (offSeconds); + infoArr.add(uiDomString + " sec"); + } else { + infoArr.add("inactive"); + } + } + + + /* + * 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 + * Add "PIRenabled" to json state. This can be used to disable/enable the sensor. + * Add "PIRoffSec" to json state. This can be used to adjust milliseconds . + */ + void addToJsonState(JsonObject& root) + { + root["PIRenabled"] = m_PIRenabled; + root["PIRoffSec"] = (m_switchOffDelay / 1000); + } + + + /* + * 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 "PIRenabled" from json state and switch enable/disable the PIR sensor. + * Read "PIRoffSec" from json state and adjust milliseconds . + */ + void readFromJsonState(JsonObject& root) + { + if (root["PIRoffSec"] != nullptr) { + m_switchOffDelay = (1000 * max(60UL, min(43200UL, root["PIRoffSec"].as()))); + } + + if (root["PIRenabled"] != nullptr) { + if (root["PIRenabled"] && !m_PIRenabled) { + attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); + newPIRsensorState(true, true); + } + else if(m_PIRenabled) { + detachInterrupt(PIRsensorPin); + } + m_PIRenabled = root["PIRenabled"]; + } + } + + + /* + * 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; + } + + //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! +}; diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index 447f541c..574bd06d 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -7,69 +7,87 @@ _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 items below - -- the state of the sensor. By clicking on the state the sensor can be deactivated/activated. -**I recommend to deactivate the sensor before installing an OTA update**. -- the remaining time of the off timer. - -## JSON API - -The usermod supports the following state changes: - -| JSON key | Value range | Description | -|------------|-------------|---------------------------------| -| PIRenabled | bool | Deactivdate/activate the sensor | -| PIRoffSec | 60 to 43200 | Off timer seconds | +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 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) for the sensor signal. +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 optionaly `-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**: +## 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 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 lattitude/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 other usermods in `usermods_list.cpp' + +or + +2. Use `#include "usermod_PIR_sensor_switch.h"` at the top of the `usermod.h` where you need it. + +**Example usermod.h :** ```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" +#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()); +class MyUsermod : public Usermod { + //... -} + void togglePIRSensor() { + #ifdef USERMOD_PIR_SENSOR_SWITCH + PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) usermods.lookup(USERMOD_ID_PIRSWITCH); + if (PIRsensor != nullptr) { + PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled()); + } + #endif + } + //... +}; ``` -Have fun - @gegu +### Configuration options + +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 +* `PIRoffSec` - number of seconds after PIR sensor deactivates when usermod triggers Off preset (or turns WLED off) +* `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` - 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 + + +Have fun - @gegu & @blazoncek + +## Change log +2021-04 +* Adaptation for runtime configuration. + +2021-11 +* Added information about dynamic configuration options +* Added option to temporary enable/disble 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 6d71a426..8a4b9a60 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -1,249 +1,553 @@ -#pragma once - -#include "wled.h" - -/* - * 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 { - private: - // PIR sensor pin - const uint8_t PIRsensorPin = 13; // D7 on D1 mini - // notification mode for colorUpdated() - const byte NotifyUpdateMode = NOTIFIER_CALL_MODE_NO_NOTIFY; // NOTIFIER_CALL_MODE_DIRECT_CHANGE - // delay before switch off after the sensor state goes LOW - uint32_t m_switchOffDelay = 600000; - // off timer start time - uint32_t m_offTimerStart = 0; - // current PIR sensor pin state - byte m_PIRsensorPinState = LOW; - // PIR sensor enabled - ISR attached - bool m_PIRenabled = true; - - /* - * return or change if new PIR sensor state is available - */ - static volatile bool newPIRsensorState(bool changeState = false, bool newState = false) { - static volatile bool s_PIRsensorState = false; - if (changeState) { - s_PIRsensorState = newState; - } - return s_PIRsensorState; - } - - /* - * PIR sensor state has changed - */ - static void IRAM_ATTR ISR_PIRstateChange() { - newPIRsensorState(true, true); - } - - /* - * switch strip on/off - */ - void switchStrip(bool switchOn) { - if (switchOn && bri == 0) { - bri = briLast; - colorUpdated(NotifyUpdateMode); - } - else if (!switchOn && bri != 0) { - briLast = bri; - bri = 0; - colorUpdated(NotifyUpdateMode); - } - } - - /* - * Read and update PIR sensor state. - * Initilize/reset switch off timer - */ - bool updatePIRsensorState() { - if (newPIRsensorState()) { - m_PIRsensorPinState = digitalRead(PIRsensorPin); - - if (m_PIRsensorPinState == HIGH) { - m_offTimerStart = 0; - switchStrip(true); - } - else if (bri != 0) { - // start switch off timer - m_offTimerStart = millis(); - } - newPIRsensorState(true, false); - return true; - } - return false; - } - - /* - * switch off the strip if the delay has elapsed - */ - bool handleOffTimer() { - if (m_offTimerStart > 0 && millis() - m_offTimerStart > m_switchOffDelay) { - switchStrip(false); - m_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() { - // PIR Sensor mode INPUT_PULLUP - pinMode(PIRsensorPin, INPUT_PULLUP); - // assign interrupt function and set CHANGE mode - attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); - } - - - /* - * 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() { - 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) - { - //this code adds "u":{"⏲ PIR sensor state":uiDomString} to the info object - // the value contains a button to toggle the sensor enabled/disabled - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray("⏲ PIR sensor state"); //name - String uiDomString = ""; - infoArr.add(uiDomString); //value - - //this code adds "u":{"⏲ switch off timer":uiDomString} to the info object - infoArr = user.createNestedArray("⏲ switch off timer"); //name - - // off timer - if (m_offTimerStart > 0) { - uiDomString = ""; - unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; - if (offSeconds >= 3600) { - uiDomString += (offSeconds / 3600); - uiDomString += " hours "; - offSeconds %= 3600; - } - if (offSeconds >= 60) { - uiDomString += (offSeconds / 60); - offSeconds %= 60; - } else if (uiDomString.length() > 0){ - uiDomString += 0; - } - if (uiDomString.length() > 0){ - uiDomString += " min "; - } - uiDomString += (offSeconds); - infoArr.add(uiDomString + " sec"); - } else { - infoArr.add("inactive"); - } - } - - - /* - * 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 - * Add "PIRenabled" to json state. This can be used to disable/enable the sensor. - * Add "PIRoffSec" to json state. This can be used to adjust milliseconds . - */ - void addToJsonState(JsonObject& root) - { - root["PIRenabled"] = m_PIRenabled; - root["PIRoffSec"] = (m_switchOffDelay / 1000); - } - - - /* - * 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 "PIRenabled" from json state and switch enable/disable the PIR sensor. - * Read "PIRoffSec" from json state and adjust milliseconds . - */ - void readFromJsonState(JsonObject& root) - { - if (root["PIRoffSec"] != nullptr) { - m_switchOffDelay = (1000 * max(60UL, min(43200UL, root["PIRoffSec"].as()))); - } - - if (root["PIRenabled"] != nullptr) { - if (root["PIRenabled"] && !m_PIRenabled) { - attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); - newPIRsensorState(true, true); - } - else if(m_PIRenabled) { - detachInterrupt(PIRsensorPin); - } - m_PIRenabled = root["PIRenabled"]; - } - } - - - /* - * 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; - } - - //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! -}; +#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. + * Initilize/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 new file mode 100644 index 00000000..1fbfe0e6 --- /dev/null +++ b/usermods/PWM_fan/readme.md @@ -0,0 +1,45 @@ +# PWM fan + +v2 Usermod to to control PWM fan with RPM feedback and temperature control + +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 measured temperature is 3°C greater than the threshold temperature, the fan will run at 100%. + +If the _tachometer_ is supported, the current speed (in RPM) will be displayed on the WLED Info page. + +## Installation + +Add the compile-time option `-D USERMOD_PWM_FAN` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_PWM_FAN` in `myconfig.h`. +You will also need `-D USERMOD_DALLASTEMPERATURE`. + +### Define Your Options + +All of the parameters are configured during run-time using Usermods settings page. +This includes: + +* 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 + +_NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency to match PWM fan sampling frequency. + +### PlatformIO requirements + +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 new file mode 100644 index 00000000..f7fe0e10 --- /dev/null +++ b/usermods/PWM_fan/usermod_PWM_fan.h @@ -0,0 +1,394 @@ +#pragma once + +#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" + +// PWM & tacho code curtesy of @KlausMu +// 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; +// Interrupt counting every rotation of the fan +// https://desire.giesecke.tk/index.php/2018/01/30/change-global-variables-from-isr/ +static void IRAM_ATTR rpm_fan() { + counter_rpm++; +} + + +class PWMFanUsermod : public Usermod { + + private: + + bool initDone = false; + bool enabled = true; + unsigned long msLastTachoMeasurement = 0; + uint16_t last_rpm = 0; + #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 = TACHO_PIN; + int8_t pwmPin = PWM_PIN; + uint8_t tachoUpdateSec = 30; + float targetTemperature = 35.0; + uint8_t minPWMValuePct = 0; + 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; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _tachoPin[]; + static const char _pwmPin[]; + static const char _temperature[]; + static const char _tachoUpdateSec[]; + static const char _minPWMValuePct[]; + 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)){ + tachoPin = -1; + return; + } + pinMode(tachoPin, INPUT); + digitalWrite(tachoPin, HIGH); + attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); + DEBUG_PRINTLN(F("Tacho sucessfully initialized.")); + } + + void deinitTacho(void) { + if (tachoPin < 0) return; + detachInterrupt(digitalPinToInterrupt(tachoPin)); + pinManager.deallocatePin(tachoPin, PinOwner::UM_Unspecified); + tachoPin = -1; + } + + void updateTacho(void) { + // store milliseconds when tacho was measured the last time + msLastTachoMeasurement = millis(); + if (tachoPin < 0) return; + + // start of tacho measurement + // detach interrupt while calculating rpm + detachInterrupt(digitalPinToInterrupt(tachoPin)); + // calculate rpm + last_rpm = (counter_rpm * 60) / numberOfInterrupsInOneSingleRotation; + last_rpm /= tachoUpdateSec; + // reset counter + counter_rpm = 0; + // attach interrupt again + attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); + } + + // 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; + } + + #ifdef ESP8266 + analogWriteRange(255); + analogWriteFreq(WLED_PWM_FREQ); + #else + pwmChannel = pinManager.allocateLedc(1); + if (pwmChannel == 255) { //no more free LEDC channels + deinitPWMfan(); return; + } + // configure LED PWM functionalitites + ledcSetup(pwmChannel, 25000, 8); + // attach the channel to the GPIO to be controlled + ledcAttachPin(pwmPin, pwmChannel); + #endif + DEBUG_PRINTLN(F("Fan PWM sucessfully initialized.")); + } + + void deinitPWMfan(void) { + if (pwmPin < 0) return; + + pinManager.deallocatePin(pwmPin, PinOwner::UM_Unspecified); + #ifdef ARDUINO_ARCH_ESP32 + pinManager.deallocateLedc(pwmChannel, 1); + #endif + pwmPin = -1; + } + + void updateFanSpeed(uint8_t pwmValue){ + if (!enabled || pwmPin < 0) return; + + #ifdef ESP8266 + analogWrite(pwmPin, pwmValue); + #else + ledcWrite(pwmChannel, pwmValue); + #endif + } + + float getActualTemperature(void) { + #if defined(USERMOD_DALLASTEMPERATURE) || defined(USERMOD_SHT) + if (tempUM != nullptr) + return tempUM->getTemperatureC(); + #endif + return -127.0f; + } + + 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; + + if ((temp == NAN) || (temp <= -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; + } + updateFanSpeed(newPWMvalue); + } + + public: + + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() { + #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(); + updateFanSpeed((minPWMValuePct * 255) / 100); // inital fan speed + initDone = true; + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() {} + + /* + * Da loop. + */ + void loop() { + if (!enabled || strip.isUpdating()) return; + + unsigned long now = millis(); + if ((now - msLastTachoMeasurement) < (tachoUpdateSec * 1000)) return; + + updateTacho(); + if (!lockFan) setFanPWMbasedOnTemperature(); + } + + /* + * 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); + + 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")); + } + } + } + + /* + * 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(); + 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. + * 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 addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_pwmPin)] = pwmPin; + top[FPSTR(_tachoPin)] = tachoPin; + top[FPSTR(_tachoUpdateSec)] = tachoUpdateSec; + top[FPSTR(_temperature)] = targetTemperature; + top[FPSTR(_minPWMValuePct)] = minPWMValuePct; + top[FPSTR(_IRQperRotation)] = numberOfInterrupsInOneSingleRotation; + DEBUG_PRINTLN(F("Autosave 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 :) + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject& root) { + int8_t newTachoPin = tachoPin; + int8_t newPwmPin = pwmPin; + + JsonObject top = root[FPSTR(_name)]; + DEBUG_PRINT(FPSTR(_name)); + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_enabled)] | enabled; + newTachoPin = top[FPSTR(_tachoPin)] | newTachoPin; + newPwmPin = top[FPSTR(_pwmPin)] | newPwmPin; + tachoUpdateSec = top[FPSTR(_tachoUpdateSec)] | tachoUpdateSec; + tachoUpdateSec = (uint8_t) max(1,(int)tachoUpdateSec); // bounds checking + targetTemperature = top[FPSTR(_temperature)] | targetTemperature; + minPWMValuePct = top[FPSTR(_minPWMValuePct)] | minPWMValuePct; + minPWMValuePct = (uint8_t) min(100,max(0,(int)minPWMValuePct)); // bounds checking + numberOfInterrupsInOneSingleRotation = top[FPSTR(_IRQperRotation)] | numberOfInterrupsInOneSingleRotation; + numberOfInterrupsInOneSingleRotation = (uint8_t) max(1,(int)numberOfInterrupsInOneSingleRotation); // bounds checking + + if (!initDone) { + // first run: reading from cfg.json + tachoPin = newTachoPin; + pwmPin = newPwmPin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing paramters from settings page + if (tachoPin != newTachoPin || pwmPin != newPwmPin) { + DEBUG_PRINTLN(F("Re-init pins.")); + // deallocate pin and release interrupts + deinitTacho(); + deinitPWMfan(); + tachoPin = newTachoPin; + pwmPin = newPwmPin; + // initialise + setup(); + } + } + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_IRQperRotation)].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_PWM_FAN; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char PWMFanUsermod::_name[] PROGMEM = "PWM-fan"; +const char PWMFanUsermod::_enabled[] PROGMEM = "enabled"; +const char PWMFanUsermod::_tachoPin[] PROGMEM = "tacho-pin"; +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::_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 60fc31f7..00000000 --- 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 5b4e2e5c..00000000 --- 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 new file mode 100644 index 00000000..0add4efc --- /dev/null +++ b/usermods/RTC/readme.md @@ -0,0 +1,8 @@ +# DS1307/DS3231 Real time clock + +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 + +Add the build flag `-D USERMOD_RTC` to your platformio environment. diff --git a/usermods/RTC/usermod_rtc.h b/usermods/RTC/usermod_rtc.h new file mode 100644 index 00000000..42965e3a --- /dev/null +++ b/usermods/RTC/usermod_rtc.h @@ -0,0 +1,51 @@ +#pragma once + +#include "src/dependencies/time/DS1307RTC.h" +#include "wled.h" + +//Connect DS1307 to standard I2C pins (ESP32: GPIO 21 (SDA)/GPIO 22 (SCL)) + +class RTCUsermod : public Usermod { + private: + unsigned long lastTime = 0; + bool disabled = false; + public: + + void setup() { + 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); + updateLocalTime(); + } else { + if (!RTC.chipPresent()) disabled = true; //don't waste time if H/W error + } + } + + void loop() { + 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 + } + } + + /* + * 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) + * 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(i2c_scl); +// pins.add(i2c_sda); +// } + + uint16_t getId() + { + return USERMOD_ID_RTC; + } +}; \ No newline at end of file diff --git a/usermods/RelayBlinds/index.htm b/usermods/RelayBlinds/index.htm new file mode 100644 index 00000000..048cff01 --- /dev/null +++ b/usermods/RelayBlinds/index.htm @@ -0,0 +1,76 @@ + + + + + Blinds + + + + + + + + + + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/usermods/RelayBlinds/presets.json b/usermods/RelayBlinds/presets.json new file mode 100644 index 00000000..95b58715 --- /dev/null +++ b/usermods/RelayBlinds/presets.json @@ -0,0 +1 @@ +{"0":{},"2":{"n":"▲","win":"U0=2"},"1":{"n":"▼","win":"U0=1"}} \ No newline at end of file diff --git a/usermods/RelayBlinds/readme.md b/usermods/RelayBlinds/readme.md new file mode 100644 index 00000000..8c533dd4 --- /dev/null +++ b/usermods/RelayBlinds/readme.md @@ -0,0 +1,8 @@ +# RelayBlinds usermod + +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. +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/RelayBlinds/usermod.cpp b/usermods/RelayBlinds/usermod.cpp new file mode 100644 index 00000000..ee61b0cc --- /dev/null +++ b/usermods/RelayBlinds/usermod.cpp @@ -0,0 +1,83 @@ +#include "wled.h" + +//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) + +//gets called once at boot. Do all initialization that doesn't depend on network here +void userSetup() +{ + +} + +//gets called every time WiFi is (re-)connected. Initialize own network interfaces here +void userConnected() +{ + +} + +/* + * Physical IO + */ +#define PIN_UP_RELAY 4 +#define PIN_DN_RELAY 5 +#define PIN_ON_TIME 500 +bool upActive = false, upActiveBefore = false, downActive = false, downActiveBefore = false; +unsigned long upStartTime = 0, downStartTime = 0; + +void handleRelay() +{ + //up and down relays + if (userVar0) { + upActive = true; + if (userVar0 == 1) { + upActive = false; + downActive = true; + } + userVar0 = 0; + } + + if (upActive) + { + if(!upActiveBefore) + { + pinMode(PIN_UP_RELAY, OUTPUT); + digitalWrite(PIN_UP_RELAY, LOW); + upActiveBefore = true; + upStartTime = millis(); + DEBUG_PRINTLN("UPA"); + } + if (millis()- upStartTime > PIN_ON_TIME) + { + upActive = false; + DEBUG_PRINTLN("UPN"); + } + } else if (upActiveBefore) + { + pinMode(PIN_UP_RELAY, INPUT); + upActiveBefore = false; + } + + if (downActive) + { + if(!downActiveBefore) + { + pinMode(PIN_DN_RELAY, OUTPUT); + digitalWrite(PIN_DN_RELAY, LOW); + downActiveBefore = true; + downStartTime = millis(); + } + if (millis()- downStartTime > PIN_ON_TIME) + { + downActive = false; + } + } else if (downActiveBefore) + { + pinMode(PIN_DN_RELAY, INPUT); + downActiveBefore = false; + } +} + +//loop. You can use "if (WLED_CONNECTED)" to check for successful connection +void userLoop() +{ + handleRelay(); +} \ No newline at end of file diff --git a/usermods/SN_Photoresistor/platformio_override.ini b/usermods/SN_Photoresistor/platformio_override.ini new file mode 100644 index 00000000..91bc5de2 --- /dev/null +++ b/usermods/SN_Photoresistor/platformio_override.ini @@ -0,0 +1,16 @@ +; 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 +; +[env:usermod_sn_photoresistor_d1_mini] +extends = env:d1_mini +build_flags = + ${common.build_flags_esp8266} + -D USERMOD_SN_PHOTORESISTOR +lib_deps = ${env.lib_deps} diff --git a/usermods/SN_Photoresistor/readme.md b/usermods/SN_Photoresistor/readme.md new file mode 100644 index 00000000..feacf41a --- /dev/null +++ b/usermods/SN_Photoresistor/readme.md @@ -0,0 +1,30 @@ +# SN_Photoresistor usermod + +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 + +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_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 via the Usermods settings page. + +## Project link + +* [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link + +### 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_sn_photoresistor_d1_mini`. + +## Change Log diff --git a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h new file mode 100644 index 00000000..60861e4c --- /dev/null +++ b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h @@ -0,0 +1,210 @@ +#pragma once + +#include "wled.h" + +//Pin defaults for QuinLed Dig-Uno (A0) +#define PHOTORESISTOR_PIN A0 + +// the frequency to check photoresistor, 10 seconds +#ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL +#define USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL 10000 +#endif + +// how many seconds after boot to take first measurement, 10 seconds +#ifndef USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT +#define USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT 10000 +#endif + +// supplied voltage +#ifndef USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE +#define USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE 5 +#endif + +// 10 bits +#ifndef USERMOD_SN_PHOTORESISTOR_ADC_PRECISION +#define USERMOD_SN_PHOTORESISTOR_ADC_PRECISION 1024.0f +#endif + +// resistor size 10K hms +#ifndef USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE +#define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0f +#endif + +// only report if differance grater than offset value +#ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE +#define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5 +#endif + +class Usermod_SN_Photoresistor : public Usermod +{ +private: + float referenceVoltage = USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE; + float resistorValue = USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE; + float adcPrecision = USERMOD_SN_PHOTORESISTOR_ADC_PRECISION; + int8_t offset = USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE; + + unsigned long readingInterval = USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL; + // set last reading as "40 sec before boot", so first reading is taken after 20 sec + unsigned long lastMeasurement = UINT32_MAX - (USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT); + // flag to indicate we have finished the first getTemperature call + // allows this library to report to the user how long until the first + // measurement + bool getLuminanceComplete = false; + uint16_t lastLDRValue = -1000; + + // flag set at startup + bool disabled = 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 _referenceVoltage[]; + static const char _resistorValue[]; + static const char _adcPrecision[]; + static const char _offset[]; + + bool checkBoundSensor(float newValue, float prevValue, float maxDiff) + { + return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff; + } + + uint16_t getLuminance() + { + // http://forum.arduino.cc/index.php?topic=37555.0 + // https://forum.arduino.cc/index.php?topic=185158.0 + float volts = analogRead(PHOTORESISTOR_PIN) * (referenceVoltage / adcPrecision); + float amps = volts / resistorValue; + float lux = amps * 1000000 * 2.0; + + lastMeasurement = millis(); + getLuminanceComplete = true; + return uint16_t(lux); + } + +public: + void setup() + { + // set pinmode + pinMode(PHOTORESISTOR_PIN, INPUT); + } + + void loop() + { + if (disabled || strip.isUpdating()) + return; + + 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; + } + + uint16_t currentLDRValue = getLuminance(); + if (checkBoundSensor(currentLDRValue, lastLDRValue, offset)) + { + lastLDRValue = currentLDRValue; + +#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(lastLDRValue).c_str()); + } + else + { + DEBUG_PRINTLN("Missing MQTT connection. Not publishing data"); + } + } +#endif + } + + uint16_t getLastLDRValue() + { + return lastLDRValue; + } + + void addToJsonInfo(JsonObject &root) + { + JsonObject user = root[F("u")]; + if (user.isNull()) + user = root.createNestedObject(F("u")); + + JsonArray lux = user.createNestedArray(F("Luminance")); + + if (!getLuminanceComplete) + { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + lux.add((USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - millis()) / 1000); + lux.add(F(" sec until read")); + return; + } + + lux.add(lastLDRValue); + lux.add(F(" lux")); + } + + uint16_t getId() + { + return USERMOD_ID_SN_PHOTORESISTOR; + } + + /** + * addToConfig() (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(_readInterval)] = readingInterval / 1000; + top[FPSTR(_referenceVoltage)] = referenceVoltage; + top[FPSTR(_resistorValue)] = resistorValue; + top[FPSTR(_adcPrecision)] = adcPrecision; + top[FPSTR(_offset)] = offset; + + DEBUG_PRINTLN(F("Photoresistor config saved.")); + } + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + */ + bool readFromConfig(JsonObject &root) + { + // we look for JSON object. + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + disabled = !(top[FPSTR(_enabled)] | !disabled); + readingInterval = (top[FPSTR(_readInterval)] | readingInterval/1000) * 1000; // convert to ms + referenceVoltage = top[FPSTR(_referenceVoltage)] | referenceVoltage; + resistorValue = top[FPSTR(_resistorValue)] | resistorValue; + adcPrecision = top[FPSTR(_adcPrecision)] | adcPrecision; + offset = top[FPSTR(_offset)] | offset; + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(" config (re)loaded.")); + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return true; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char Usermod_SN_Photoresistor::_name[] PROGMEM = "Photoresistor"; +const char Usermod_SN_Photoresistor::_enabled[] PROGMEM = "enabled"; +const char Usermod_SN_Photoresistor::_readInterval[] PROGMEM = "read-interval-s"; +const char Usermod_SN_Photoresistor::_referenceVoltage[] PROGMEM = "supplied-voltage"; +const char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = "resistor-value"; +const char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = "adc-precision"; +const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; \ No newline at end of file diff --git a/usermods/SN_Photoresistor/usermods_list.cpp b/usermods/SN_Photoresistor/usermods_list.cpp new file mode 100644 index 00000000..649e1973 --- /dev/null +++ b/usermods/SN_Photoresistor/usermods_list.cpp @@ -0,0 +1,14 @@ +#include "wled.h" +/* + * Register your v2 usermods here! + */ +#ifdef USERMOD_SN_PHOTORESISTOR +#include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h" +#endif + +void registerUsermods() +{ +#ifdef USERMOD_SN_PHOTORESISTOR + usermods.add(new Usermod_SN_Photoresistor()); +#endif +} \ No newline at end of file diff --git a/usermods/ST7789_display/README.md b/usermods/ST7789_display/README.md new file mode 100644 index 00000000..ebaae492 --- /dev/null +++ b/usermods/ST7789_display/README.md @@ -0,0 +1,77 @@ +# Using the ST7789 TFT IPS 240x240 pixel color display with ESP32 boards + +This usermod enables display of the following: + +* Current date and time; +* Network SSID; +* IP address; +* WiFi signal strength; +* Brightness; +* Selected effect; +* Selected palette; +* Effect speed and intensity; +* Estimated current in mA; + +## Hardware + +*** +![Hardware](images/ST7789_Guide.jpg) + +## Library used + +[Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) + +## Setup + +*** + +### Platformio.ini changes + +In the `platformio.ini` file, uncomment the `TFT_eSPI` line within the [common] section, under `lib_deps`: + +```ini +# platformio.ini +... +[common] +... +lib_deps = + ... + #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line + #TFT_eSPI +... +``` + +In the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows: + +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 saved, the required library files should be automatically downloaded for modifications in a later step. + +### TFT_eSPI Library Adjustments + +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. + +Edit `Setup_ST7789.h` file and uncomment and change GPIO pin numbers in lines containing `TFT_MOSI`, `TFT_SCLK`, `TFT_RST`, `TFT_DC`. + +Modify the `User_Setup_Select.h` by uncommenting the line containing `#include ` and commenting out the line containing `#include `. + +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 new file mode 100644 index 00000000..144cccbf --- /dev/null +++ b/usermods/ST7789_display/ST7789_display.h @@ -0,0 +1,410 @@ +// Credits to @mrVanboy, @gwaland and my dearest friend @westward +// Also for @spiff72 for usermod TTGO-T-Display +// 210217 +#pragma once + +#include "wled.h" +#include +#include + +#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_BL + #define TFT_BL -1 +#endif + +#define USERMOD_ID_ST7789_DISPLAY 97 + +TFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT); // 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; + // needRedraw marks if redraw is required to prevent often redrawing. + bool needRedraw = true; + // Next variables hold the previous known values to determine if redraw is required. + String knownSsid = ""; + IPAddress knownIp; + uint8_t knownBrightness = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + 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 + + /* + * 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() + { + 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); + tft.setTextColor(TFT_RED); + tft.setCursor(60, 100); + tft.setTextDatum(MC_DATUM); + tft.setTextSize(2); + tft.print("Loading..."); + if (TFT_BL >= 0) + { + pinMode(TFT_BL, OUTPUT); // Set backlight pin to output mode + digitalWrite(TFT_BL, HIGH); // Turn backlight on. + } + } + + /* + * 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() { + char buff[LINE_BUFFER_SIZE]; + + // Check if we time interval for redrawing passes. + if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) + { + return; + } + lastUpdate = millis(); + + // Turn off display after 5 minutes with no change. + if (!displayTurnedOff && millis() - lastRedraw > 5*60*1000) + { + 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) || + (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, 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; + + tft.fillScreen(TFT_BLACK); + + 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) + { + tft.setCursor(0, 84); + tft.print("AP IP: "); + tft.print(knownIp); + tft.setCursor(0,108); + tft.print("AP Pass:"); + tft.print(apPass); + } + else + { + 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("%"); + } + + // 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) + { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray lightArr = user.createNestedArray("ST7789"); //name + lightArr.add(enabled?F("installed"):F("disabled")); //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; + } + + + /* + * 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!")); + } + + + /* + * 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 addToConfig(JsonObject& root) + { + 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) + * + * 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 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) + return true; + } + + + /* + * 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_ST7789_DISPLAY; + } + + //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/ST7789_display/images/ST7789_Guide.jpg b/usermods/ST7789_display/images/ST7789_Guide.jpg new file mode 100644 index 00000000..80fee420 Binary files /dev/null and b/usermods/ST7789_display/images/ST7789_Guide.jpg differ diff --git a/usermods/Si7021_MQTT_HA/readme.md b/usermods/Si7021_MQTT_HA/readme.md new file mode 100644 index 00000000..99a240f7 --- /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 00000000..bdf78484 --- /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 5674c466..439f9832 100644 --- a/usermods/TTGO-T-Display/README.md +++ b/usermods/TTGO-T-Display/README.md @@ -1,16 +1,26 @@ # 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 - * in AP mode and turned off lightning AP password is 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 (NOTE: for this to be a reasonable value, the correct LED type must be specified in the LED Prefs section) -Usermod based on a rework of the ssd1306_i2c_oled_u8g2 usermod from the WLED repo. +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. 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. + +Based on a rework of the ssd1306_i2c_oled_u8g2 usermod from the WLED repo. ## Hardware ![Hardware](assets/ttgo_hardware1.png) +![Hardware](assets/ttgo-tdisplay-enclosure1a.png) +![Hardware](assets/ttgo-tdisplay-enclosure2a.png) +![Hardware](assets/ttgo-tdisplay-enclosure3a.png) +![Hardware](assets/ttgo-tdisplay-enclosure3a.png) ## Github reference for TTGO-Tdisplay @@ -20,7 +30,11 @@ 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 full string of 68 LEDs. +* 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). ## Platformio Requirements ### Platformio.ini changes @@ -37,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): @@ -66,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/assets/ttgo-tdisplay-enclosure1a.png b/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure1a.png new file mode 100644 index 00000000..5c2c2bef Binary files /dev/null and b/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure1a.png differ diff --git a/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure2a.png b/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure2a.png new file mode 100644 index 00000000..ac76ade4 Binary files /dev/null and b/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure2a.png differ diff --git a/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure3a.png b/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure3a.png new file mode 100644 index 00000000..21c416f7 Binary files /dev/null and b/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure3a.png differ diff --git a/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure4a.png b/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure4a.png new file mode 100644 index 00000000..a098fc23 Binary files /dev/null and b/usermods/TTGO-T-Display/assets/ttgo-tdisplay-enclosure4a.png differ diff --git a/usermods/TTGO-T-Display/assets/ttgo_hardware1.png b/usermods/TTGO-T-Display/assets/ttgo_hardware1.png index 42d338db..3d2b940d 100644 Binary files a/usermods/TTGO-T-Display/assets/ttgo_hardware1.png and b/usermods/TTGO-T-Display/assets/ttgo_hardware1.png differ diff --git a/usermods/TTGO-T-Display/platformio_override.ini b/usermods/TTGO-T-Display/platformio_override.ini index 4b176096..7e42d9a5 100644 --- a/usermods/TTGO-T-Display/platformio_override.ini +++ b/usermods/TTGO-T-Display/platformio_override.ini @@ -3,6 +3,6 @@ build_flags = ${common.build_flags_esp32} ; PIN defines - uncomment and change, if needed: ; -D LEDPIN=2 -D BTNPIN=35 -; -D IR_PIN=4 +; -D IRPIN=4 ; -D RLYPIN=12 ; -D RLYMDE=1 diff --git a/usermods/TTGO-T-Display/usermod.cpp b/usermods/TTGO-T-Display/usermod.cpp index a4bb28c8..b126d40a 100644 --- a/usermods/TTGO-T-Display/usermod.cpp +++ b/usermods/TTGO-T-Display/usermod.cpp @@ -56,7 +56,7 @@ void userSetup() { tft.setTextColor(TFT_WHITE); tft.setCursor(1, 10); tft.setTextDatum(MC_DATUM); - tft.setTextSize(2); + tft.setTextSize(3); tft.print("Loading..."); if (TFT_BL > 0) { // TFT_BL has been set in the TFT_eSPI library in the User Setup file TTGO_T_Display.h @@ -110,9 +110,9 @@ void userLoop() { needRedraw = true; } else if (knownBrightness != bri) { needRedraw = true; - } else if (knownMode != strip.getMode()) { + } else if (knownMode != strip.getMainSegment().mode) { needRedraw = true; - } else if (knownPalette != strip.getSegment(0).palette) { + } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } @@ -136,79 +136,60 @@ void userLoop() { #endif knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); knownBrightness = bri; - knownMode = strip.getMode(); - knownPalette = strip.getSegment(0).palette; + knownMode = strip.getMainSegment().mode; + knownPalette = strip.getMainSegment().palette; tft.fillScreen(TFT_BLACK); tft.setTextSize(2); // First row with Wifi name - tft.setCursor(1, 10); + 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 if (knownSsid.length() > tftcharwidth) tft.print("~"); - // Second row with IP or Psssword - tft.setCursor(1, 40); - // Print password in AP mode and if led is OFF. - if (apActive && bri == 0) - tft.print(apPass); - else + // Second row with AP IP and Password or IP + tft.setTextSize(2); + tft.setCursor(1, 24); + // Print AP IP and password in AP mode or knownIP if AP not active. + // if (apActive && bri == 0) + // tft.print(apPass); + // else + // tft.print(knownIp); + + if (apActive) { + tft.print("AP IP: "); tft.print(knownIp); + tft.setCursor(1,46); + tft.print("AP Pass:"); + tft.print(apPass); + } + else { + tft.print("IP: "); + tft.print(knownIp); + tft.setCursor(1,46); + //tft.print("Signal Strength: "); + //tft.print(i.wifi.signal); + tft.print("Brightness: "); + tft.print(((float(bri)/255)*100)); + tft.print("%"); + } // Third row with mode name - tft.setCursor(1, 70); - 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; - } - // Fourth row with palette name - tft.setCursor(1, 100); - 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; - } + tft.setCursor(1, 68); + char lineBuffer[tftcharwidth+1]; + extractModeName(knownMode, JSON_mode_names, lineBuffer, tftcharwidth); + tft.print(lineBuffer); -} \ No newline at end of file + // Fourth row with palette name + tft.setCursor(1, 90); + 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). + tft.print(strip.currentMilliamps); + tft.print("mA (estimated)"); + +} diff --git a/usermods/Temperature/platformio_override.ini b/usermods/Temperature/platformio_override.ini new file mode 100644 index 00000000..0e354da9 --- /dev/null +++ b/usermods/Temperature/platformio_override.ini @@ -0,0 +1,12 @@ +; Options +; ------- +; USERMOD_DALLASTEMPERATURE - define this to have this user mod included wled00\usermods_list.cpp +; USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds +; +[env:d1_mini_usermod_dallas_temperature_C] +extends = env:d1_mini +build_flags = ${common.build_flags_esp8266} -D USERMOD_DALLASTEMPERATURE +lib_deps = ${env.lib_deps} + 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 ef244447..2657c6c8 100644 --- a/usermods/Temperature/readme.md +++ b/usermods/Temperature/readme.md @@ -1,23 +1,35 @@ # 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 will 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 `usermod_temperature.h` to the wled00 directory. -Uncomment the corresponding lines in `usermods_list.h` and compile! -If this is the only v2 usermod you plan to use, you can alternatively replace `usermods_list.h` in wled00 with the one in this folder. +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` - 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 via the Usermods settings page, including pin, temperature in degrees Celsius or Farenheit and measurement interval. ## Project link * [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link +* [Srg74-WLED-Wemos-shield](https://github.com/srg74/WLED-wemos-shield) - another great DIY WLED board ### PlatformIO requirements -You might have to uncomment `DallasTemperature@~3.8.0`,`OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: +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 # platformio.ini @@ -29,12 +41,23 @@ default_envs = d1_mini ... [common] ... -lib_deps_external = +lib_deps = ... - #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 -... + #For Dallas sensor uncomment following line + 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 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. +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 eb123df0..a15baf87 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -1,79 +1,432 @@ #pragma once #include "wled.h" +#include "OneWire.h" -#include //DS18B20 - -//Pin defaults for QuinLed Dig-Uno -#ifdef ARDUINO_ARCH_ESP32 -#define TEMPERATURE_PIN 18 -#else //ESP8266 boards -#define TEMPERATURE_PIN 14 +//Pin defaults for QuinLed Dig-Uno if not overriden +#ifndef TEMPERATURE_PIN + #ifdef ARDUINO_ARCH_ESP32 + #define TEMPERATURE_PIN 18 + #else //ESP8266 boards + #define TEMPERATURE_PIN 14 + #endif #endif -#define TEMP_CELSIUS // Comment out for Fahrenheit - -#define MEASUREMENT_INTERVAL 60000 //1 Minute - -OneWire oneWire(TEMPERATURE_PIN); -DallasTemperature sensor(&oneWire); +// the frequency to check temperature, 1 minute +#ifndef USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL +#define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 +#endif class UsermodTemperature : public Usermod { + private: - //set last reading as "40 sec before boot", so first reading is taken after 20 sec - unsigned long lastMeasurement = UINT32_MAX - 40000; - float temperature = 0.0f; + + bool initDone = false; + OneWire *oneWire; + // GPIO pin used for sensor (with a default compile-time fallback) + int8_t temperaturePin = TEMPERATURE_PIN; + // measurement unit (true==°C, false==°F) + 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 + unsigned long lastMeasurement = UINT32_MAX - USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; + // last time requestTemperatures was called + // used to determine when we can read the sensors temperature + // we have to wait at least 93.75 ms after requestTemperatures() is called + unsigned long lastTemperaturesRequest; + float temperature; + // indicates requestTemperatures has been called but the sensor measurement is not complete + bool waitingForConversion = false; + // flag set at startup if DS18B20 sensor not found, avoids trying to keep getting + // temperature if flashed to a board without a sensor attached + byte sensorFound; + + 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(); + void requestTemperatures(); + void readTemperature(); + bool findSensor(); +#ifndef WLED_DISABLE_MQTT + void publishHomeAssistantAutodiscovery(); +#endif + public: - void getReading() { - sensor.requestTemperatures(); - #ifdef TEMP_CELSIUS - temperature = sensor.getTempCByIndex(0); - #else - temperature = sensor.getTempFByIndex(0); - #endif + + /* + * API calls te enable data exchange between WLED modules + */ + 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); + + //bool handleButton(uint8_t b); + //void handleOverlayDraw(); + + void addToJsonInfo(JsonObject& root); + //void addToJsonState(JsonObject &root); + //void readFromJsonState(JsonObject &root); + void addToConfig(JsonObject &root); + bool readFromConfig(JsonObject &root); + + void appendConfigData(); +}; + +//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)); } - - void setup() { - sensor.begin(); - sensor.setResolution(9); + #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 loop() { - if (millis() - lastMeasurement > MEASUREMENT_INTERVAL) - { - getReading(); +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; +} - if (WLED_MQTT_CONNECTED) { - char subuf[38]; - strcpy(subuf, mqttDeviceTopic); - strcat(subuf, "/temperature"); - mqtt->publish(subuf, 0, true, String(temperature).c_str()); +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 + +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 } - lastMeasurement = millis(); + } + 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 + } +} - void addToJsonInfo(JsonObject& root) { - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); +/** + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ +//void UsermodTemperature::connected() {} - JsonArray temp = user.createNestedArray("Temperature"); - if (temperature == DEVICE_DISCONNECTED_C) { - temp.add(0); - temp.add(" Sensor Error!"); - return; - } +#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 - temp.add(temperature); - #ifdef TEMP_CELSIUS - temp.add("°C"); - #else - temp.add("°F"); - #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 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(); +} - uint16_t getId() - { - return USERMOD_ID_TEMPERATURE; - } -}; \ No newline at end of file +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 1a1efdd7..00000000 --- a/usermods/Temperature/usermods_list.cpp +++ /dev/null @@ -1,25 +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" -#include "usermod_temperature.h" -//#include "usermod_v2_empty.h" - -void registerUsermods() -{ - /* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //usermods.add(new MyExampleUsermod()); - usermods.add(new UsermodTemperature()); - //usermods.add(new UsermodRenameMe()); -} \ No newline at end of file diff --git a/usermods/VL53L0X_gestures/readme.md b/usermods/VL53L0X_gestures/readme.md new file mode 100644 index 00000000..a230b1a6 --- /dev/null +++ b/usermods/VL53L0X_gestures/readme.md @@ -0,0 +1,29 @@ +# Description + +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 = ${env.build_flags} -D USERMOD_VL53L0X_GESTURES` +3. Add "pololu/VL53L0X" dependency below to `lib_deps` like this: +```ini +lib_deps = ${env.lib_deps} + pololu/VL53L0X @ ^1.3.0 +``` + +My entire `platformio_override.ini` for example (for nodemcu board): +```ini +[platformio] +default_envs = nodemcuv2 + +[env:nodemcuv2] +build_flags = ${env.build_flags} -D USERMOD_VL53L0X_GESTURES +lib_deps = ${env.lib_deps} + pololu/VL53L0X @ ^1.3.0 +``` diff --git a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h b/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h new file mode 100644 index 00000000..fe6b958f --- /dev/null +++ b/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h @@ -0,0 +1,129 @@ +/* + * 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). + * + * 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 = ${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 + */ +#pragma once + +#include "wled.h" + +#include +#include + +#ifndef VL53L0X_MAX_RANGE_MM +#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 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 +#endif + +#ifndef VL53L0X_LONG_MOTION_DELAY_MS +#define VL53L0X_LONG_MOTION_DELAY_MS 1000 // switch onto "long motion" action after this delay +#endif + +class UsermodVL53L0XGestures : public Usermod { + private: + //Private class members. You can declare variables and functions only accessible to your usermod here + unsigned long lastTime = 0; + VL53L0X sensor; + bool enabled = true; + + bool wasMotionBefore = false; + bool isLongMotion = false; + unsigned long motionStartTime = 0; + + public: + + void setup() { + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } + + sensor.setTimeout(150); + if (!sensor.init()) + { + DEBUG_PRINTLN(F("Failed to detect and initialize VL53L0X sensor!")); + } else { + sensor.setMeasurementTimingBudget(20000); // set high speed mode + } + } + + + void loop() { + if (!enabled || strip.isUpdating()) return; + if (millis() - lastTime > VL53L0X_DELAY_MS) + { + lastTime = millis(); + + int range = sensor.readRangeSingleMillimeters(); + DEBUG_PRINTF("range: %d, brightness: %d\r\n", range, bri); + + if (range < VL53L0X_MAX_RANGE_MM) + { + if (!wasMotionBefore) + { + motionStartTime = millis(); + DEBUG_PRINTF("motionStartTime: %d\r\n", motionStartTime); + } + wasMotionBefore = true; + + if (millis() - motionStartTime > VL53L0X_LONG_MOTION_DELAY_MS) //long motion + { + DEBUG_PRINTF("long motion: %d\r\n", motionStartTime); + if (!isLongMotion) + { + 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("new brightness: %d", bri); + stateUpdated(1); + } + } else if (wasMotionBefore) { //released + if (!isLongMotion) + { //short press + DEBUG_PRINTLN(F("shortPressAction...")); + shortPressAction(); + } + wasMotionBefore = false; + isLongMotion = false; + } + } + } + + /* + * 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) + * 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(i2c_scl); +// pins.add(i2c_sda); +// } + + /* + * 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_VL53L0X; + } +}; \ No newline at end of file diff --git a/usermods/Wemos_D1_mini+Wemos32_mini_shield/readme.md b/usermods/Wemos_D1_mini+Wemos32_mini_shield/readme.md index eebc50da..105f2a24 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 a93b20c9..78cc32a8 100644 --- a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp +++ b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp @@ -101,6 +101,7 @@ void userLoop() { if (temptimer - lastMeasure > 60000) { lastMeasure = temptimer; +#ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (mqtt != nullptr) { @@ -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. @@ -137,9 +139,9 @@ void userLoop() { needRedraw = true; } else if (knownBrightness != bri) { needRedraw = true; - } else if (knownMode != strip.getMode()) { + } else if (knownMode != strip.getMainSegment().mode) { needRedraw = true; - } else if (knownPalette != strip.getSegment(0).palette) { + } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } @@ -163,8 +165,8 @@ void userLoop() { #endif knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); knownBrightness = bri; - knownMode = strip.getMode(); - knownPalette = strip.getSegment(0).palette; + knownMode = strip.getMainSegment().mode; + knownPalette = strip.getMainSegment().palette; u8x8.clear(); u8x8.setFont(u8x8_font_chroma48medium8_r); @@ -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; + 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/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp index 15ec58ad..c9d9a527 100644 --- a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp +++ b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp @@ -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. @@ -143,9 +145,9 @@ void userLoop() { needRedraw = true; } else if (knownBrightness != bri) { needRedraw = true; - } else if (knownMode != strip.getMode()) { + } else if (knownMode != strip.getMainSegment().mode) { needRedraw = true; - } else if (knownPalette != strip.getSegment(0).palette) { + } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } @@ -169,8 +171,8 @@ void userLoop() { #endif knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); knownBrightness = bri; - knownMode = strip.getMode(); - knownPalette = strip.getSegment(0).palette; + knownMode = strip.getMainSegment().mode; + knownPalette = strip.getMainSegment().palette; u8x8.clear(); u8x8.setFont(u8x8_font_chroma48medium8_r); @@ -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 00000000..357e612c --- /dev/null +++ b/usermods/audioreactive/audio_reactive.h @@ -0,0 +1,1813 @@ +#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. + * .... + */ + +// 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() +static bool limiterOn = true; // bool: enable / disable dynamics limiter +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 sqare 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 = 10; // 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 tiem 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 reasults 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 +#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT +static float windowWeighingFactors[samplesFFT] = {0.0f}; +#endif + +// Create FFT object +#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT +// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 +#define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc), and an a few other speedups +#define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt +#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 50% on ESP32 (as alternative to FFT_SQRT_APPROXIMATION) +#else +// lib_deps += https://github.com/blazoncek/arduinoFFT.git +#endif +#include + +#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT +static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); +#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 resut 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 + 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 + 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 detetermine 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, smooting, 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.0; + 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]; // spcial 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.42; // 42 is the answer ;-) + currentResult -= 8.0; // this skips the lowest row, giving some room for peaks + if (currentResult > 1.0) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function + else currentResult = 0.0; // 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.0; // 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.0) currentResult = sqrtf(currentResult); + else currentResult = 0.0; // 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; + + // 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 > 1) && (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) + bool enabled = false; + 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 controler. + 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 zome (<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 muliplier 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.0f)) { + // 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 = intgration 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 emergy 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 controler 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 calculatioins. + 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 + if ((binNum < 10) && (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; + 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; + + fftUdp.beginMulticastPacket(); + 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); + 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. + if ((userloopDelay > 23) && !disableSoundProcessing && (audioSyncEnabled == 0)) { + DEBUG_PRINTF("[AR userLoop] hickup 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(); + 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.15 * maxSample5sec) + 0.85 *((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 sucessfully 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.0) { + 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 GPIO config")); + } + } + + // 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"; +const char AudioReactive::_analogmic[] PROGMEM = "analogmic"; +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 00000000..0ba24e8b --- /dev/null +++ b/usermods/audioreactive/audio_source.h @@ -0,0 +1,768 @@ +#pragma once + +#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_ESP32S2) || 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, ESP32-C3 or ESP32-S2. + #else + #warning This audio reactive usermod does not support ESP32-C2, ESP32-C3 or ESP32-S2. + #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) continously 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, 3)) +// 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, 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, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { + 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 ininitialization 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) { + 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 Modude + This is an I2S sound processing unit that requires ininitialization 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 noticable 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) { + + 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, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { + _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(uint8_t i2swsPin, uint8_t i2ssdPin, uint8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { + 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 + } +}; diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md new file mode 100644 index 00000000..d9f9ea78 --- /dev/null +++ b/usermods/audioreactive/readme.md @@ -0,0 +1,73 @@ +# Audioreactive usermod + +Enabless 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 succesfully 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 slighly 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#develop @ 1.9.2` + +## 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) + +**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: continously 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_keypad_controller/assets/bat-key-ctrl-1.jpg b/usermods/battery_keypad_controller/assets/bat-key-ctrl-1.jpg index f352ba3a..459151e8 100644 Binary files a/usermods/battery_keypad_controller/assets/bat-key-ctrl-1.jpg and b/usermods/battery_keypad_controller/assets/bat-key-ctrl-1.jpg differ diff --git a/usermods/battery_keypad_controller/assets/bat-key-ctrl-2.jpg b/usermods/battery_keypad_controller/assets/bat-key-ctrl-2.jpg index 2bf359d3..b89302a8 100644 Binary files a/usermods/battery_keypad_controller/assets/bat-key-ctrl-2.jpg and b/usermods/battery_keypad_controller/assets/bat-key-ctrl-2.jpg differ diff --git a/usermods/battery_keypad_controller/assets/bat-key-ctrl-3.jpg b/usermods/battery_keypad_controller/assets/bat-key-ctrl-3.jpg index 892b13ff..6e2a5bf0 100644 Binary files a/usermods/battery_keypad_controller/assets/bat-key-ctrl-3.jpg and b/usermods/battery_keypad_controller/assets/bat-key-ctrl-3.jpg differ diff --git a/usermods/battery_keypad_controller/wled06_usermod.ino b/usermods/battery_keypad_controller/wled06_usermod.ino index 877713b6..b70682b4 100644 --- a/usermods/battery_keypad_controller/wled06_usermod.ino +++ b/usermods/battery_keypad_controller/wled06_usermod.ino @@ -54,46 +54,38 @@ void userLoop() switch (myKey) { case '1': applyPreset(1); - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); break; case '2': applyPreset(2); - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); break; case '3': applyPreset(3); - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); break; case '4': applyPreset(4); - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); break; case '5': applyPreset(5); - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); break; case '6': applyPreset(6); - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); break; case 'A': applyPreset(7); - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); break; case 'B': applyPreset(8); - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); break; case '7': effectCurrent += 1; if (effectCurrent >= MODE_COUNT) effectCurrent = 0; - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); + colorUpdated(CALL_MODE_FX_CHANGED); break; case '*': effectCurrent -= 1; if (effectCurrent < 0) effectCurrent = (MODE_COUNT-1); - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); + colorUpdated(CALL_MODE_FX_CHANGED); break; case '8': @@ -102,7 +94,7 @@ void userLoop() } else if (effectSpeed < 255) { effectSpeed += 1; } - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); + colorUpdated(CALL_MODE_FX_CHANGED); break; case '0': if (effectSpeed > 15) { @@ -110,7 +102,7 @@ void userLoop() } else if (effectSpeed > 0) { effectSpeed -= 1; } - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); + colorUpdated(CALL_MODE_FX_CHANGED); break; case '9': @@ -119,7 +111,7 @@ void userLoop() } else if (effectIntensity < 255) { effectIntensity += 1; } - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); + colorUpdated(CALL_MODE_FX_CHANGED); break; case '#': if (effectIntensity > 15) { @@ -127,18 +119,18 @@ void userLoop() } else if (effectIntensity > 0) { effectIntensity -= 1; } - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); + colorUpdated(CALL_MODE_FX_CHANGED); break; case 'C': effectPalette += 1; if (effectPalette >= 50) effectPalette = 0; - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); + colorUpdated(CALL_MODE_FX_CHANGED); break; case 'D': effectPalette -= 1; if (effectPalette <= 0) effectPalette = 50; - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); + colorUpdated(CALL_MODE_FX_CHANGED); break; } diff --git a/usermods/blynk_relay_control/README.md b/usermods/blynk_relay_control/README.md deleted file mode 100644 index b6494b46..00000000 --- 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 d4028ea5..00000000 --- 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 00000000..a1e25775 --- /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 00000000..34583009 --- /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/buzzer/usermod_v2_buzzer.h b/usermods/buzzer/usermod_v2_buzzer.h new file mode 100644 index 00000000..ebd8dcb1 --- /dev/null +++ b/usermods/buzzer/usermod_v2_buzzer.h @@ -0,0 +1,81 @@ +#pragma once + +#include "wled.h" +#include "Arduino.h" + +#include + +#define USERMOD_ID_BUZZER 900 +#ifndef USERMOD_BUZZER_PIN +#define USERMOD_BUZZER_PIN GPIO_NUM_32 +#endif + +/* + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * + * 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 BuzzerUsermod : public Usermod { + private: + unsigned long lastTime_ = 0; + unsigned long delay_ = 0; + std::deque> sequence_ {}; + public: + /* + * 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() { + // Setup the pin, and default to LOW + pinMode(USERMOD_BUZZER_PIN, OUTPUT); + digitalWrite(USERMOD_BUZZER_PIN, LOW); + + // Beep on startup + sequence_.push_back({ HIGH, 50 }); + sequence_.push_back({ LOW, 0 }); + } + + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() { + // Double beep on WiFi + sequence_.push_back({ LOW, 100 }); + sequence_.push_back({ HIGH, 50 }); + sequence_.push_back({ LOW, 30 }); + sequence_.push_back({ HIGH, 50 }); + sequence_.push_back({ LOW, 0 }); + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() { + if (sequence_.size() < 1) return; // Wait until there is a sequence + if (millis() - lastTime_ <= delay_) return; // Wait until delay has elapsed + + auto event = sequence_.front(); + sequence_.pop_front(); + + digitalWrite(USERMOD_BUZZER_PIN, event.first); + delay_ = event.second; + + lastTime_ = millis(); + } + + + /* + * 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_BUZZER; + } +}; \ No newline at end of file diff --git a/usermods/mpu6050_imu/readme.md b/usermods/mpu6050_imu/readme.md index adb19ef8..41200415 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 965ab41b..748ddf1a 100644 --- a/usermods/mpu6050_imu/usermod_mpu6050_imu.h +++ b/usermods/mpu6050_imu/usermod_mpu6050_imu.h @@ -55,7 +55,8 @@ void IRAM_ATTR dmpDataReady() { class MPU6050Driver : public Usermod { private: MPU6050 mpu; - + bool enabled = true; + // MPU control/status vars bool dmpReady = false; // set true if DMP init was successful uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU @@ -84,25 +85,24 @@ class MPU6050Driver : public Usermod { * setup() is called once at boot. WiFi is not yet connected at this point. */ void setup() { - // 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 // initialize device - Serial.println(F("Initializing I2C devices...")); + DEBUG_PRINTLN(F("Initializing I2C devices...")); mpu.initialize(); pinMode(INTERRUPT_PIN, INPUT); // verify connection - Serial.println(F("Testing device connections...")); - Serial.println(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed")); + DEBUG_PRINTLN(F("Testing device connections...")); + DEBUG_PRINTLN(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed")); // load and configure the DMP - Serial.println(F("Initializing DMP...")); + DEBUG_PRINTLN(F("Initializing DMP...")); devStatus = mpu.dmpInitialize(); // supply your own gyro offsets here, scaled for min sensitivity @@ -114,16 +114,16 @@ class MPU6050Driver : public Usermod { // make sure it worked (returns 0 if so) if (devStatus == 0) { // turn on the DMP, now that it's ready - Serial.println(F("Enabling DMP...")); + DEBUG_PRINTLN(F("Enabling DMP...")); mpu.setDMPEnabled(true); // enable Arduino interrupt detection - Serial.println(F("Enabling interrupt detection (Arduino external interrupt 0)...")); + DEBUG_PRINTLN(F("Enabling interrupt detection (Arduino external interrupt 0)...")); attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING); mpuIntStatus = mpu.getIntStatus(); // set our DMP Ready flag so the main loop() function knows it's okay to use it - Serial.println(F("DMP ready! Waiting for first interrupt...")); + DEBUG_PRINTLN(F("DMP ready! Waiting for first interrupt...")); dmpReady = true; // get expected DMP packet size for later comparison @@ -133,9 +133,9 @@ class MPU6050Driver : public Usermod { // 1 = initial memory load failed // 2 = DMP configuration updates failed // (if it's going to break, usually the code will be 1) - Serial.print(F("DMP Initialization failed (code ")); - Serial.print(devStatus); - Serial.println(F(")")); + DEBUG_PRINT(F("DMP Initialization failed (code ")); + DEBUG_PRINT(devStatus); + DEBUG_PRINTLN(")"); } } @@ -144,7 +144,7 @@ class MPU6050Driver : public Usermod { * Use it to initialize network interfaces */ void connected() { - //Serial.println("Connected to WiFi!"); + //DEBUG_PRINTLN("Connected to WiFi!"); } @@ -153,7 +153,7 @@ class MPU6050Driver : public Usermod { */ void loop() { // if programming failed, don't try to do anything - if (!dmpReady) return; + if (!enabled || !dmpReady || strip.isUpdating()) return; // wait for MPU interrupt or extra packet(s) available if (!mpuInterrupt && fifoCount < packetSize) return; @@ -169,7 +169,7 @@ class MPU6050Driver : public Usermod { if ((mpuIntStatus & 0x10) || fifoCount == 1024) { // reset so we can continue cleanly mpu.resetFIFO(); - Serial.println(F("FIFO overflow!")); + DEBUG_PRINTLN(F("FIFO overflow!")); // otherwise, check for DMP data ready interrupt (this should happen frequently) } else if (mpuIntStatus & 0x02) { @@ -206,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); @@ -247,22 +247,35 @@ 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) - { - //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); - } - - + //void readFromJsonState(JsonObject& root) + //{ + //if (root["bri"] == 255) DEBUG_PRINTLN(F("Don't burn down your garage!")); + //} + + + /* + * 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) + * 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); +// } + /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). */ @@ -271,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 new file mode 100644 index 00000000..148e4a56 --- /dev/null +++ b/usermods/mqtt_switch_v2/README.md @@ -0,0 +1,54 @@ +# DEPRECATION NOTICE +This usermod is deprecated and no longer maintained. It will be removed in a future WLED release. Please use usermod multi_relay which has more features. + + +# MQTT controllable switches +This usermod allows controlling switches (e.g. relays) via MQTT. + +## Usermod installation + +1. Copy the file `usermod_mqtt_switch.h` to the `wled00` directory. +2. Register the usermod by adding `#include "usermod_mqtt_switch.h"` in the top and `registerUsermod(new UsermodMqttSwitch());` in the bottom of `usermods_list.cpp`. + + +Example `usermods_list.cpp`: + +``` +#include "wled.h" +#include "usermod_mqtt_switch.h" + +void registerUsermods() +{ + usermods.add(new UsermodMqttSwitch()); +} +``` + +## Define pins +Add a define for MQTTSWITCHPINS to platformio_override.ini. +The following example defines 3 switches connected to the GPIO pins 13, 5 and 2: + +``` +[env:livingroom] +board = esp12e +platform = ${common.platform_wled_default} +board_build.ldscript = ${common.ldscript_4m1m} +build_flags = ${common.build_flags_esp8266} + -D LEDPIN=3 + -D BTNPIN=4 + -D RLYPIN=12 + -D RLYMDE=1 + -D STATUSPIN=15 + -D MQTTSWITCHPINS="13, 5, 2" +``` + +Pins can be inverted by setting `MQTTSWITCHINVERT`. For example `-D MQTTSWITCHINVERT="false, false, true"` would invert the switch on pin 2 in the previous example. + +The default state after booting before any MQTT message can be set by `MQTTSWITCHDEFAULTS`. For example `-D MQTTSWITCHDEFAULTS="ON, OFF, OFF"` would power on the switch on pin 13 and power off switches on pins 5 and 2. + +## MQTT topics +This usermod listens on `[mqttDeviceTopic]/switch/0/set` (where 0 is replaced with the index of the switch) for commands. Anything starting with `ON` turns on the switch, everything else turns it off. +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. + diff --git a/usermods/mqtt_switch_v2/usermod_mqtt_switch.h b/usermods/mqtt_switch_v2/usermod_mqtt_switch.h new file mode 100644 index 00000000..67dfc9cc --- /dev/null +++ b/usermods/mqtt_switch_v2/usermod_mqtt_switch.h @@ -0,0 +1,159 @@ +#pragma once + +#warning "This usermod is deprecated and no longer maintained. It will be removed in a future WLED release. Please use usermod multi_relay which has more features." + +#include "wled.h" +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +#ifndef MQTTSWITCHPINS +#error "Please define MQTTSWITCHPINS in platformio_override.ini. e.g. -D MQTTSWITCHPINS="12, 0, 2" " +// The following define helps Eclipse's C++ parser but is never used in production due to the #error statement on the line before +#define MQTTSWITCHPINS 12, 0, 2 +#endif + +// Default behavior: All outputs active high +#ifndef MQTTSWITCHINVERT +#define MQTTSWITCHINVERT +#endif + +// Default behavior: All outputs off +#ifndef MQTTSWITCHDEFAULTS +#define MQTTSWITCHDEFAULTS +#endif + +static const uint8_t switchPins[] = { MQTTSWITCHPINS }; +//This is a hack to get the number of pins defined by the user +#define NUM_SWITCH_PINS (sizeof(switchPins)) +static const bool switchInvert[NUM_SWITCH_PINS] = { MQTTSWITCHINVERT}; +//Make settings in config file more readable +#define ON 1 +#define OFF 0 +static const bool switchDefaults[NUM_SWITCH_PINS] = { MQTTSWITCHDEFAULTS}; +#undef ON +#undef OFF + +class UsermodMqttSwitch: public Usermod +{ +private: + bool mqttInitialized; + bool switchState[NUM_SWITCH_PINS]; + +public: + UsermodMqttSwitch() : + mqttInitialized(false) + { + } + + void setup() + { + for (int pinNr = 0; pinNr < NUM_SWITCH_PINS; pinNr++) { + setState(pinNr, switchDefaults[pinNr]); + pinMode(switchPins[pinNr], OUTPUT); + } + } + + void loop() + { + if (!mqttInitialized) { + mqttInit(); + return; // Try again in next loop iteration + } + } + + void mqttInit() + { + if (!mqtt) + return; + mqtt->onMessage( + std::bind(&UsermodMqttSwitch::onMqttMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6)); + mqtt->onConnect(std::bind(&UsermodMqttSwitch::onMqttConnect, this, std::placeholders::_1)); + mqttInitialized = true; + } + + void onMqttConnect(bool sessionPresent); + + void onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total); + void updateState(uint8_t pinNr); + + void setState(uint8_t pinNr, bool active) + { + if (pinNr > NUM_SWITCH_PINS) + return; + switchState[pinNr] = active; + digitalWrite((char) switchPins[pinNr], (char) (switchInvert[pinNr] ? !active : active)); + updateState(pinNr); + } +}; + +inline void UsermodMqttSwitch::onMqttConnect(bool sessionPresent) +{ + if (mqttDeviceTopic[0] == 0) + return; + + for (int pinNr = 0; pinNr < NUM_SWITCH_PINS; pinNr++) { + char buf[128]; + StaticJsonDocument<1024> json; + sprintf(buf, "%s Switch %d", serverDescription, pinNr + 1); + json[F("name")] = buf; + + sprintf(buf, "%s/switch/%d", mqttDeviceTopic, pinNr); + json["~"] = buf; + strcat(buf, "/set"); + mqtt->subscribe(buf, 0); + + json[F("stat_t")] = "~/state"; + json[F("cmd_t")] = "~/set"; + json[F("pl_off")] = F("OFF"); + json[F("pl_on")] = F("ON"); + + char uid[16]; + sprintf(uid, "%s_sw%d", escapedMac.c_str(), pinNr); + json[F("unique_id")] = uid; + + strcpy(buf, mqttDeviceTopic); + strcat(buf, "/status"); + json[F("avty_t")] = buf; + json[F("pl_avail")] = F("online"); + json[F("pl_not_avail")] = F("offline"); + //TODO: dev + sprintf(buf, "homeassistant/switch/%s/config", uid); + char json_str[1024]; + size_t payload_size = serializeJson(json, json_str); + mqtt->publish(buf, 0, true, json_str, payload_size); + updateState(pinNr); + } +} + +inline void UsermodMqttSwitch::onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) +{ + //Note: Payload is not necessarily null terminated. Check "len" instead. + for (int pinNr = 0; pinNr < NUM_SWITCH_PINS; pinNr++) { + char buf[64]; + sprintf(buf, "%s/switch/%d/set", mqttDeviceTopic, pinNr); + if (strcmp(topic, buf) == 0) { + //Any string starting with "ON" is interpreted as ON, everything else as OFF + setState(pinNr, len >= 2 && payload[0] == 'O' && payload[1] == 'N'); + break; + } + } +} + +inline void UsermodMqttSwitch::updateState(uint8_t pinNr) +{ + if (!mqttInitialized) + return; + + if (pinNr > NUM_SWITCH_PINS) + return; + + char buf[64]; + sprintf(buf, "%s/switch/%d/state", mqttDeviceTopic, pinNr); + if (switchState[pinNr]) { + mqtt->publish(buf, 0, false, "ON"); + } else { + mqtt->publish(buf, 0, false, "OFF"); + } +} diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md new file mode 100644 index 00000000..71a54070 --- /dev/null +++ b/usermods/multi_relay/readme.md @@ -0,0 +1,110 @@ +# Multi Relay + +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 conscutively (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 in JSON format. + +* Status Request: `http://[device-ip]/relays` +* Switch Command: `http://[device-ip]/relays?switch=1,0,1,1` + +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 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: +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 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 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 a relay is switched, a message is published: + +* `wled`/_deviceMAC_/`relay`/`0` `on`|`off` + + +## Usermod installation + +1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `usermods.add(new MultiRelay());` at the bottom of `usermods_list.cpp`. +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 of relays (which is 4) by defining MULTI_RELAY_MAX_RELAYS. + +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_multi_relay.h" + +void registerUsermods() +{ + /* + * Add your usermod class name here + * || || || + * \/ \/ \/ + */ + //usermods.add(new MyExampleUsermod()); + //usermods.add(new UsermodTemperature()); + usermods.add(new MultiRelay()); + +} +``` + +## Configuration + +Usermod can be configured via the Usermods settings page. + +* `enabled` - enable/disable usermod +* `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` - 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. + +Have fun - @blazoncek + +## Change log +2021-04 +* First implementation. + +2021-11 +* Added information about dynamic configuration options +* 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 new file mode 100644 index 00000000..7234df90 --- /dev/null +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -0,0 +1,823 @@ +#pragma once + +#include "wled.h" + +#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) + +#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; + 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; + + +class MultiRelay : public Usermod { + + private: + // array of relays + 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[]; + static const char _enabled[]; + static const char _relay_str[]; + static const char _delay_str[]; + static const char _activeHigh[]; + static const char _external[]; + static const char _button[]; + static const char _broadcast[]; + static const char _HAautodiscovery[]; + static const char _pcf8574[]; + static const char _pcfAddress[]; + + void handleOffTimer(); + void InitHtmlAPIHandle(); + int getValue(String data, char separator, int index); + uint8_t getActiveRelayCount(); + + byte IOexpanderWrite(byte address, byte _data); + byte IOexpanderRead(int address); + + void publishMqtt(int relay); +#ifndef WLED_DISABLE_MQTT + void publishHomeAssistantAutodiscovery(); +#endif + + public: + /** + * constructor + */ + MultiRelay(); + + /** + * desctructor + */ + //~MultiRelay() {} + + /** + * Enable/Disable the usermod + */ + inline void enable(bool enable) { enabled = enable; } + + /** + * Get usermod enabled/disabled state + */ + inline bool isEnabled() { return enabled; } + + /** + * 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. + */ + inline uint16_t getId() { return USERMOD_ID_MULTI_RELAY; } + + /** + * switch relay on/off + */ + void switchRelay(uint8_t relay, bool mode); + + /** + * toggle relay + */ + inline void toggleRelay(uint8_t relay) { + switchRelay(relay, !_relay[relay].state); + } + + /** + * 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 + */ + inline void connected() { InitHtmlAPIHandle(); } + + /** + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop(); + +#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); +}; + + +// class implementetion + +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); + } + } + } 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); + }); +} + +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 ouputs 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() { + yield(); + if (!enabled || strip.isUpdating()) return; + + static unsigned long lastUpdate = 0; + 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::_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 new file mode 100644 index 00000000..f83bb01a --- /dev/null +++ b/usermods/photoresistor_sensor_mqtt_v1/README.md @@ -0,0 +1,13 @@ +# Photoresister sensor with MQTT + +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, otherwise the board hangs. + +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 + +Copy and replace the file `usermod.cpp` in wled00 directory. diff --git a/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp b/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp new file mode 100644 index 00000000..fff7118f --- /dev/null +++ b/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp @@ -0,0 +1,70 @@ +#include "wled.h" +/* + * This v1 usermod 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) + * If you just need 8 bytes, use 2551-2559 (you do not need to increase EEPSIZE) + * + * Consider the v2 usermod API if you need a more advanced feature set! + */ + +//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) + +const int LIGHT_PIN = A0; // define analog pin +const long UPDATE_MS = 30000; // Upper threshold between mqtt messages +const char MQTT_TOPIC[] = "/light"; // MQTT topic for sensor values +const int CHANGE_THRESHOLD = 5; // Change threshold in percentage to send before UPDATE_MS + +// variables +long lastTime = 0; +long timeDiff = 0; +long readTime = 0; +int lightValue = 0; +float lightPercentage = 0; +float lastPercentage = 0; + +//gets called once at boot. Do all initialization that doesn't depend on network here +void userSetup() +{ + pinMode(LIGHT_PIN, INPUT); +} + +//gets called every time WiFi is (re-)connected. Initialize own network interfaces here +void userConnected() +{ + +} + +void publishMqtt(float state) +{ + //Check if MQTT Connected, otherwise it will crash the 8266 + if (mqtt != nullptr){ + char subuf[38]; + strcpy(subuf, mqttDeviceTopic); + strcat(subuf, MQTT_TOPIC); + mqtt->publish(subuf, 0, true, String(state).c_str()); + } +} + +//loop. You can use "if (WLED_CONNECTED)" to check for successful connection +void userLoop() +{ + // Read only every 500ms, otherwise it causes the board to hang + if (millis() - readTime > 500) + { + readTime = millis(); + timeDiff = millis() - lastTime; + + // Convert value to percentage + lightValue = analogRead(LIGHT_PIN); + lightPercentage = ((float)lightValue * -1 + 1024)/(float)1024 *100; + + // Send MQTT message on significant change or after UPDATE_MS + if (abs(lightPercentage - lastPercentage) > CHANGE_THRESHOLD || timeDiff > UPDATE_MS) + { + publishMqtt(lightPercentage); + lastTime = millis(); + lastPercentage = lightPercentage; + } + } +} diff --git a/usermods/project_cars_shiftlight/readme.md b/usermods/project_cars_shiftlight/readme.md index 4490a2ba..433da430 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/project_cars_shiftlight/wled06_usermod.ino b/usermods/project_cars_shiftlight/wled06_usermod.ino index b00c2946..9d3f1d44 100644 --- a/usermods/project_cars_shiftlight/wled06_usermod.ino +++ b/usermods/project_cars_shiftlight/wled06_usermod.ino @@ -5,6 +5,8 @@ * I've had good results with settings around 5 (20 fps). * */ +#include "wled.h" + const uint8_t PCARS_dimcolor = 20; WiFiUDP UDP; const unsigned int PCARS_localUdpPort = 5606; // local port to listen on @@ -18,25 +20,6 @@ u16 PCARS_maxRPM; long PCARS_lastRead = millis() - 2001; float PCARS_rpmRatio; - -void userSetup() -{ - UDP.begin(PCARS_localUdpPort); -} - -void userConnected() -{ - // new wifi, who dis? -} - -void userLoop() -{ - PCARS_readValues(); - if (PCARS_lastRead > millis() - 2000) { - PCARS_buildcolorbars(); - } -} - void PCARS_readValues() { int PCARS_packetSize = UDP.parsePacket(); @@ -48,7 +31,7 @@ void PCARS_readValues() { if (len == 1367) { // Telemetry packet. Ignoring everything else. PCARS_lastRead = millis(); - arlsLock(realtimeTimeoutMs, REALTIME_MODE_GENERIC); + realtimeLock(realtimeTimeoutMs, REALTIME_MODE_GENERIC); // current RPM memcpy(&PCARS_tempChar, &PCARS_packet[124], 2); PCARS_RPM = (PCARS_tempChar[1] << 8) + PCARS_tempChar[0]; @@ -68,11 +51,12 @@ void PCARS_readValues() { void PCARS_buildcolorbars() { boolean activated = false; float ledratio = 0; + uint16_t totalLen = strip.getLengthTotal(); - for (uint16_t i = 0; i < ledCount; i++) { + for (uint16_t i = 0; i < totalLen; i++) { if (PCARS_rpmRatio < .95 || (millis() % 100 > 70 )) { - ledratio = (float)i / (float)ledCount; + ledratio = (float)i / (float)totalLen; if (ledratio < PCARS_rpmRatio) { activated = true; } else { @@ -93,4 +77,22 @@ void PCARS_buildcolorbars() { } colorUpdated(5); strip.show(); +} + +void userSetup() +{ + UDP.begin(PCARS_localUdpPort); +} + +void userConnected() +{ + // new wifi, who dis? +} + +void userLoop() +{ + PCARS_readValues(); + if (PCARS_lastRead > millis() - 2000) { + PCARS_buildcolorbars(); + } } \ No newline at end of file diff --git a/usermods/pwm_outputs/readme.md b/usermods/pwm_outputs/readme.md new file mode 100644 index 00000000..0309ad36 --- /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 00000000..1880308c --- /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/quinled-an-penta.h b/usermods/quinled-an-penta/quinled-an-penta.h new file mode 100644 index 00000000..5153ee58 --- /dev/null +++ b/usermods/quinled-an-penta/quinled-an-penta.h @@ -0,0 +1,755 @@ +#pragma once + +#include "U8g2lib.h" +#include "SHT85.h" +#include "Wire.h" +#include "wled.h" + +class QuinLEDAnPentaUsermod : public Usermod +{ + private: + bool enabled = false; + bool firstRunDone = false; + bool initDone = false; + U8G2 *oledDisplay = nullptr; + SHT *sht30TempHumidSensor; + + // Network info vars + bool networkHasChanged = false; + bool lastKnownNetworkConnected; + IPAddress lastKnownIp; + bool lastKnownWiFiConnected; + String lastKnownSsid; + bool lastKnownApActive; + char *lastKnownApSsid; + char *lastKnownApPass; + byte lastKnownApChannel; + int lastKnownEthType; + bool lastKnownEthLinkUp; + + // Brightness / LEDC vars + byte lastKnownBri = 0; + int8_t currentBussesNumPins[5] = {0, 0, 0, 0, 0}; + int8_t currentLedPins[5] = {0, 0, 0, 0, 0}; + uint8_t currentLedcReads[5] = {0, 0, 0, 0, 0}; + uint8_t lastKnownLedcReads[5] = {0, 0, 0, 0, 0}; + + // OLED vars + bool oledEnabled = false; + bool oledInitDone = false; + bool oledUseProgressBars = false; + bool oledFlipScreen = false; + bool oledFixBuggedScreen = false; + byte oledMaxPage = 3; + byte oledCurrentPage = 3; // Start with the network page to help identifying the IP + byte oledSecondsPerPage = 10; + unsigned long oledLogoDrawn = 0; + unsigned long oledLastTimeUpdated = 0; + unsigned long oledLastTimePageChange = 0; + unsigned long oledLastTimeFixBuggedScreen = 0; + + // SHT30 vars + bool shtEnabled = false; + bool shtInitDone = false; + bool shtReadDataSuccess = false; + byte shtI2cAddress = 0x44; + unsigned long shtLastTimeUpdated = 0; + bool shtDataRequested = false; + float shtCurrentTemp = 0; + float shtLastKnownTemp = 0; + float shtCurrentHumidity = 0; + float shtLastKnownHumidity = 0; + + // Pin/IO vars + const int8_t anPentaLEDPins[5] = {14, 13, 12, 4, 2}; + int8_t oledSpiClk = 15; + int8_t oledSpiData = 16; + int8_t oledSpiCs = 27; + int8_t oledSpiDc = 32; + int8_t oledSpiRst = 33; + int8_t shtSda = 1; + int8_t shtScl = 3; + + + bool isAnPentaLedPin(int8_t pin) + { + for(int8_t i = 0; i <= 4; i++) + { + if(anPentaLEDPins[i] == pin) + return true; + } + return false; + } + + void getCurrentUsedLedPins() + { + for (int8_t lp = 0; lp <= 4; lp++) currentLedPins[lp] = 0; + byte numBusses = busses.getNumBusses(); + byte numUsedPins = 0; + + for (int8_t b = 0; b < numBusses; b++) { + Bus* curBus = busses.getBus(b); + if (curBus != nullptr) { + uint8_t pins[5] = {0, 0, 0, 0, 0}; + currentBussesNumPins[b] = curBus->getPins(pins); + for (int8_t p = 0; p < currentBussesNumPins[b]; p++) { + if (isAnPentaLedPin(pins[p])) { + currentLedPins[numUsedPins] = pins[p]; + numUsedPins++; + } + } + } + } + } + + void getCurrentLedcValues() + { + byte numBusses = busses.getNumBusses(); + byte numLedc = 0; + + for (int8_t b = 0; b < numBusses; b++) { + Bus* curBus = busses.getBus(b); + if (curBus != nullptr) { + uint32_t curPixColor = curBus->getPixelColor(0); + uint8_t _data[5] = {255, 255, 255, 255, 255}; + _data[3] = curPixColor >> 24; + _data[0] = curPixColor >> 16; + _data[1] = curPixColor >> 8; + _data[2] = curPixColor; + + for (uint8_t i = 0; i < currentBussesNumPins[b]; i++) { + currentLedcReads[numLedc] = (_data[i] * bri) / 255; + numLedc++; + } + } + } + } + + + void initOledDisplay() + { + PinManagerPinType pins[5] = { { oledSpiClk, true }, { oledSpiData, true }, { oledSpiCs, true }, { oledSpiDc, true }, { oledSpiRst, true } }; + if (!pinManager.allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) { + DEBUG_PRINTF("[%s] OLED pin allocation failed!\n", _name); + oledEnabled = oledInitDone = false; + return; + } + + oledDisplay = (U8G2 *) new U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI(U8G2_R0, oledSpiClk, oledSpiData, oledSpiCs, oledSpiDc, oledSpiRst); + if (oledDisplay == nullptr) { + DEBUG_PRINTF("[%s] OLED init failed!\n", _name); + oledEnabled = oledInitDone = false; + return; + } + + oledDisplay->begin(); + oledDisplay->setBusClock(40 * 1000 * 1000); + oledDisplay->setContrast(10); + oledDisplay->setPowerSave(0); + oledDisplay->setFont(u8g2_font_6x10_tf); + oledDisplay->setFlipMode(oledFlipScreen); + + oledDisplay->firstPage(); + do { + oledDisplay->drawXBMP(0, 16, 128, 36, quinLedLogo); + } while (oledDisplay->nextPage()); + oledLogoDrawn = millis(); + + oledInitDone = true; + } + + void cleanupOledDisplay() + { + if (oledInitDone) { + oledDisplay->clear(); + } + + pinManager.deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta); + pinManager.deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta); + pinManager.deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta); + pinManager.deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta); + pinManager.deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta); + + delete oledDisplay; + + oledEnabled = false; + oledInitDone = false; + } + + bool isOledReady() + { + return oledEnabled && oledInitDone; + } + + void initSht30TempHumiditySensor() + { + PinManagerPinType pins[2] = { { shtSda, true }, { shtScl, true } }; + if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) { + DEBUG_PRINTF("[%s] SHT30 pin allocation failed!\n", _name); + shtEnabled = shtInitDone = false; + return; + } + + TwoWire *wire = new TwoWire(1); + wire->setClock(400000); + + sht30TempHumidSensor = (SHT *) new SHT30(); + sht30TempHumidSensor->begin(shtI2cAddress, wire); + // The SHT lib calls wire.begin() again without the SDA and SCL pins... So call it again here... + wire->begin(shtSda, shtScl); + if (sht30TempHumidSensor->readStatus() == 0xFFFF) { + DEBUG_PRINTF("[%s] SHT30 init failed!\n", _name); + shtEnabled = shtInitDone = false; + return; + } + + shtInitDone = true; + } + + void cleanupSht30TempHumiditySensor() + { + if (shtInitDone) { + sht30TempHumidSensor->reset(); + } + + pinManager.deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta); + pinManager.deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta); + + delete sht30TempHumidSensor; + + shtEnabled = false; + shtInitDone = false; + } + + void cleanup() + { + if (isOledReady()) { + cleanupOledDisplay(); + } + + if (isShtReady()) { + cleanupSht30TempHumiditySensor(); + } + + enabled = false; + } + + bool oledCheckForNetworkChanges() + { + if (lastKnownNetworkConnected != Network.isConnected() || lastKnownIp != Network.localIP() + || lastKnownWiFiConnected != WiFi.isConnected() || lastKnownSsid != WiFi.SSID() + || lastKnownApActive != apActive || lastKnownApSsid != apSSID || lastKnownApPass != apPass || lastKnownApChannel != apChannel) { + lastKnownNetworkConnected = Network.isConnected(); + lastKnownIp = Network.localIP(); + lastKnownWiFiConnected = WiFi.isConnected(); + lastKnownSsid = WiFi.SSID(); + lastKnownApActive = apActive; + lastKnownApSsid = apSSID; + lastKnownApPass = apPass; + lastKnownApChannel = apChannel; + + return networkHasChanged = true; + } + #ifdef WLED_USE_ETHERNET + if (lastKnownEthType != ethernetType || lastKnownEthLinkUp != ETH.linkUp()) { + lastKnownEthType = ethernetType; + lastKnownEthLinkUp = ETH.linkUp(); + + return networkHasChanged = true; + } + #endif + + return networkHasChanged = false; + } + + byte oledGetNextPage() + { + return oledCurrentPage + 1 <= oledMaxPage ? oledCurrentPage + 1 : 1; + } + + void oledShowPage(byte page, bool updateLastTimePageChange = false) + { + oledCurrentPage = page; + updateOledDisplay(); + oledLastTimeUpdated = millis(); + if (updateLastTimePageChange) oledLastTimePageChange = oledLastTimeUpdated; + } + + /* + * Page 1: Overall brightness and LED outputs + * Page 2: General info like temp, humidity and others + * Page 3: Network info + */ + void updateOledDisplay() + { + if (!isOledReady()) return; + + oledDisplay->firstPage(); + do { + oledDisplay->setFont(u8g2_font_chroma48medium8_8r); + oledDisplay->drawStr(0, 8, serverDescription); + oledDisplay->drawHLine(0, 13, 127); + oledDisplay->setFont(u8g2_font_6x10_tf); + + byte charPerRow = 21; + byte oledRow = 23; + switch (oledCurrentPage) { + // LED Outputs + case 1: + { + char charCurrentBrightness[charPerRow+1] = "Brightness:"; + if (oledUseProgressBars) { + oledDisplay->drawStr(0, oledRow, charCurrentBrightness); + // There is no method to draw a filled box with rounded corners. So draw the rounded frame first, then fill that frame accordingly to LED percentage + oledDisplay->drawRFrame(68, oledRow - 6, 60, 7, 2); + oledDisplay->drawBox(69, oledRow - 5, int(round(58*getPercentageForBrightness(bri)) / 100), 5); + } + else { + sprintf(charCurrentBrightness, "%s %d%%", charCurrentBrightness, getPercentageForBrightness(bri)); + oledDisplay->drawStr(0, oledRow, charCurrentBrightness); + } + oledRow += 8; + + byte drawnLines = 0; + for (int8_t app = 0; app <= 4; app++) { + for (int8_t clp = 0; clp <= 4; clp++) { + if (anPentaLEDPins[app] == currentLedPins[clp]) { + char charCurrentLedcReads[17]; + sprintf(charCurrentLedcReads, "LED %d:", app+1); + if (oledUseProgressBars) { + oledDisplay->drawStr(0, oledRow+(drawnLines*8), charCurrentLedcReads); + oledDisplay->drawRFrame(38, oledRow - 6 + (drawnLines * 8), 90, 7, 2); + oledDisplay->drawBox(39, oledRow - 5 + (drawnLines * 8), int(round(88*getPercentageForBrightness(currentLedcReads[clp])) / 100), 5); + } + else { + sprintf(charCurrentLedcReads, "%s %d%%", charCurrentLedcReads, getPercentageForBrightness(currentLedcReads[clp])); + oledDisplay->drawStr(0, oledRow+(drawnLines*8), charCurrentLedcReads); + } + + drawnLines++; + } + } + } + break; + } + + // Various info + case 2: + { + if (isShtReady() && shtReadDataSuccess) { + char charShtCurrentTemp[charPerRow+4]; // Reserve 3 more bytes than usual as we gonna have one UTF8 char which can be up to 4 bytes. + sprintf(charShtCurrentTemp, "Temperature: %.02f°C", shtCurrentTemp); + char charShtCurrentHumidity[charPerRow+1]; + sprintf(charShtCurrentHumidity, "Humidity: %.02f RH", shtCurrentHumidity); + + oledDisplay->drawUTF8(0, oledRow, charShtCurrentTemp); + oledDisplay->drawStr(0, oledRow + 10, charShtCurrentHumidity); + oledRow += 20; + } + + if (mqttEnabled && mqttServer[0] != 0) { + char charMqttStatus[charPerRow+1]; + sprintf(charMqttStatus, "MQTT: %s", (WLED_MQTT_CONNECTED ? "Connected" : "Disconnected")); + oledDisplay->drawStr(0, oledRow, charMqttStatus); + oledRow += 10; + } + + // Always draw these two on the bottom + char charUptime[charPerRow+1]; + sprintf(charUptime, "Uptime: %ds", int(millis()/1000 + rolloverMillis*4294967)); // From json.cpp + oledDisplay->drawStr(0, 53, charUptime); + + char charWledVersion[charPerRow+1]; + sprintf(charWledVersion, "WLED v%s", versionString); + oledDisplay->drawStr(0, 63, charWledVersion); + break; + } + + // Network Info + case 3: + #ifdef WLED_USE_ETHERNET + if (lastKnownEthType == WLED_ETH_NONE) { + oledDisplay->drawStr(0, oledRow, "Ethernet: No board selected"); + oledRow += 10; + } + else if (!lastKnownEthLinkUp) { + oledDisplay->drawStr(0, oledRow, "Ethernet: Link Down"); + oledRow += 10; + } + #endif + + if (lastKnownNetworkConnected) { + #ifdef WLED_USE_ETHERNET + if (lastKnownEthLinkUp) { + oledDisplay->drawStr(0, oledRow, "Ethernet: Link Up"); + oledRow += 10; + } + else + #endif + // Wi-Fi can be active with ETH being connected, but we don't mind... + if (lastKnownWiFiConnected) { + #ifdef WLED_USE_ETHERNET + if (!lastKnownEthLinkUp) { + #endif + + oledDisplay->drawStr(0, oledRow, "Wi-Fi: Connected"); + char currentSsidChar[lastKnownSsid.length() + 1]; + lastKnownSsid.toCharArray(currentSsidChar, lastKnownSsid.length() + 1); + char charCurrentSsid[50]; + sprintf(charCurrentSsid, "SSID: %s", currentSsidChar); + oledDisplay->drawStr(0, oledRow + 10, charCurrentSsid); + oledRow += 20; + + #ifdef WLED_USE_ETHERNET + } + #endif + } + + String currentIpStr = lastKnownIp.toString(); + char currentIpChar[currentIpStr.length() + 1]; + currentIpStr.toCharArray(currentIpChar, currentIpStr.length() + 1); + char charCurrentIp[30]; + sprintf(charCurrentIp, "IP: %s", currentIpChar); + oledDisplay->drawStr(0, oledRow, charCurrentIp); + } + // If WLED AP is active. Theoretically, it can even be active with ETH being connected, but we don't mind... + else if (lastKnownApActive) { + char charCurrentApStatus[charPerRow+1]; + sprintf(charCurrentApStatus, "WLED AP: %s (Ch: %d)", (lastKnownApActive ? "On" : "Off"), lastKnownApChannel); + oledDisplay->drawStr(0, oledRow, charCurrentApStatus); + + char charCurrentApSsid[charPerRow+1]; + sprintf(charCurrentApSsid, "SSID: %s", lastKnownApSsid); + oledDisplay->drawStr(0, oledRow + 10, charCurrentApSsid); + + char charCurrentApPass[charPerRow+1]; + sprintf(charCurrentApPass, "PW: %s", lastKnownApPass); + oledDisplay->drawStr(0, oledRow + 20, charCurrentApPass); + + // IP is hardcoded / no var exists in WLED at the time this mod was coded, so also hardcode it here + oledDisplay->drawStr(0, oledRow + 30, "IP: 4.3.2.1"); + } + + break; + } + } while (oledDisplay->nextPage()); + } + + bool isShtReady() + { + return shtEnabled && shtInitDone; + } + + + public: + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _oledEnabled[]; + static const char _oledUseProgressBars[]; + static const char _oledFlipScreen[]; + static const char _oledSecondsPerPage[]; + static const char _oledFixBuggedScreen[]; + static const char _shtEnabled[]; + static const unsigned char quinLedLogo[]; + + + static int8_t getPercentageForBrightness(byte brightness) + { + return int(((float)brightness / (float)255) * 100); + } + + + /* + * 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) { + lastKnownBri = bri; + + if (oledEnabled) { + initOledDisplay(); + } + + if (shtEnabled) { + initSht30TempHumiditySensor(); + } + + getCurrentUsedLedPins(); + + initDone = true; + } + + firstRunDone = true; + } + + /* + * 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 || !initDone || strip.isUpdating()) return; + + if (isShtReady()) { + if (millis() - shtLastTimeUpdated > 30000 && !shtDataRequested) { + sht30TempHumidSensor->requestData(); + shtDataRequested = true; + + shtLastTimeUpdated = millis(); + } + + if (shtDataRequested) { + if (sht30TempHumidSensor->dataReady()) { + if (sht30TempHumidSensor->readData()) { + shtCurrentTemp = sht30TempHumidSensor->getTemperature(); + shtCurrentHumidity = sht30TempHumidSensor->getHumidity(); + shtReadDataSuccess = true; + } + else { + shtReadDataSuccess = false; + } + + shtDataRequested = false; + } + } + } + + if (isOledReady() && millis() - oledLogoDrawn > 3000) { + // Check for changes on the current page and update the OLED if a change is detected + if (millis() - oledLastTimeUpdated > 150) { + // If there was a network change, force page 3 (network page) + if (oledCheckForNetworkChanges()) { + oledCurrentPage = 3; + } + // Only redraw a page if there was a change for that page + switch (oledCurrentPage) { + case 1: + lastKnownBri = bri; + // Probably causes lag to always do ledcRead(), so rather re-do the math, 'cause we can't easily get it... + getCurrentLedcValues(); + + if (bri != lastKnownBri || lastKnownLedcReads[0] != currentLedcReads[0] || lastKnownLedcReads[1] != currentLedcReads[1] || lastKnownLedcReads[2] != currentLedcReads[2] + || lastKnownLedcReads[3] != currentLedcReads[3] || lastKnownLedcReads[4] != currentLedcReads[4]) { + lastKnownLedcReads[0] = currentLedcReads[0]; lastKnownLedcReads[1] = currentLedcReads[1]; lastKnownLedcReads[2] = currentLedcReads[2]; lastKnownLedcReads[3] = currentLedcReads[3]; lastKnownLedcReads[4] = currentLedcReads[4]; + + oledShowPage(1); + } + break; + + case 2: + if (shtLastKnownTemp != shtCurrentTemp || shtLastKnownHumidity != shtCurrentHumidity) { + shtLastKnownTemp = shtCurrentTemp; + shtLastKnownHumidity = shtCurrentHumidity; + + oledShowPage(2); + } + break; + + case 3: + if (networkHasChanged) { + networkHasChanged = false; + + oledShowPage(3, true); + } + break; + } + } + // Cycle through OLED pages + if (millis() - oledLastTimePageChange > oledSecondsPerPage * 1000) { + // Periodically fixing a "bugged out" OLED. More details in the ReadMe + if (oledFixBuggedScreen && millis() - oledLastTimeFixBuggedScreen > 60000) { + oledDisplay->begin(); + oledLastTimeFixBuggedScreen = millis(); + } + oledShowPage(oledGetNextPage(), true); + } + } + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_oledEnabled)] = oledEnabled; + top[FPSTR(_oledUseProgressBars)] = oledUseProgressBars; + top[FPSTR(_oledFlipScreen)] = oledFlipScreen; + top[FPSTR(_oledSecondsPerPage)] = oledSecondsPerPage; + top[FPSTR(_oledFixBuggedScreen)] = oledFixBuggedScreen; + top[FPSTR(_shtEnabled)] = shtEnabled; + + // Update LED pins on config save + getCurrentUsedLedPins(); + } + + /** + * 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) + { + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); + return false; + } + + bool oldEnabled = enabled; + bool oldOledEnabled = oledEnabled; + bool oldOledFlipScreen = oledFlipScreen; + bool oldShtEnabled = shtEnabled; + + getJsonValue(top[FPSTR(_enabled)], enabled); + getJsonValue(top[FPSTR(_oledEnabled)], oledEnabled); + getJsonValue(top[FPSTR(_oledUseProgressBars)], oledUseProgressBars); + getJsonValue(top[FPSTR(_oledFlipScreen)], oledFlipScreen); + getJsonValue(top[FPSTR(_oledSecondsPerPage)], oledSecondsPerPage); + getJsonValue(top[FPSTR(_oledFixBuggedScreen)], oledFixBuggedScreen); + getJsonValue(top[FPSTR(_shtEnabled)], shtEnabled); + + // 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 (oldOledEnabled != oledEnabled) { + oledEnabled ? initOledDisplay() : cleanupOledDisplay(); + } + else if (oledEnabled && oldOledFlipScreen != oledFlipScreen) { + oledDisplay->clear(); + oledDisplay->setFlipMode(oledFlipScreen); + oledShowPage(oledCurrentPage); + } + + if (oldShtEnabled != shtEnabled) { + shtEnabled ? initSht30TempHumiditySensor() : cleanupSht30TempHumiditySensor(); + } + + DEBUG_PRINTF("[%s] Config (re)loaded\n", _name); + } + + return true; + } + + void addToJsonInfo(JsonObject& root) + { + if (!enabled && !isShtReady()) { + return; + } + + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray jsonTemp = user.createNestedArray("Temperature"); + JsonArray jsonHumidity = user.createNestedArray("Humidity"); + + if (shtLastTimeUpdated == 0 || !shtReadDataSuccess) { + jsonTemp.add(0); + jsonHumidity.add(0); + if (shtLastTimeUpdated == 0) { + jsonTemp.add(" Not read yet"); + jsonHumidity.add(" Not read yet"); + } + else { + jsonTemp.add(" Error"); + jsonHumidity.add(" Error"); + } + + return; + } + + jsonHumidity.add(shtCurrentHumidity); + jsonHumidity.add(" RH"); + + jsonTemp.add(shtCurrentTemp); + jsonTemp.add(" °C"); + } + + /* + * 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_QUINLED_AN_PENTA; + } +}; + +// strings to reduce flash memory usage (used more than twice) +// Config settings +const char QuinLEDAnPentaUsermod::_name[] PROGMEM = "QuinLED-An-Penta"; +const char QuinLEDAnPentaUsermod::_enabled[] PROGMEM = "Enabled"; +const char QuinLEDAnPentaUsermod::_oledEnabled[] PROGMEM = "Enable-OLED"; +const char QuinLEDAnPentaUsermod::_oledUseProgressBars[] PROGMEM = "OLED-Use-Progress-Bars"; +const char QuinLEDAnPentaUsermod::_oledFlipScreen[] PROGMEM = "OLED-Flip-Screen-180"; +const char QuinLEDAnPentaUsermod::_oledSecondsPerPage[] PROGMEM = "OLED-Seconds-Per-Page"; +const char QuinLEDAnPentaUsermod::_oledFixBuggedScreen[] PROGMEM = "OLED-Fix-Bugged-Screen"; +const char QuinLEDAnPentaUsermod::_shtEnabled[] PROGMEM = "Enable-SHT30-Temp-Humidity-Sensor"; +// Other strings + +const unsigned char QuinLEDAnPentaUsermod::quinLedLogo[] PROGMEM = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xFD, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x80, 0xFF, + 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0x07, 0xFE, 0xFF, 0xFF, 0x0F, 0xFC, + 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFC, 0x0F, 0xFE, + 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xE3, 0xFF, 0xA5, 0xFF, 0xFF, 0xFF, + 0x0F, 0xFC, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xE1, 0xFF, + 0x00, 0xF0, 0xE3, 0xFF, 0x0F, 0xFE, 0x1F, 0xFE, 0xFF, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0xE3, 0xFF, 0x00, 0xF0, 0x00, 0xFF, 0x07, 0xFE, 0x1F, 0xFC, + 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, 0xFF, 0x00, 0xF0, 0x00, 0xFE, + 0x07, 0xFF, 0x1F, 0xFC, 0xF0, 0xC7, 0x3F, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, + 0xF1, 0xFF, 0x00, 0xFC, 0x07, 0xFF, 0x1F, 0xFE, 0xF0, 0xC3, 0x1F, 0xFE, + 0x00, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0x30, 0xF8, 0x07, 0xFF, 0x1F, 0xFE, + 0xF0, 0xC3, 0x1F, 0xFE, 0x00, 0xFC, 0xC3, 0xFF, 0xE1, 0xFF, 0xF0, 0xF0, + 0x03, 0xFF, 0x0F, 0x7E, 0xF0, 0xC3, 0x1F, 0x7E, 0x00, 0xF8, 0xE3, 0xFF, + 0xE1, 0xFF, 0xF1, 0xF1, 0x83, 0xFF, 0x0F, 0x7E, 0xF0, 0xC3, 0x1F, 0x7E, + 0x00, 0xF0, 0xC3, 0xFF, 0xE1, 0xFF, 0xF1, 0xE1, 0x83, 0xFF, 0x0F, 0xFE, + 0xF0, 0xC3, 0x1F, 0xFE, 0xF8, 0xF0, 0xC3, 0xFF, 0xA1, 0xFF, 0xF1, 0xE3, + 0x81, 0xFF, 0x0F, 0x7E, 0xF0, 0xC1, 0x1F, 0x7E, 0xF0, 0xF0, 0xC3, 0xFF, + 0x01, 0xF8, 0xE1, 0xC3, 0x83, 0xFF, 0x0F, 0x7F, 0xF8, 0xC3, 0x1F, 0x7E, + 0xF8, 0xF0, 0xC3, 0xFF, 0x03, 0xF8, 0xE1, 0xC7, 0x81, 0xE4, 0x0F, 0x7F, + 0xF0, 0xC3, 0x1F, 0xFE, 0xF8, 0xF0, 0xC3, 0xFF, 0x01, 0xF8, 0xE3, 0xC7, + 0x01, 0xC0, 0x07, 0x7F, 0xF8, 0xC1, 0x1F, 0x7E, 0xF0, 0xE1, 0xC3, 0xFF, + 0xC3, 0xFD, 0xE1, 0x87, 0x01, 0x00, 0x07, 0x7F, 0xF8, 0xC3, 0x1F, 0x7E, + 0xF8, 0xF0, 0xC3, 0xFF, 0xE3, 0xFF, 0xE3, 0x87, 0x01, 0x00, 0x82, 0x3F, + 0xF8, 0xE1, 0x1F, 0xFE, 0xF8, 0xE1, 0xC3, 0xFF, 0xC3, 0xFF, 0xC3, 0x87, + 0x01, 0x00, 0x80, 0x3F, 0xF8, 0xC1, 0x1F, 0x7E, 0xF0, 0xF1, 0xC3, 0xFF, + 0xC3, 0xFF, 0xC3, 0x87, 0x03, 0x0F, 0x80, 0x3F, 0xF8, 0xE1, 0x0F, 0x7E, + 0xF8, 0xE1, 0x87, 0xFF, 0xC3, 0xFF, 0xC7, 0x87, 0x03, 0x04, 0xC0, 0x7F, + 0xF0, 0xE1, 0x0F, 0xFF, 0xF8, 0xF1, 0x87, 0xFF, 0xC3, 0xFF, 0xC3, 0x87, + 0x07, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0x1F, 0x7E, 0xF0, 0xE0, 0xC3, 0xFF, + 0xC7, 0xFF, 0x87, 0x87, 0x0F, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0x0F, 0x7F, + 0xF8, 0xE1, 0x07, 0x80, 0x07, 0xEA, 0x87, 0xC1, 0x0F, 0x00, 0x80, 0xFF, + 0x00, 0xE0, 0x1F, 0x7E, 0xF0, 0xE1, 0x07, 0x00, 0x03, 0x80, 0x07, 0xC0, + 0x7F, 0x00, 0x00, 0xFF, 0x01, 0xE0, 0x1F, 0xFF, 0xF8, 0xE1, 0x07, 0x00, + 0x07, 0x00, 0x07, 0xE0, 0xFF, 0xF7, 0x01, 0xFF, 0x57, 0xF7, 0x9F, 0xFF, + 0xFC, 0xF1, 0x0F, 0x00, 0x07, 0x80, 0x0F, 0xE0, 0xFF, 0xFF, 0x03, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xBF, 0xFE, + 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, +}; \ No newline at end of file diff --git a/usermods/quinled-an-penta/readme.md b/usermods/quinled-an-penta/readme.md new file mode 100644 index 00000000..2338747d --- /dev/null +++ b/usermods/quinled-an-penta/readme.md @@ -0,0 +1,79 @@ +# QuinLED-An-Penta +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 +* "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 QUINLED_AN_PENTA` and the below library dependencies. + +ESP32 (**without** ethernet): +``` +[env:custom_esp32dev_usermod_quinled_an_penta] +extends = env:esp32dev +build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 -D QUINLED_AN_PENTA +lib_deps = ${esp32.lib_deps} + olikraus/U8g2@~2.28.8 + robtillaart/SHT85@~0.2.0 +``` + +ESP32 (**with** ethernet): +``` +[env:custom_esp32dev_usermod_quinled_an_penta] +extends = env:esp32dev +build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D WLED_USE_ETHERNET -D QUINLED_AN_PENTA +lib_deps = ${esp32.lib_deps} + olikraus/U8g2@~2.28.8 + robtillaart/SHT85@~0.2.0 +``` + +## 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. +Note: you _must_ use an **SPI** driven OLED, **not an i2c one**! + +### Limitations combined with Ethernet +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 "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 +Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D QUINLED_AN_PENTA`, you will see the config for it there: +* Enable-OLED: + * What it does: Enables the optional SPI driven OLED that can be mounted to the 7-pin female header. Won't work with Ethernet, read above. + * Possible values: Enabled/Disabled + * Default: Disabled +* OLED-Use-Progress-Bars: + * What it does: Toggle between showing percentage numbers or a progress-bar-like visualization for overall brightness and each LED channels brightness level + * Possible values: Enabled/Disabled + * Default: Disabled +* OLED-Flip-Screen-180: + * What it does: Flips the screen 180° + * Possible values: Enabled/Disabled + * Default: Disabled +* OLED-Seconds-Per-Page: + * 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: + * What it does: Enable this if your OLED flickers after some time. For more info read above under ["My OLED flickers after some time, what should I do?"](#My-OLED-flickers-after-some-time-what-should-I-do) + * Possible values: Enabled/Disabled + * Default: Disabled +* Enable-SHT30-Temp-Humidity-Sensor: + * What it does: Enables the onboard SHT30 temperature and humidity sensor + * Possible values: Enabled/Disabled + * Default: Disabled + +## Change log +2021-12 +* Adjusted IO layout to match An-Penta v1r1 +2021-10 +* First implementation. + +## Credits +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 0c56efae..8aa8d6ab 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 new file mode 100644 index 00000000..ba5aad4d --- /dev/null +++ b/usermods/rgb-rotary-encoder/readme.md @@ -0,0 +1,86 @@ +# RGB Encoder Board + +This usermod-v2 adds support for the awesome RGB Rotary Encoder Board by Adam Zeloof / "Isotope Engineering" to control the overall brightness of your WLED instance: https://github.com/isotope-engineering/RGB-Encoder-Board. A great DIY rotary encoder with 20 tiny SK6805 / "NeoPixel Nano" LEDs. + +https://user-images.githubusercontent.com/3090131/124680599-0180ab80-dec7-11eb-9065-a6d08ebe0287.mp4 + +## Credits +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 + +## 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 and add the buildflag `-D RGB_ROTARY_ENCODER`. + +ESP32: +``` +[env:custom_esp32dev_usermod_rgb_encoder_board] +extends = env:esp32dev +build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 -D RGB_ROTARY_ENCODER +lib_deps = ${esp32.lib_deps} + lennarthennigs/ESP Rotary@^1.5.0 +``` + +ESP8266 / D1 Mini: +``` +[env:custom_d1_mini_usermod_rgb_encoder_board] +extends = env:d1_mini +build_flags = ${common.build_flags_esp8266} -D RGB_ROTARY_ENCODER +lib_deps = ${esp8266.lib_deps} + lennarthennigs/ESP Rotary@^1.5.0 +``` + +## How to connect the board to your ESP +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'll also need power: + +* "vdd": Needs to be connected to **+5V**. +* "gnd": Ground. + +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 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: controls the LED ring +* ea pin: + * Possible values: Any valid and available GPIO + * Default: 15 + * What it does: First of the two rotary encoder pins +* eb pin: + * Possible values: Any valid and available GPIO + * Default: 32 + * What it does: Second of the two rotary encoder pins +* LED Mode: + * Possible values: 1-3 + * Default: 3 + * 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" + * Bottom right is "3" +* LED Brightness: + * Possible values: 1-255 + * Default: 64 + * 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 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 encoders have 20 "clicks" or positions. This value should be set to 100/`number of clicks` + +## Change log +2021-07 +* First implementation. diff --git a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h new file mode 100644 index 00000000..e57641bf --- /dev/null +++ b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h @@ -0,0 +1,343 @@ +#pragma once + +#include "ESPRotary.h" +#include +#include "wled.h" + +class RgbRotaryEncoderUsermod : public Usermod +{ + private: + bool enabled = false; + bool initDone = false; + bool isDirty = false; + BusDigital *ledBus; + /* + * Green - eb - Q4 - 32 + * Red - ea - Q1 - 15 + * Black - sw - Q2 - 12 + */ + ESPRotary *rotaryEncoder; + int8_t ledIo = 3; // GPIO to control the LEDs + int8_t eaIo = 15; // "ea" from RGB Encoder Board + int8_t ebIo = 32; // "eb" from RGB Encoder Board + byte stepsPerClick = 4; // How many "steps" your rotary encoder does per click. This varies per rotary encoder + /* This could vary per rotary encoder: Usually rotary encoders have 20 "clicks". + If yours has less/more, adjust this to: 100% = 20 LEDs * incrementPerClick */ + byte incrementPerClick = 5; + byte ledMode = 3; + byte ledBrightness = 64; + + // This is all needed to calculate the brightness, rotary position, etc. + const byte minPos = 5; // minPos is not zero, because if we want to turn the LEDs off, we use the built-in button ;) + const byte maxPos = 100; // maxPos=100, like 100% + const byte numLeds = 20; + byte lastKnownPos = 0; + + byte currentColors[3]; + byte lastKnownBri = 0; + + + void initRotaryEncoder() + { + PinManagerPinType pins[2] = { { eaIo, false }, { ebIo, false } }; + if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) { + eaIo = -1; + ebIo = -1; + cleanup(); + return; + } + + // I don't know why, but setting the upper bound here does not work. It results into 1717922932 O_o + rotaryEncoder = new ESPRotary(eaIo, ebIo, stepsPerClick, incrementPerClick, maxPos, currentPos, incrementPerClick); + rotaryEncoder->setUpperBound(maxPos); // I have to again set it here and then it works / is actually 100... + + rotaryEncoder->setChangedHandler(RgbRotaryEncoderUsermod::cbRotate); + } + + void initLedBus() + { + byte _pins[5] = {(byte)ledIo, 255, 255, 255, 255}; + BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0); + + ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1); + if (!ledBus->isOk()) { + cleanup(); + return; + } + + ledBus->setBrightness(ledBrightness); + } + + void updateLeds() + { + switch (ledMode) { + case 2: + { + currentColors[0] = 255; currentColors[1] = 0; currentColors[2] = 0; + for (int i = 0; i < currentPos / incrementPerClick - 1; i++) { + ledBus->setPixelColor(i, 0); + } + ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgbw(currentColors)); + for (int i = currentPos / incrementPerClick; i < numLeds; i++) { + ledBus->setPixelColor(i, 0); + } + } + break; + + default: + case 1: + case 3: + // WLED orange (of course), which we will use in mode 1 + currentColors[0] = 255; currentColors[1] = 160; currentColors[2] = 0; + for (int i = 0; i < currentPos / incrementPerClick; i++) { + if (ledMode == 3) { + hsv2rgb((i) / float(numLeds), 1, .25); + } + ledBus->setPixelColor(i, colorFromRgbw(currentColors)); + } + for (int i = currentPos / incrementPerClick; i < numLeds; i++) { + ledBus->setPixelColor(i, 0); + } + break; + } + + isDirty = true; + } + + void cleanup() + { + // Only deallocate pins if we allocated them ;) + if (eaIo != -1) { + pinManager.deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder); + eaIo = -1; + } + if (ebIo != -1) { + pinManager.deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder); + ebIo = -1; + } + + delete rotaryEncoder; + delete ledBus; + + enabled = false; + } + + int getPositionForBrightness() + { + return int(((float)bri / (float)255) * 100); + } + + float fract(float x) { return x - int(x); } + + float mix(float a, float b, float t) { return a + (b - a) * t; } + + void hsv2rgb(float h, float s, float v) { + currentColors[0] = int((v * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255); + currentColors[1] = int((v * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255); + currentColors[2] = int((v * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255); + } + + public: + static byte currentPos; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _ledIo[]; + static const char _eaIo[]; + static const char _ebIo[]; + static const char _ledMode[]; + static const char _ledBrightness[]; + static const char _stepsPerClick[]; + static const char _incrementPerClick[]; + + + static void cbRotate(ESPRotary& r) { + currentPos = r.getPosition(); + } + + /** + * Enable/Disable the usermod + */ + // inline void enable(bool enable) { 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() + { + if (enabled) { + currentPos = getPositionForBrightness(); + lastKnownBri = bri; + + initRotaryEncoder(); + initLedBus(); + + // No updating of LEDs here, as that's sometimes not working; loop() will take care of that + + initDone = true; + } + } + + /* + * 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; + + rotaryEncoder->loop(); + + // If the rotary was changed + if(lastKnownPos != currentPos) { + lastKnownPos = currentPos; + + bri = min(int(round((2.55 * currentPos))), 255); + lastKnownBri = bri; + + updateLeds(); + colorUpdated(CALL_MODE_DIRECT_CHANGE); + } + + // If the brightness is changed not with the rotary, update the rotary + if (bri != lastKnownBri) { + currentPos = lastKnownPos = getPositionForBrightness(); + lastKnownBri = bri; + rotaryEncoder->resetPosition(currentPos); + updateLeds(); + } + + // Update LEDs here in loop to also validate that we can update/show + if (isDirty && ledBus->canShow()) { + isDirty = false; + ledBus->show(); + } + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_ledIo)] = ledIo; + top[FPSTR(_eaIo)] = eaIo; + top[FPSTR(_ebIo)] = ebIo; + top[FPSTR(_ledMode)] = ledMode; + top[FPSTR(_ledBrightness)] = ledBrightness; + top[FPSTR(_stepsPerClick)] = stepsPerClick; + top[FPSTR(_incrementPerClick)] = incrementPerClick; + } + + /** + * 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) + { + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); + return false; + } + + bool oldEnabled = enabled; + int8_t oldLedIo = ledIo; + int8_t oldEaIo = eaIo; + int8_t oldEbIo = ebIo; + byte oldLedMode = ledMode; + byte oldStepsPerClick = stepsPerClick; + byte oldIncrementPerClick = incrementPerClick; + byte oldLedBrightness = ledBrightness; + + getJsonValue(top[FPSTR(_enabled)], enabled); + getJsonValue(top[FPSTR(_ledIo)], ledIo); + getJsonValue(top[FPSTR(_eaIo)], eaIo); + getJsonValue(top[FPSTR(_ebIo)], ebIo); + getJsonValue(top[FPSTR(_stepsPerClick)], stepsPerClick); + getJsonValue(top[FPSTR(_incrementPerClick)], incrementPerClick); + ledMode = top[FPSTR(_ledMode)] > 0 && top[FPSTR(_ledMode)] < 4 ? top[FPSTR(_ledMode)] : ledMode; + ledBrightness = top[FPSTR(_ledBrightness)] > 0 && top[FPSTR(_ledBrightness)] <= 255 ? top[FPSTR(_ledBrightness)] : ledBrightness; + + if (!initDone) { + // First run: reading from cfg.json + // Nothing to do here, will be all done in setup() + } + // Mod was disabled, so run setup() + else if (enabled && enabled != oldEnabled) { + DEBUG_PRINTF("[%s] Usermod has been re-enabled\n", _name); + setup(); + } + // Config has been changed, so adopt to changes + else { + if (!enabled) { + DEBUG_PRINTF("[%s] Usermod has been disabled\n", _name); + cleanup(); + } + else { + DEBUG_PRINTF("[%s] Usermod is enabled\n", _name); + if (ledIo != oldLedIo) { + delete ledBus; + initLedBus(); + } + + if (ledBrightness != oldLedBrightness) { + ledBus->setBrightness(ledBrightness); + isDirty = true; + } + + if (ledMode != oldLedMode) { + updateLeds(); + } + + if (eaIo != oldEaIo || ebIo != oldEbIo || stepsPerClick != oldStepsPerClick || incrementPerClick != oldIncrementPerClick) { + pinManager.deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder); + pinManager.deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder); + + delete rotaryEncoder; + initRotaryEncoder(); + } + } + + DEBUG_PRINTF("[%s] Config (re)loaded\n", _name); + } + + return true; + } + + /* + * 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_RGB_ROTARY_ENCODER; + } + + //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! +}; + +byte RgbRotaryEncoderUsermod::currentPos = 5; +// strings to reduce flash memory usage (used more than twice) +const char RgbRotaryEncoderUsermod::_name[] PROGMEM = "RGB-Rotary-Encoder"; +const char RgbRotaryEncoderUsermod::_enabled[] PROGMEM = "Enabled"; +const char RgbRotaryEncoderUsermod::_ledIo[] PROGMEM = "LED-pin"; +const char RgbRotaryEncoderUsermod::_eaIo[] PROGMEM = "ea-pin"; +const char RgbRotaryEncoderUsermod::_ebIo[] PROGMEM = "eb-pin"; +const char RgbRotaryEncoderUsermod::_ledMode[] PROGMEM = "LED-Mode"; +const char RgbRotaryEncoderUsermod::_ledBrightness[] PROGMEM = "LED-Brightness"; +const char RgbRotaryEncoderUsermod::_stepsPerClick[] PROGMEM = "Steps-per-Click"; +const char RgbRotaryEncoderUsermod::_incrementPerClick[] PROGMEM = "Increment-per-Click"; \ No newline at end of file diff --git a/usermods/rotary_encoder_change_brightness/usermod.cpp b/usermods/rotary_encoder_change_brightness/usermod.cpp deleted file mode 100644 index 8fae6120..00000000 --- a/usermods/rotary_encoder_change_brightness/usermod.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "wled.h" -/* - * 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 - */ - -//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) - -/* -** Rotary Encoder Example -** Use the Sparkfun Rotary Encoder to vary brightness of LED -** -** Sample the encoder at 500Hz using the millis() function -*/ - -int fadeAmount = 5; // how many points to fade the Neopixel with each step -unsigned long currentTime; -unsigned long loopTime; -const int pinA = D6; // DT from encoder -const int pinB = D7; // CLK from encoder - -unsigned char Enc_A; -unsigned char Enc_B; -unsigned char Enc_A_prev = 0; - -//gets called once at boot. Do all initialization that doesn't depend on network here -void userSetup() { - pinMode(pinA, INPUT_PULLUP); - pinMode(pinB, INPUT_PULLUP); - currentTime = millis(); - loopTime = currentTime; -} - -//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() { - currentTime = millis(); // get the current elapsed time - if(currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz - { - int Enc_A = digitalRead(pinA); // Read encoder pins - int Enc_B = digitalRead(pinB); - if((! Enc_A) && (Enc_A_prev)) { // A has gone from high to low - if(Enc_B == HIGH) { // B is high so clockwise - if(bri + fadeAmount <= 255) bri += fadeAmount; // increase the brightness, dont go over 255 - - } else if (Enc_B == LOW) { // B is low so counter-clockwise - if(bri - fadeAmount >= 0) bri -= fadeAmount; // decrease the brightness, dont go below 0 - } - } - Enc_A_prev = Enc_A; // Store value of A for next time - loopTime = currentTime; // Updates loopTime - - //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 - colorUpdated(6); - } -} diff --git a/usermods/rotary_encoder_change_brightness/usermode_rotary_set.h b/usermods/rotary_encoder_change_brightness/usermode_rotary_set.h deleted file mode 100644 index 5c95573d..00000000 --- a/usermods/rotary_encoder_change_brightness/usermode_rotary_set.h +++ /dev/null @@ -1,211 +0,0 @@ -#pragma once - -#include "wled.h" - -//v2 usermod that allows to change brightness and color using a rotary encoder, -//change between modes by pressing a button (many encoder have one included) -class RotaryEncoderSet : public Usermod -{ -private: - //Private class members. You can declare variables and functions only accessible to your usermod here - unsigned long lastTime = 0; - /* -** Rotary Encoder Example -** Use the Sparkfun Rotary Encoder to vary brightness of LED -** -** Sample the encoder at 500Hz using the millis() function -*/ - - int fadeAmount = 5; // how many points to fade the Neopixel with each step - unsigned long currentTime; - unsigned long loopTime; - const int pinA = 5; // DT from encoder - const int pinB = 18; // CLK from encoder - const int pinC = 23; // SW from encoder - unsigned char select_state = 0; // 0 = brightness 1 = color - unsigned char button_state = HIGH; - unsigned char prev_button_state = HIGH; - CRGB fastled_col; - CHSV prim_hsv; - int16_t new_val; - - unsigned char Enc_A; - unsigned char Enc_B; - unsigned char Enc_A_prev = 0; - -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() - { - //Serial.println("Hello from my usermod!"); - pinMode(pinA, INPUT_PULLUP); - pinMode(pinB, INPUT_PULLUP); - pinMode(pinC, INPUT_PULLUP); - currentTime = millis(); - loopTime = currentTime; - } - - /* - * 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() - { - currentTime = millis(); // get the current elapsed time - - if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz - { - button_state = digitalRead(pinC); - if (prev_button_state != button_state) - { - if (button_state == LOW) - { - if (select_state == 1) - { - select_state = 0; - } - else - { - select_state = 1; - } - prev_button_state = button_state; - } - else - { - prev_button_state = button_state; - } - } - int Enc_A = digitalRead(pinA); // Read encoder pins - int Enc_B = digitalRead(pinB); - if ((!Enc_A) && (Enc_A_prev)) - { // A has gone from high to low - if (Enc_B == HIGH) - { // B is high so clockwise - if (select_state == 0) - { - if (bri + fadeAmount <= 255) - bri += fadeAmount; // increase the brightness, dont go over 255 - } - else - { - fastled_col.red = col[0]; - fastled_col.green = col[1]; - fastled_col.blue = col[2]; - prim_hsv = rgb2hsv_approximate(fastled_col); - new_val = (int16_t)prim_hsv.h + fadeAmount; - if (new_val > 255) - new_val -= 255; // roll-over if bigger than 255 - if (new_val < 0) - new_val += 255; // roll-over if smaller than 0 - prim_hsv.h = (byte)new_val; - hsv2rgb_rainbow(prim_hsv, fastled_col); - col[0] = fastled_col.red; - col[1] = fastled_col.green; - col[2] = fastled_col.blue; - } - } - else if (Enc_B == LOW) - { // B is low so counter-clockwise - if (select_state == 0) - { - if (bri - fadeAmount >= 0) - bri -= fadeAmount; // decrease the brightness, dont go below 0 - } - else - { - fastled_col.red = col[0]; - fastled_col.green = col[1]; - fastled_col.blue = col[2]; - prim_hsv = rgb2hsv_approximate(fastled_col); - new_val = (int16_t)prim_hsv.h - fadeAmount; - if (new_val > 255) - new_val -= 255; // roll-over if bigger than 255 - if (new_val < 0) - new_val += 255; // roll-over if smaller than 0 - prim_hsv.h = (byte)new_val; - hsv2rgb_rainbow(prim_hsv, fastled_col); - col[0] = fastled_col.red; - col[1] = fastled_col.green; - col[2] = fastled_col.blue; - } - } - //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 - colorUpdated(NOTIFIER_CALL_MODE_BUTTON); - updateInterfaces() - } - Enc_A_prev = Enc_A; // Store value of A for next time - loopTime = currentTime; // Updates loopTime - } - } - - /* - * 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 - } - */ - - /* - * 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; - } - - /* - * 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!")); - } - - /* - * 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 0xABCD; - } - - //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! -}; diff --git a/usermods/rotary_encoder_change_effect/wled06_usermod.ino b/usermods/rotary_encoder_change_effect/wled06_usermod.ino index f004ba2f..5444ab9f 100644 --- a/usermods/rotary_encoder_change_effect/wled06_usermod.ino +++ b/usermods/rotary_encoder_change_effect/wled06_usermod.ino @@ -39,7 +39,7 @@ void userLoop() { //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 - colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); + colorUpdated(CALL_MODE_FX_CHANGED); lastTime = millis(); } } diff --git a/usermods/sd_card/readme.md b/usermods/sd_card/readme.md new file mode 100644 index 00000000..299b68eb --- /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) | 14 | + | `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) {...} + ``` \ No newline at end of file diff --git a/usermods/sd_card/usermod_sd_card.h b/usermods/sd_card/usermod_sd_card.h new file mode 100644 index 00000000..5dac7915 --- /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 new file mode 100644 index 00000000..d427d3e1 --- /dev/null +++ b/usermods/sensors_to_mqtt/readme.md @@ -0,0 +1,87 @@ +# Send sensor data To Home Assistant + +Publishes BMP280, CCS811 and Si7021 measurements to Home Assistant via MQTT. + +Uses Home Assistant Automatic Device Discovery. + +The use of Home Assistant is not mandatory. The mod will publish sensor values via MQTT just fine without it. + +Uses the MQTT connection set in the WLED web user interface. + +## Maintainer + +twitter.com/mpronk89 + +## Features + +- Reads BMP280, CCS811 and Si7021 senors +- 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: + +`$mqttDeviceTopic` is set in webui of WLED! + +``` +temperature: $mqttDeviceTopic/temperature +pressure: $mqttDeviceTopic/pressure +humidity: $mqttDeviceTopic/humidity +tvoc: $mqttDeviceTopic/tvoc +eCO2: $mqttDeviceTopic/eco2 +IAQ: $mqttDeviceTopic/iaq +``` + +# Installation + +## Hardware + +### Requirements + +1. BMP280/CCS811/Si7021 sensor. E.g. https://aliexpress.com/item/32979998543.html +2. A microcontroller that supports i2c. e.g. esp32 + +### installation + +Attach the sensor to the i2c interface. + +Default PINs esp32: + +``` +SCL_PIN = 22; +SDA_PIN = 21; +``` + +Default PINs ESP8266: + +``` +SCL_PIN = 5; +SDA_PIN = 4; +``` + +## Enable in WLED + +1. Copy `usermod_v2_SensorsToMqtt.h` into the `wled00` directory. +2. Add to `build_flags` in platformio.ini: + +``` + -D USERMOD_SENSORSTOMQTT +``` + +3. And add to `lib_deps` in platformio.ini: + +``` + adafruit/Adafruit BMP280 Library @ 2.1.0 + adafruit/Adafruit CCS811 Library @ 1.0.4 + adafruit/Adafruit Si7021 Library @ 1.4.0 +``` + +The #ifdefs in `usermods_list.cpp` should do the rest + +# Credits + +- Aircoookie for making WLED +- Other usermod creators for example code +- Bouke_Regnerus for https://community.home-assistant.io/t/example-indoor-air-quality-text-sensor-using-ccs811-sensor/125854 +- You, for reading this diff --git a/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h b/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h new file mode 100644 index 00000000..4f51750a --- /dev/null +++ b/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h @@ -0,0 +1,278 @@ +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +#pragma once + +#include "wled.h" +#include +#include +#include +#include +#include + +Adafruit_BMP280 bmp; +Adafruit_Si7021 si7021; +Adafruit_CCS811 ccs811; + +class UserMod_SensorsToMQTT : public Usermod +{ +private: + bool initialized = false; + bool mqttInitialized = false; + float SensorPressure = 0; + float SensorTemperature = 0; + float SensorHumidity = 0; + char *SensorIaq = "Unknown"; + String mqttTemperatureTopic = ""; + String mqttHumidityTopic = ""; + String mqttPressureTopic = ""; + String mqttTvocTopic = ""; + String mqttEco2Topic = ""; + String mqttIaqTopic = ""; + unsigned int SensorTvoc = 0; + unsigned int SensorEco2 = 0; + unsigned long nextMeasure = 0; + + void _initialize() + { + initialized = bmp.begin(BMP280_ADDRESS_ALT); + bmp.setSampling(Adafruit_BMP280::MODE_NORMAL, /* Operating Mode. */ + Adafruit_BMP280::SAMPLING_X16, /* Temp. oversampling */ + Adafruit_BMP280::SAMPLING_X16, /* Pressure oversampling */ + Adafruit_BMP280::FILTER_X16, /* Filtering. */ + Adafruit_BMP280::STANDBY_MS_2000); /* Refresh values every 20 seconds */ + + initialized &= si7021.begin(); + initialized &= ccs811.begin(); + ccs811.setDriveMode(CCS811_DRIVE_MODE_10SEC); /* Refresh values every 10s */ + Serial.print(initialized); + } + + void _mqttInitialize() + { + mqttTemperatureTopic = String(mqttDeviceTopic) + "/temperature"; + mqttPressureTopic = String(mqttDeviceTopic) + "/pressure"; + mqttHumidityTopic = String(mqttDeviceTopic) + "/humidity"; + mqttTvocTopic = String(mqttDeviceTopic) + "/tvoc"; + mqttEco2Topic = String(mqttDeviceTopic) + "/eco2"; + mqttIaqTopic = String(mqttDeviceTopic) + "/iaq"; + + String t = String("homeassistant/sensor/") + mqttClientID + "/temperature/config"; + + _createMqttSensor("temperature", mqttTemperatureTopic, "temperature", "°C"); + _createMqttSensor("pressure", mqttPressureTopic, "pressure", "hPa"); + _createMqttSensor("humidity", mqttHumidityTopic, "humidity", "%"); + _createMqttSensor("tvoc", mqttTvocTopic, "", "ppb"); + _createMqttSensor("eco2", mqttEco2Topic, "", "ppm"); + _createMqttSensor("iaq", mqttIaqTopic, "", ""); + } + + void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) + { + String t = String("homeassistant/sensor/") + mqttClientID + "/" + name + "/config"; + + StaticJsonDocument<300> doc; + + doc["name"] = name; + doc["state_topic"] = 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["identifiers"] = String("wled-sensor-") + mqttClientID; + device["manufacturer"] = "Aircoookie"; + device["model"] = "WLED"; + device["sw_version"] = VERSION; + device["name"] = mqttClientID; + + String temp; + serializeJson(doc, temp); + Serial.println(t); + Serial.println(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } + + void _updateSensorData() + { + SensorTemperature = bmp.readTemperature(); + SensorHumidity = si7021.readHumidity(); + SensorPressure = (bmp.readPressure() / 100.0F); + ccs811.setEnvironmentalData(SensorHumidity, SensorTemperature); + ccs811.readData(); + SensorTvoc = ccs811.getTVOC(); + SensorEco2 = ccs811.geteCO2(); + SensorIaq = _getIaqIndex(SensorHumidity, SensorTvoc, SensorEco2); + + Serial.printf("%f c, %f humidity, %f hPA, %u tvoc, %u Eco2, %s iaq\n", + SensorTemperature, SensorHumidity, SensorPressure, + SensorTvoc, SensorEco2, SensorIaq); + } + + /** + * Credits: Bouke_Regnerus @ https://community.home-assistant.io/t/example-indoor-air-quality-text-sensor-using-ccs811-sensor/125854 + */ + char *_getIaqIndex(float humidity, int tvoc, int eco2) + { + int iaq_index = 0; + + /* + * Transform indoor humidity values to IAQ points according to Indoor Air Quality UK: + * http://www.iaquk.org.uk/ + */ + if (humidity < 10 or humidity > 90) + { + iaq_index += 1; + } + else if (humidity < 20 or humidity > 80) + { + iaq_index += 2; + } + else if (humidity < 30 or humidity > 70) + { + iaq_index += 3; + } + else if (humidity < 40 or humidity > 60) + { + iaq_index += 4; + } + else if (humidity >= 40 and humidity <= 60) + { + iaq_index += 5; + } + + /* + * Transform eCO2 values to IAQ points according to Indoor Air Quality UK: + * http://www.iaquk.org.uk/ + */ + if (eco2 <= 600) + { + iaq_index += 5; + } + else if (eco2 <= 800) + { + iaq_index += 4; + } + else if (eco2 <= 1500) + { + iaq_index += 3; + } + else if (eco2 <= 1800) + { + iaq_index += 2; + } + else if (eco2 > 1800) + { + iaq_index += 1; + } + + /* + * Transform TVOC values to IAQ points according to German environmental guidelines: + * https://www.repcomsrl.com/wp-content/uploads/2017/06/Environmental_Sensing_VOC_Product_Brochure_EN.pdf + */ + if (tvoc <= 65) + { + iaq_index += 5; + } + else if (tvoc <= 220) + { + iaq_index += 4; + } + else if (tvoc <= 660) + { + iaq_index += 3; + } + else if (tvoc <= 2200) + { + iaq_index += 2; + } + else if (tvoc > 2200) + { + iaq_index += 1; + } + + if (iaq_index <= 6) + { + return "Unhealty"; + } + else if (iaq_index <= 9) + { + return "Poor"; + } + else if (iaq_index <= 12) + { + return "Moderate"; + } + else if (iaq_index <= 14) + { + return "Good"; + } + else if (iaq_index > 14) + { + return "Excellent"; + } + } + +public: + void setup() + { + Serial.println("Starting!"); + Serial.println("Initializing sensors.. "); + _initialize(); + } + + // gets called every time WiFi is (re-)connected. + void connected() + { + nextMeasure = millis() + 5000; // Schedule next measure in 5 seconds + } + + void loop() + { + unsigned long tempTimer = millis(); + + if (tempTimer > nextMeasure) + { + nextMeasure = tempTimer + 60000; // Schedule next measure in 60 seconds + + if (!initialized) + { + Serial.println("Error! Sensors not initialized in loop()!"); + _initialize(); + return; // lets try again next loop + } + + if (mqtt != nullptr && mqtt->connected()) + { + if (!mqttInitialized) + { + _mqttInitialize(); + mqttInitialized = true; + } + + // Update sensor data + _updateSensorData(); + + // Create string populated with user defined device topic from the UI, + // and the read temperature, humidity and pressure. + // Then publish to MQTT server. + mqtt->publish(mqttTemperatureTopic.c_str(), 0, true, String(SensorTemperature).c_str()); + mqtt->publish(mqttPressureTopic.c_str(), 0, true, String(SensorPressure).c_str()); + mqtt->publish(mqttHumidityTopic.c_str(), 0, true, String(SensorHumidity).c_str()); + mqtt->publish(mqttTvocTopic.c_str(), 0, true, String(SensorTvoc).c_str()); + mqtt->publish(mqttEco2Topic.c_str(), 0, true, String(SensorEco2).c_str()); + mqtt->publish(mqttIaqTopic.c_str(), 0, true, String(SensorIaq).c_str()); + } + else + { + Serial.println("Missing MQTT connection. Not publishing data"); + mqttInitialized = false; + } + } + } +}; diff --git a/usermods/seven_segment_display/readme.md b/usermods/seven_segment_display/readme.md new file mode 100644 index 00000000..a5294701 --- /dev/null +++ b/usermods/seven_segment_display/readme.md @@ -0,0 +1,55 @@ +# Seven Segment Display + +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 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. 7 segments per digit. +#### perPeriod -- ssLEDPerPeriod +The number of individual LEDs per period. A ':' (colon) has two periods. +#### startIdx -- ssStartLED +Index of the LED the display starts at. Enabless 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. +#### scrollSpd -- ssScrollSpeed +Time, in milliseconds, between message shifts when the length of displayMsg exceeds the length of the displayMask. +#### displayMask -- ssDisplayMask +This should represent the configuration of the physical display. +
+HH - 0-23. hh - 1-12, kk - 1-24 hours  
+MM or mm - 0-59 minutes  
+SS or ss = 0-59 seconds  
+: for a colon  
+All others for alpha numeric, (will be blank when displaying time)
+
+##### Example +```HHMMSS ``` +```hh:MM:SS ``` +#### displayMsg -- ssDisplayMessage +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 your LEDs are configured in. All segments in the display need to be wired the same way. +
+           -------
+         /   A   /          0 - EDCGFAB
+        / F     / B         1 - EDCBAFG
+       /       /            2 - GCDEFAB
+       -------              3 - GBAFEDC
+     /   G   /              4 - FABGEDC
+    / E     / C             5 - FABCDEG
+   /       /
+   -------
+      D
+
+ +## Version +20211009 - Initial release diff --git a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h b/usermods/seven_segment_display/usermod_v2_seven_segment_display.h new file mode 100644 index 00000000..e5b726e5 --- /dev/null +++ b/usermods/seven_segment_display/usermod_v2_seven_segment_display.h @@ -0,0 +1,501 @@ +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +#pragma once + +#include "wled.h" + +class SevenSegmentDisplay : public Usermod +{ + +#define WLED_SS_BUFFLEN 6 +#define REFRESHTIME 497 +private: + //Runtime variables. + unsigned long lastRefresh = 0; + unsigned long lastCharacterStep = 0; + String ssDisplayBuffer = ""; + char ssCharacterMask[36] = {0x77, 0x11, 0x6B, 0x3B, 0x1D, 0x3E, 0x7E, 0x13, 0x7F, 0x1F, 0x5F, 0x7C, 0x66, 0x79, 0x6E, 0x4E, 0x76, 0x5D, 0x44, 0x71, 0x5E, 0x64, 0x27, 0x58, 0x77, 0x4F, 0x1F, 0x48, 0x3E, 0x6C, 0x75, 0x25, 0x7D, 0x2A, 0x3D, 0x6B}; + int ssDisplayMessageIdx = 0; //Position of the start of the message to be physically displayed. + bool ssDoDisplayTime = true; + int ssVirtualDisplayMessageIdxStart = 0; + int ssVirtualDisplayMessageIdxEnd = 0; + unsigned long resfreshTime = 497; + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + int ssLEDPerSegment = 1; //The number of LEDs in each segment of the 7 seg (total per digit is 7 * ssLedPerSegment) + int ssLEDPerPeriod = 1; //A Period will have 1x and a Colon will have 2x + int ssStartLED = 0; //The pixel that the display starts at. + /* HH - 0-23. hh - 1-12, kk - 1-24 hours + // MM or mm - 0-59 minutes + // SS or ss = 0-59 seconds + // : for a colon + // All others for alpha numeric, (will be blank when displaying time) + */ + String ssDisplayMask = "HHMMSS"; //Physical Display Mask, this should reflect physical equipment. + /* ssDisplayConfig + // ------- + // / A / 0 - EDCGFAB + // / F / B 1 - EDCBAFG + // / / 2 - GCDEFAB + // ------- 3 - GBAFEDC + // / G / 4 - FABGEDC + // / E / C 5 - FABCDEG + // / / + // ------- + // D + */ + int ssDisplayConfig = 5; //Physical configuration of the Seven segment display + String ssDisplayMessage = "~"; + bool ssTimeEnabled = true; //If not, display message. + unsigned int ssScrollSpeed = 1000; //Time between advancement of extended message scrolling, in milliseconds. + + //String to reduce flash memory usage + static const char _str_perSegment[]; + static const char _str_perPeriod[]; + static const char _str_startIdx[]; + static const char _str_displayCfg[]; + static const char _str_timeEnabled[]; + static const char _str_scrollSpd[]; + static const char _str_displayMask[]; + static const char _str_displayMsg[]; + static const char _str_sevenSeg[]; + static const char _str_subFormat[]; + static const char _str_topicFormat[]; + + unsigned long _overlaySevenSegmentProcess() + { + //Do time for now. + if (ssDoDisplayTime) + { + //Format the ssDisplayBuffer based on ssDisplayMask + int displayMaskLen = static_cast(ssDisplayMask.length()); + for (int index = 0; index < displayMaskLen; index++) + { + //Only look for time formatting if there are at least 2 characters left in the buffer. + if ((index < displayMaskLen - 1) && (ssDisplayMask[index] == ssDisplayMask[index + 1])) + { + int timeVar = 0; + switch (ssDisplayMask[index]) + { + case 'h': + timeVar = hourFormat12(localTime); + break; + case 'H': + timeVar = hour(localTime); + break; + case 'k': + timeVar = hour(localTime) + 1; + break; + case 'M': + case 'm': + timeVar = minute(localTime); + break; + case 'S': + case 's': + timeVar = second(localTime); + break; + } + + //Only want to leave a blank in the hour formatting. + if ((ssDisplayMask[index] == 'h' || ssDisplayMask[index] == 'H' || ssDisplayMask[index] == 'k') && timeVar < 10) + ssDisplayBuffer[index] = ' '; + else + ssDisplayBuffer[index] = 0x30 + (timeVar / 10); + ssDisplayBuffer[index + 1] = 0x30 + (timeVar % 10); + + //Need to increment the index because of the second digit. + index++; + } + else + { + ssDisplayBuffer[index] = (ssDisplayMask[index] == ':' ? ':' : ' '); + } + } + return REFRESHTIME; + } + else + { + /* This will handle displaying a message and the scrolling of the message if its longer than the buffer length */ + + //Check to see if the message has scrolled completely + int len = static_cast(ssDisplayMessage.length()); + if (ssDisplayMessageIdx > len) + { + //If it has scrolled the whole message, reset it. + setSevenSegmentMessage(ssDisplayMessage); + return REFRESHTIME; + } + //Display message + int displayMaskLen = static_cast(ssDisplayMask.length()); + for (int index = 0; index < displayMaskLen; index++) + { + if (ssDisplayMessageIdx + index < len && ssDisplayMessageIdx + index >= 0) + ssDisplayBuffer[index] = ssDisplayMessage[ssDisplayMessageIdx + index]; + else + ssDisplayBuffer[index] = ' '; + } + + //Increase the displayed message index to progress it one character if the length exceeds the display length. + if (len > displayMaskLen) + ssDisplayMessageIdx++; + + return ssScrollSpeed; + } + } + + void _overlaySevenSegmentDraw() + { + + //Start pixels at ssStartLED, Use ssLEDPerSegment, ssLEDPerPeriod, ssDisplayBuffer + int indexLED = ssStartLED; + int displayMaskLen = static_cast(ssDisplayMask.length()); + for (int indexBuffer = 0; indexBuffer < displayMaskLen; indexBuffer++) + { + if (ssDisplayBuffer[indexBuffer] == 0) + break; + else if (ssDisplayBuffer[indexBuffer] == '.') + { + //Won't ever turn off LED lights for a period. (or will we?) + indexLED += ssLEDPerPeriod; + continue; + } + else if (ssDisplayBuffer[indexBuffer] == ':') + { + //Turn off colon if odd second? + indexLED += ssLEDPerPeriod * 2; + } + else if (ssDisplayBuffer[indexBuffer] == ' ') + { + //Turn off all 7 segments. + _overlaySevenSegmentLEDOutput(0, indexLED); + indexLED += ssLEDPerSegment * 7; + } + else + { + //Turn off correct segments. + _overlaySevenSegmentLEDOutput(_overlaySevenSegmentGetCharMask(ssDisplayBuffer[indexBuffer]), indexLED); + indexLED += ssLEDPerSegment * 7; + } + } + } + + void _overlaySevenSegmentLEDOutput(char mask, int indexLED) + { + for (char index = 0; index < 7; index++) + { + if ((mask & (0x40 >> index)) != (0x40 >> index)) + { + for (int numPerSeg = 0; numPerSeg < ssLEDPerSegment; numPerSeg++) + { + strip.setPixelColor(indexLED + numPerSeg, 0x000000); + } + } + indexLED += ssLEDPerSegment; + } + } + + char _overlaySevenSegmentGetCharMask(char var) + { + if (var >= 0x30 && var <= 0x39) + { /*If its a number, shift to index 0.*/ + var -= 0x30; + } + else if (var >= 0x41 && var <= 0x5a) + { /*If its an Upper case, shift to index 0xA.*/ + var -= 0x37; + } + else if (var >= 0x61 && var <= 0x7A) + { /*If its a lower case, shift to index 0xA.*/ + var -= 0x57; + } + else + { /* Else unsupported, return 0; */ + return 0; + } + char mask = ssCharacterMask[static_cast(var)]; + /* + 0 - EDCGFAB + 1 - EDCBAFG + 2 - GCDEFAB + 3 - GBAFEDC + 4 - FABGEDC + 5 - FABCDEG + */ + switch (ssDisplayConfig) + { + case 1: + mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1); + mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1); + break; + case 2: + mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1); + mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1); + break; + case 3: + mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); + mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1); + mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1); + break; + case 4: + mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); + break; + case 5: + mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); + mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1); + mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1); + break; + } + return mask; + } + + char _overlaySevenSegmentSwapBits(char x, char p1, char p2, char n) + { + /* Move all bits of first set to rightmost side */ + char set1 = (x >> p1) & ((1U << n) - 1); + + /* Move all bits of second set to rightmost side */ + char set2 = (x >> p2) & ((1U << n) - 1); + + /* Xor the two sets */ + char Xor = (set1 ^ set2); + + /* Put the Xor bits back to their original positions */ + Xor = (Xor << p1) | (Xor << p2); + + /* Xor the 'Xor' with the original number so that the + two sets are swapped */ + char result = x ^ Xor; + + return result; + } + + void _publishMQTTint_P(const char *subTopic, int value) + { + if(mqtt == NULL) return; + + char buffer[64]; + char valBuffer[12]; + sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_sevenSeg, subTopic); + sprintf_P(valBuffer, PSTR("%d"), value); + mqtt->publish(buffer, 2, true, valBuffer); + } + + void _publishMQTTstr_P(const char *subTopic, String Value) + { + if(mqtt == NULL) return; + char buffer[64]; + sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_sevenSeg, subTopic); + mqtt->publish(buffer, 2, true, Value.c_str(), Value.length()); + } + + void _updateMQTT() + { + _publishMQTTint_P(_str_perSegment, ssLEDPerSegment); + _publishMQTTint_P(_str_perPeriod, ssLEDPerPeriod); + _publishMQTTint_P(_str_startIdx, ssStartLED); + _publishMQTTint_P(_str_displayCfg, ssDisplayConfig); + _publishMQTTint_P(_str_timeEnabled, ssTimeEnabled); + _publishMQTTint_P(_str_scrollSpd, ssScrollSpeed); + + _publishMQTTstr_P(_str_displayMask, ssDisplayMask); + _publishMQTTstr_P(_str_displayMsg, ssDisplayMessage); + } + + bool _cmpIntSetting_P(char *topic, char *payload, const char *setting, void *value) + { + if (strcmp_P(topic, setting) == 0) + { + *((int *)value) = strtol(payload, NULL, 10); + _publishMQTTint_P(setting, *((int *)value)); + return true; + } + return false; + } + + bool _handleSetting(char *topic, char *payload) + { + if (_cmpIntSetting_P(topic, payload, _str_perSegment, &ssLEDPerSegment)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_perPeriod, &ssLEDPerPeriod)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_startIdx, &ssStartLED)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_displayCfg, &ssDisplayConfig)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_timeEnabled, &ssTimeEnabled)) + return true; + if (_cmpIntSetting_P(topic, payload, _str_scrollSpd, &ssScrollSpeed)) + return true; + if (strcmp_P(topic, _str_displayMask) == 0) + { + ssDisplayMask = String(payload); + ssDisplayBuffer = ssDisplayMask; + _publishMQTTstr_P(_str_displayMask, ssDisplayMask); + return true; + } + if (strcmp_P(topic, _str_displayMsg) == 0) + { + setSevenSegmentMessage(String(payload)); + return true; + } + return false; + } + +public: + void setSevenSegmentMessage(String message) + { + //If the message isn't blank display it otherwise show time, if enabled. + if (message.length() < 1 || message == "~") + ssDoDisplayTime = ssTimeEnabled; + else + ssDoDisplayTime = false; + + //Determine is the message is longer than the display, if it is configure it to scroll the message. + if (message.length() > ssDisplayMask.length()) + ssDisplayMessageIdx = -ssDisplayMask.length(); + else + ssDisplayMessageIdx = 0; + + //If the message isn't the same, update runtime/mqtt (most calls will be resetting message scroll) + if (!ssDisplayMessage.equals(message)) + { + _publishMQTTstr_P(_str_displayMsg, message); + ssDisplayMessage = message; + } + } + //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() + { + ssDisplayBuffer = ssDisplayMask; + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() + { + if (millis() - lastRefresh > resfreshTime) + { + //In theory overlaySevenSegmentProcess should return the amount of time until it changes next. + //So we should be okay to trigger the stripi on every process loop. + resfreshTime = _overlaySevenSegmentProcess(); + lastRefresh = millis(); + strip.trigger(); + } + } + + void handleOverlayDraw() + { + _overlaySevenSegmentDraw(); + } + + void onMqttConnect(bool sessionPresent) + { + char subBuffer[48]; + if (mqttDeviceTopic[0] != 0) + { + _updateMQTT(); + //subscribe for sevenseg messages on the device topic + sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttDeviceTopic, _str_sevenSeg); + mqtt->subscribe(subBuffer, 2); + } + + if (mqttGroupTopic[0] != 0) + { + //subcribe for sevenseg messages on the group topic + sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttGroupTopic, _str_sevenSeg); + mqtt->subscribe(subBuffer, 2); + } + } + + bool onMqttMessage(char *topic, char *payload) + { + //If topic beings iwth 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; + else + return false; + //We only care if the topic ends with /set + size_t topicLen = strlen(topic); + if (topicLen > 4 && + topic[topicLen - 4] == '/' && + topic[topicLen - 3] == 's' && + topic[topicLen - 2] == 'e' && + topic[topicLen - 1] == 't') + { + //Trim /set and handle it + topic[topicLen - 4] = '\0'; + _handleSetting(topic, payload); + } + return true; + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root[FPSTR(_str_sevenSeg)]; + if (top.isNull()) + { + top = root.createNestedObject(FPSTR(_str_sevenSeg)); + } + top[FPSTR(_str_perSegment)] = ssLEDPerSegment; + top[FPSTR(_str_perPeriod)] = ssLEDPerPeriod; + top[FPSTR(_str_startIdx)] = ssStartLED; + top[FPSTR(_str_displayMask)] = ssDisplayMask; + top[FPSTR(_str_displayCfg)] = ssDisplayConfig; + top[FPSTR(_str_displayMsg)] = ssDisplayMessage; + top[FPSTR(_str_timeEnabled)] = ssTimeEnabled; + top[FPSTR(_str_scrollSpd)] = ssScrollSpeed; + } + + bool readFromConfig(JsonObject &root) + { + JsonObject top = root[FPSTR(_str_sevenSeg)]; + + bool configComplete = !top.isNull(); + + //if sevenseg section doesn't exist return + if (!configComplete) + return configComplete; + + configComplete &= getJsonValue(top[FPSTR(_str_perSegment)], ssLEDPerSegment); + configComplete &= getJsonValue(top[FPSTR(_str_perPeriod)], ssLEDPerPeriod); + configComplete &= getJsonValue(top[FPSTR(_str_startIdx)], ssStartLED); + configComplete &= getJsonValue(top[FPSTR(_str_displayMask)], ssDisplayMask); + configComplete &= getJsonValue(top[FPSTR(_str_displayCfg)], ssDisplayConfig); + + String newDisplayMessage; + configComplete &= getJsonValue(top[FPSTR(_str_displayMsg)], newDisplayMessage); + setSevenSegmentMessage(newDisplayMessage); + + configComplete &= getJsonValue(top[FPSTR(_str_timeEnabled)], ssTimeEnabled); + configComplete &= getJsonValue(top[FPSTR(_str_scrollSpd)], ssScrollSpeed); + return configComplete; + } + + /* + * 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_SEVEN_SEGMENT_DISPLAY; + } +}; + +const char SevenSegmentDisplay::_str_perSegment[] PROGMEM = "perSegment"; +const char SevenSegmentDisplay::_str_perPeriod[] PROGMEM = "perPeriod"; +const char SevenSegmentDisplay::_str_startIdx[] PROGMEM = "startIdx"; +const char SevenSegmentDisplay::_str_displayCfg[] PROGMEM = "displayCfg"; +const char SevenSegmentDisplay::_str_timeEnabled[] PROGMEM = "timeEnabled"; +const char SevenSegmentDisplay::_str_scrollSpd[] PROGMEM = "scrollSpd"; +const char SevenSegmentDisplay::_str_displayMask[] PROGMEM = "displayMask"; +const char SevenSegmentDisplay::_str_displayMsg[] PROGMEM = "displayMsg"; +const char SevenSegmentDisplay::_str_sevenSeg[] PROGMEM = "sevenSeg"; \ No newline at end of file diff --git a/usermods/seven_segment_display_reloaded/readme.md b/usermods/seven_segment_display_reloaded/readme.md new file mode 100644 index 00000000..d373a7ee --- /dev/null +++ b/usermods/seven_segment_display_reloaded/readme.md @@ -0,0 +1,129 @@ +# Seven Segment Display Reloaded + +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". + + +## Installation + +Add the compile-time option `-D USERMOD_SSDR` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_SSDR` in `my_config.h`. + +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 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 usermod + +### inverted +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 + +### enable-auto-brightness +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, 0 - 1000 lux, will be mapped to auto-brightness-min and auto-brightness-max + +WLED current protection will override the calculated value if it is too high. + +### Display-Mask +Defines the type of the time/date display. +For example "H:m" (default) +- H - 00-23 hours +- h - 01-12 hours +- k - 01-24 hours +- m - 00-59 minutes +- s - 00-59 seconds +- d - 01-31 day of month +- M - 01-12 month +- y - 21 last two positions of year +- Y - 2021 year +- : for a colon + +### LED-Numbers +- LED-Numbers-Hours +- LED-Numbers-Minutes +- LED-Numbers-Seconds +- LED-Numbers-Colons +- LED-Numbers-Day +- LED-Numbers-Month +- LED-Numbers-Year + +See following example for usage. + + +## Example + +Example of an LED definition: +``` + < A > +/\ /\ +F B +\/ \/ + < G > +/\ /\ +E C +\/ \/ + < D > +``` + +LEDs or Range of LEDs are separated by a comma "," + +Segments are separated by a semicolon ";" and are read as 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, 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" + +- minute "37-38;39-40;42-43;44,31;32-33;35-36;34,41:21-22;23-24;26-27;28,15;16-17;19-20;18,25" + +or + +- hour "6,7;8,9;11,12;13,0;1,2;4,5;3,10:52,53;54,55;57,58;59,46;47,48;50,51;49,56" + +- minute "15,28;16,17;19,20;21,22;23,24;26,27;18,25:31,44;32,33;35,36;37,38;39,40;42,43;34,41" + +depending on the orientation. + +# 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 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 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 +- Segment B: 47, 48 +- Segment C: 50, 51 +- Segment D: 52, 53 +- Segment E: 54, 55 +- Segment F: 57, 58 +- Segment G: 49, 56 + +### second digit of the hour + +- Segment A: 0, 13 +- Segment B: 1, 2 +- Segment C: 4, 5 +- Segment D: 6, 7 +- Segment E: 8, 9 +- Segment F: 11, 12 +- 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 new file mode 100644 index 00000000..27977405 --- /dev/null +++ b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h @@ -0,0 +1,559 @@ +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +#pragma once + +#include "wled.h" + +class UsermodSSDR : public Usermod { + +//#define REFRESHTIME 497 + +private: + //Runtime variables. + unsigned long umSSDRLastRefresh = 0; + unsigned long umSSDRResfreshTime = 3000; + bool umSSDRDisplayTime = false; + bool umSSDRInverted = false; + bool umSSDRColonblink = true; + bool umSSDREnableLDR = false; + String umSSDRHours = ""; + String umSSDRMinutes = ""; + String umSSDRSeconds = ""; + String umSSDRColons = ""; + String umSSDRDays = ""; + String umSSDRMonths = ""; + String umSSDRYears = ""; + uint16_t umSSDRLength = 0; + uint16_t umSSDRBrightnessMin = 0; + uint16_t umSSDRBrightnessMax = 255; + + bool* umSSDRMask = 0; + + /*// H - 00-23 hours + // h - 01-12 hours + // k - 01-24 hours + // m - 00-59 minutes + // s - 00-59 seconds + // d - 01-31 day of month + // M - 01-12 month + // y - 21 last two positions of year + // Y - 2021 year + // : for a colon + */ + String umSSDRDisplayMask = "H:m"; //This should reflect physical equipment. + + /* Segment order, seen from the front: + + < A > + /\ /\ + F B + \/ \/ + < G > + /\ /\ + E C + \/ \/ + < D > + + */ + + uint8_t umSSDRNumbers[11][7] = { + // A B C D E F G + { 1, 1, 1, 1, 1, 1, 0 }, // 0 + { 0, 1, 1, 0, 0, 0, 0 }, // 1 + { 1, 1, 0, 1, 1, 0, 1 }, // 2 + { 1, 1, 1, 1, 0, 0, 1 }, // 3 + { 0, 1, 1, 0, 0, 1, 1 }, // 4 + { 1, 0, 1, 1, 0, 1, 1 }, // 5 + { 1, 0, 1, 1, 1, 1, 1 }, // 6 + { 1, 1, 1, 0, 0, 0, 0 }, // 7 + { 1, 1, 1, 1, 1, 1, 1 }, // 8 + { 1, 1, 1, 1, 0, 1, 1 }, // 9 + { 0, 0, 0, 0, 0, 0, 0 } // blank + }; + + //String to reduce flash memory usage + static const char _str_name[]; + static const char _str_ldrEnabled[]; + static const char _str_timeEnabled[]; + static const char _str_inverted[]; + static const char _str_colonblink[]; + static const char _str_displayMask[]; + static const char _str_hours[]; + static const char _str_minutes[]; + static const char _str_seconds[]; + static const char _str_colons[]; + static const char _str_days[]; + static const char _str_months[]; + static const char _str_years[]; + static const char _str_minBrightness[]; + static const char _str_maxBrightness[]; + +#ifdef USERMOD_SN_PHOTORESISTOR + Usermod_SN_Photoresistor *ptr; +#else + void* ptr = nullptr; +#endif + + void _overlaySevenSegmentDraw() { + int displayMaskLen = static_cast(umSSDRDisplayMask.length()); + bool colonsDone = false; + _setAllFalse(); + for (int index = 0; index < displayMaskLen; index++) { + int timeVar = 0; + switch (umSSDRDisplayMask[index]) { + case 'h': + timeVar = hourFormat12(localTime); + _showElements(&umSSDRHours, timeVar, 0, 1); + break; + case 'H': + timeVar = hour(localTime); + _showElements(&umSSDRHours, timeVar, 0, 1); + break; + case 'k': + timeVar = hour(localTime) + 1; + _showElements(&umSSDRHours, timeVar, 0, 0); + break; + case 'm': + timeVar = minute(localTime); + _showElements(&umSSDRMinutes, timeVar, 0, 0); + break; + case 's': + timeVar = second(localTime); + _showElements(&umSSDRSeconds, timeVar, 0, 0); + break; + case 'd': + timeVar = day(localTime); + _showElements(&umSSDRDays, timeVar, 0, 0); + break; + case 'M': + timeVar = month(localTime); + _showElements(&umSSDRMonths, timeVar, 0, 0); + break; + case 'y': + timeVar = second(localTime); + _showElements(&umSSDRYears, timeVar, 0, 0); + break; + case 'Y': + timeVar = year(localTime); + _showElements(&umSSDRYears, timeVar, 0, 0); + break; + case ':': + if (!colonsDone) { // only call _setColons once as all colons are printed when the first colon is found + _setColons(); + colonsDone = true; + } + break; + } + } + _setMaskToLeds(); + } + + void _setColons() { + if ( umSSDRColonblink ) { + if ( second(localTime) % 2 == 0 ) { + _showElements(&umSSDRColons, 0, 1, 0); + } + } else { + _showElements(&umSSDRColons, 0, 1, 0); + } + } + + void _showElements(String *map, int timevar, bool isColon, bool removeZero + +) { + if (!(*map).equals("") && !(*map) == NULL) { + int length = String(timevar).length(); + bool addZero = false; + if (length == 1) { + length = 2; + addZero = true; + } + int timeArr[length]; + if(addZero) { + if(removeZero) + { + timeArr[1] = 10; + timeArr[0] = timevar; + } + else + { + timeArr[1] = 0; + timeArr[0] = timevar; + } + } else { + int count = 0; + while (timevar) { + timeArr[count] = timevar%10; + timevar /= 10; + count++; + }; + } + + + int colonsLen = static_cast((*map).length()); + int count = 0; + int countSegments = 0; + int countDigit = 0; + bool range = false; + int lastSeenLedNr = 0; + + for (int index = 0; index < colonsLen; index++) { + switch ((*map)[index]) { + case '-': + lastSeenLedNr = _checkForNumber(count, index, map); + count = 0; + range = true; + break; + case ':': + _setLeds(_checkForNumber(count, index, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon); + count = 0; + range = false; + countDigit++; + countSegments = 0; + break; + case ';': + _setLeds(_checkForNumber(count, index, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon); + count = 0; + range = false; + countSegments++; + break; + case ',': + _setLeds(_checkForNumber(count, index, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon); + count = 0; + range = false; + break; + default: + count++; + break; + } + } + _setLeds(_checkForNumber(count, colonsLen, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon); + } + } + + void _setLeds(int lednr, int lastSeenLedNr, bool range, int countSegments, int number, bool colon) { + + if ((colon && umSSDRColonblink) || umSSDRNumbers[number][countSegments]) { + + if (range) { + for(int i = lastSeenLedNr; i <= lednr; i++) { + umSSDRMask[i] = true; + } + } else { + umSSDRMask[lednr] = true; + } + } + } + + void _setMaskToLeds() { + for(int i = 0; i <= umSSDRLength; i++) { + if ((!umSSDRInverted && !umSSDRMask[i]) || (umSSDRInverted && umSSDRMask[i])) { + strip.setPixelColor(i, 0x000000); + } + } + } + + void _setAllFalse() { + for(int i = 0; i <= umSSDRLength; i++) { + umSSDRMask[i] = false; + } + } + + int _checkForNumber(int count, int index, String *map) { + String number = (*map).substring(index - count, index); + return number.toInt(); + } + + void _publishMQTTint_P(const char *subTopic, int value) + { + if(mqtt == NULL) return; + + char buffer[64]; + char valBuffer[12]; + sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_name, subTopic); + sprintf_P(valBuffer, PSTR("%d"), value); + mqtt->publish(buffer, 2, true, valBuffer); + } + + void _publishMQTTstr_P(const char *subTopic, String Value) + { + if(mqtt == NULL) return; + char buffer[64]; + sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_name, subTopic); + mqtt->publish(buffer, 2, true, Value.c_str(), Value.length()); + } + + bool _cmpIntSetting_P(char *topic, char *payload, const char *setting, void *value) + { + if (strcmp_P(topic, setting) == 0) + { + *((int *)value) = strtol(payload, NULL, 10); + _publishMQTTint_P(setting, *((int *)value)); + return true; + } + return false; + } + + bool _handleSetting(char *topic, char *payload) { + if (_cmpIntSetting_P(topic, payload, _str_timeEnabled, &umSSDRDisplayTime)) { + return true; + } + if (_cmpIntSetting_P(topic, payload, _str_ldrEnabled, &umSSDREnableLDR)) { + return true; + } + if (_cmpIntSetting_P(topic, payload, _str_inverted, &umSSDRInverted)) { + return true; + } + if (_cmpIntSetting_P(topic, payload, _str_colonblink, &umSSDRColonblink)) { + return true; + } + if (strcmp_P(topic, _str_displayMask) == 0) { + umSSDRDisplayMask = String(payload); + _publishMQTTstr_P(_str_displayMask, umSSDRDisplayMask); + return true; + } + return false; + } + + void _updateMQTT() + { + _publishMQTTint_P(_str_timeEnabled, umSSDRDisplayTime); + _publishMQTTint_P(_str_ldrEnabled, umSSDREnableLDR); + _publishMQTTint_P(_str_inverted, umSSDRInverted); + _publishMQTTint_P(_str_colonblink, umSSDRColonblink); + + _publishMQTTstr_P(_str_hours, umSSDRHours); + _publishMQTTstr_P(_str_minutes, umSSDRMinutes); + _publishMQTTstr_P(_str_seconds, umSSDRSeconds); + _publishMQTTstr_P(_str_colons, umSSDRColons); + _publishMQTTstr_P(_str_days, umSSDRDays); + _publishMQTTstr_P(_str_months, umSSDRMonths); + _publishMQTTstr_P(_str_years, umSSDRYears); + _publishMQTTstr_P(_str_displayMask, umSSDRDisplayMask); + + _publishMQTTint_P(_str_minBrightness, umSSDRBrightnessMin); + _publishMQTTint_P(_str_maxBrightness, umSSDRBrightnessMax); + } + + void _addJSONObject(JsonObject& root) { + JsonObject ssdrObj = root[FPSTR(_str_name)]; + if (ssdrObj.isNull()) { + ssdrObj = root.createNestedObject(FPSTR(_str_name)); + } + + ssdrObj[FPSTR(_str_timeEnabled)] = umSSDRDisplayTime; + ssdrObj[FPSTR(_str_ldrEnabled)] = umSSDREnableLDR; + ssdrObj[FPSTR(_str_inverted)] = umSSDRInverted; + ssdrObj[FPSTR(_str_colonblink)] = umSSDRColonblink; + ssdrObj[FPSTR(_str_displayMask)] = umSSDRDisplayMask; + ssdrObj[FPSTR(_str_hours)] = umSSDRHours; + ssdrObj[FPSTR(_str_minutes)] = umSSDRMinutes; + ssdrObj[FPSTR(_str_seconds)] = umSSDRSeconds; + ssdrObj[FPSTR(_str_colons)] = umSSDRColons; + ssdrObj[FPSTR(_str_days)] = umSSDRDays; + ssdrObj[FPSTR(_str_months)] = umSSDRMonths; + ssdrObj[FPSTR(_str_years)] = umSSDRYears; + ssdrObj[FPSTR(_str_minBrightness)] = umSSDRBrightnessMin; + ssdrObj[FPSTR(_str_maxBrightness)] = umSSDRBrightnessMax; + } + +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() { + umSSDRLength = strip.getLengthTotal(); + if (umSSDRMask != 0) { + umSSDRMask = (bool*) realloc(umSSDRMask, umSSDRLength * sizeof(bool)); + } else { + umSSDRMask = (bool*) malloc(umSSDRLength * sizeof(bool)); + } + _setAllFalse(); + + #ifdef USERMOD_SN_PHOTORESISTOR + ptr = (Usermod_SN_Photoresistor*) usermods.lookup(USERMOD_ID_SN_PHOTORESISTOR); + #endif + DEBUG_PRINTLN(F("Setup done")); + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() { + if (!umSSDRDisplayTime || strip.isUpdating()) { + return; + } + #ifdef USERMOD_SN_PHOTORESISTOR + if(bri != 0 && umSSDREnableLDR && (millis() - umSSDRLastRefresh > umSSDRResfreshTime)) { + if (ptr != nullptr) { + uint16_t lux = ptr->getLastLDRValue(); + uint16_t brightness = map(lux, 0, 1000, umSSDRBrightnessMin, umSSDRBrightnessMax); + if (bri != brightness) { + bri = brightness; + stateUpdated(1); + } + } + umSSDRLastRefresh = millis(); + } + #endif + } + + void handleOverlayDraw() { + if (umSSDRDisplayTime) { + _overlaySevenSegmentDraw(); + } + } + +/* + * 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[F("u")]; + if (user.isNull()) { + user = root.createNestedObject(F("u")); + } + JsonArray enabled = user.createNestedArray("Time enabled"); + enabled.add(umSSDRDisplayTime); + JsonArray invert = user.createNestedArray("Time inverted"); + invert.add(umSSDRInverted); + JsonArray blink = user.createNestedArray("Blinking colon"); + blink.add(umSSDRColonblink); + JsonArray ldrEnable = user.createNestedArray("Auto Brightness enabled"); + ldrEnable.add(umSSDREnableLDR); + + } + + /* + * 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) { + JsonObject user = root[F("u")]; + if (user.isNull()) { + user = root.createNestedObject(F("u")); + } + _addJSONObject(user); + } + + /* + * 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) { + JsonObject user = root[F("u")]; + if (!user.isNull()) { + JsonObject ssdrObj = user[FPSTR(_str_name)]; + umSSDRDisplayTime = ssdrObj[FPSTR(_str_timeEnabled)] | umSSDRDisplayTime; + umSSDREnableLDR = ssdrObj[FPSTR(_str_ldrEnabled)] | umSSDREnableLDR; + umSSDRInverted = ssdrObj[FPSTR(_str_inverted)] | umSSDRInverted; + umSSDRColonblink = ssdrObj[FPSTR(_str_colonblink)] | umSSDRColonblink; + umSSDRDisplayMask = ssdrObj[FPSTR(_str_displayMask)] | umSSDRDisplayMask; + } + } + + void onMqttConnect(bool sessionPresent) { + char subBuffer[48]; + if (mqttDeviceTopic[0] != 0) + { + _updateMQTT(); + //subscribe for sevenseg messages on the device topic + sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttDeviceTopic, _str_name); + mqtt->subscribe(subBuffer, 2); + } + + if (mqttGroupTopic[0] != 0) + { + //subcribe 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. + size_t topicPrefixLen = strlen_P(PSTR("/wledSS/")); + if (strncmp_P(topic, PSTR("/wledSS/"), topicPrefixLen) == 0) { + topic += topicPrefixLen; + } else { + return false; + } + //We only care if the topic ends with /set + size_t topicLen = strlen(topic); + if (topicLen > 4 && + topic[topicLen - 4] == '/' && + topic[topicLen - 3] == 's' && + topic[topicLen - 2] == 'e' && + topic[topicLen - 1] == 't') + { + //Trim /set and handle it + topic[topicLen - 4] = '\0'; + _handleSetting(topic, payload); + } + return true; + } + + void addToConfig(JsonObject &root) { + _addJSONObject(root); + } + + bool readFromConfig(JsonObject &root) { + JsonObject top = root[FPSTR(_str_name)]; + + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_str_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + umSSDRDisplayTime = (top[FPSTR(_str_timeEnabled)] | umSSDRDisplayTime); + umSSDREnableLDR = (top[FPSTR(_str_ldrEnabled)] | umSSDREnableLDR); + umSSDRInverted = (top[FPSTR(_str_inverted)] | umSSDRInverted); + umSSDRColonblink = (top[FPSTR(_str_colonblink)] | umSSDRColonblink); + + umSSDRDisplayMask = top[FPSTR(_str_displayMask)] | umSSDRDisplayMask; + umSSDRHours = top[FPSTR(_str_hours)] | umSSDRHours; + umSSDRMinutes = top[FPSTR(_str_minutes)] | umSSDRMinutes; + umSSDRSeconds = top[FPSTR(_str_seconds)] | umSSDRSeconds; + umSSDRColons = top[FPSTR(_str_colons)] | umSSDRColons; + umSSDRDays = top[FPSTR(_str_days)] | umSSDRDays; + umSSDRMonths = top[FPSTR(_str_months)] | umSSDRMonths; + umSSDRYears = top[FPSTR(_str_years)] | umSSDRYears; + umSSDRBrightnessMin = top[FPSTR(_str_minBrightness)] | umSSDRBrightnessMin; + umSSDRBrightnessMax = top[FPSTR(_str_maxBrightness)] | umSSDRBrightnessMax; + + DEBUG_PRINT(FPSTR(_str_name)); + DEBUG_PRINTLN(F(" config (re)loaded.")); + + return true; + } + /* + * 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_SSDR; + } +}; + +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_displayMask[] PROGMEM = "Display-Mask"; +const char UsermodSSDR::_str_hours[] PROGMEM = "LED-Numbers-Hours"; +const char UsermodSSDR::_str_minutes[] PROGMEM = "LED-Numbers-Minutes"; +const char UsermodSSDR::_str_seconds[] PROGMEM = "LED-Numbers-Seconds"; +const char UsermodSSDR::_str_colons[] PROGMEM = "LED-Numbers-Colons"; +const char UsermodSSDR::_str_days[] PROGMEM = "LED-Numbers-Day"; +const char UsermodSSDR::_str_months[] PROGMEM = "LED-Numbers-Month"; +const char UsermodSSDR::_str_years[] PROGMEM = "LED-Numbers-Year"; +const char UsermodSSDR::_str_ldrEnabled[] PROGMEM = "enable-auto-brightness"; +const char UsermodSSDR::_str_minBrightness[] PROGMEM = "auto-brightness-min"; +const char UsermodSSDR::_str_maxBrightness[] PROGMEM = "auto-brightness-max"; diff --git a/usermods/sht/readme.md b/usermods/sht/readme.md new file mode 100644 index 00000000..0337805b --- /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 00000000..56cea219 --- /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 donce 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 00000000..5c3ef807 --- /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 00000000..8d2b04ff --- /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/ssd1306_i2c_oled_u8g2/README.md b/usermods/ssd1306_i2c_oled_u8g2/README.md deleted file mode 100644 index 70919cc5..00000000 --- a/usermods/ssd1306_i2c_oled_u8g2/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# SSD1306 128x32 OLED via I2C with u8g2 -This usermod allows to connect 128x32 Oled display to WLED controlled and show -the next information: -- Current SSID -- IP address if obtained - * in AP mode and turned off lightning AP password is shown -- Current effect -- Current palette -- On/Off icon (sun/moon) - -## Hardware -![Hardware connection](assets/hw_connection.png) - -## Requirements -Functionality checked with: -- commit 095429a7df4f9e2b34dd464f7bbfd068df6558eb -- Wemos d1 mini -- PlatformIO -- Generic SSD1306 128x32 I2C OLED display from aliexpress - -### Platformio -Add `U8g2@~2.27.2` dependency to `lib_deps_external` under `[common]` section in `platformio.ini`: -```ini -# platformio.ini -... -[common] -... -lib_deps_external = - ... - U8g2@~2.27.2 -... -``` - -### Arduino IDE -Install library `U8g2 by oliver` in `Tools | Include Library | Manage libraries` menu. \ No newline at end of file diff --git a/usermods/ssd1306_i2c_oled_u8g2/assets/hw_connection.png b/usermods/ssd1306_i2c_oled_u8g2/assets/hw_connection.png deleted file mode 100644 index a0e51b4d..00000000 Binary files a/usermods/ssd1306_i2c_oled_u8g2/assets/hw_connection.png and /dev/null differ diff --git a/usermods/ssd1306_i2c_oled_u8g2/wled06_usermod.ino b/usermods/ssd1306_i2c_oled_u8g2/wled06_usermod.ino deleted file mode 100644 index 8c749603..00000000 --- a/usermods/ssd1306_i2c_oled_u8g2/wled06_usermod.ino +++ /dev/null @@ -1,175 +0,0 @@ -#include // from https://github.com/olikraus/u8g2/ - -//The SCL and SDA pins are defined here. -//Lolin32 boards use SCL=5 SDA=4 -#define U8X8_PIN_SCL 5 -#define U8X8_PIN_SDA 4 - - -// 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 -U8X8_SSD1306_128X32_UNIVISION_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() { - u8x8.begin(); - 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); - u8x8.drawString(0, 0, "Loading..."); -} - -// gets called every time WiFi is (re-)connected. Initialize own network -// interfaces here -void userConnected() {} - -// needRedraw marks if redraw is required to prevent often redrawing. -bool needRedraw = true; - -// Next variables hold the previous known values to determine if redraw is -// required. -String knownSsid = ""; -IPAddress knownIp; -uint8_t knownBrightness = 0; -uint8_t knownMode = 0; -uint8_t knownPalette = 0; - -long lastUpdate = 0; -long lastRedraw = 0; -bool displayTurnedOff = false; -// How often we are redrawing screen -#define USER_LOOP_REFRESH_RATE_MS 5000 - -void userLoop() { - - // Check if we time interval for redrawing passes. - if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { - return; - } - lastUpdate = millis(); - - // Turn off display after 3 minutes with no change. - if(!displayTurnedOff && millis() - lastRedraw > 3*60*1000) { - u8x8.setPowerSave(1); - 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.getMode()) { - needRedraw = true; - } else if (knownPalette != strip.getSegment(0).palette) { - needRedraw = true; - } - - if (!needRedraw) { - return; - } - needRedraw = false; - - if (displayTurnedOff) - { - u8x8.setPowerSave(0); - 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.getMode(); - knownPalette = strip.getSegment(0).palette; - - u8x8.clear(); - u8x8.setFont(u8x8_font_chroma48medium8_r); - - // 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 - if (knownSsid.length() > u8x8.getCols()) - u8x8.print("~"); - - // Second row with IP or Psssword - u8x8.setCursor(1, 1); - // Print password in AP mode and if led is OFF. - if (apActive && bri == 0) - u8x8.print(apPass); - else - u8x8.print(knownIp); - - // 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; - } - // 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; - } - - u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); - u8x8.drawGlyph(0, 0, 80); // wifi icon - u8x8.drawGlyph(0, 1, 68); // home icon - u8x8.setFont(u8x8_font_open_iconic_weather_2x2); - u8x8.drawGlyph(0, 2, 66 + (bri > 0 ? 3 : 0)); // sun/moon icon -} diff --git a/usermods/stairway_wipe_basic/readme.md b/usermods/stairway_wipe_basic/readme.md index 632b7d85..35bc0d41 100644 --- a/usermods/stairway_wipe_basic/readme.md +++ b/usermods/stairway_wipe_basic/readme.md @@ -2,13 +2,13 @@ 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 new file mode 100644 index 00000000..238ec7d9 --- /dev/null +++ b/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h @@ -0,0 +1,124 @@ +#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 is Stairway-Wipe as a v2 usermod. + * + * Using this usermod: + * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) + * 2. Register the usermod by adding #include "stairway-wipe-usermod-v2.h" in the top and registerUsermod(new StairwayWipeUsermod()) in the bottom of usermods_list.cpp + */ + +class StairwayWipeUsermod : public Usermod { + private: + //Private class members. You can declare variables and functions only accessible to your usermod here + unsigned long lastTime = 0; + byte wipeState = 0; //0: inactive 1: wiping 2: solid + unsigned long timeStaticStart = 0; + uint16_t previousUserVar0 = 0; + +//comment this out if you want the turn off effect to be just fading out instead of reverse wipe +#define STAIRCASE_WIPE_OFF + public: + + void loop() { + //userVar0 (U0 in HTTP API): + //has to be set to 1 if movement is detected on the PIR that is the same side of the staircase as the ESP8266 + //has to be set to 2 if movement is detected on the PIR that is the opposite side + //can be set to 0 if no movement is detected. Otherwise LEDs will turn off after a configurable timeout (userVar1 seconds) + + if (userVar0 > 0) + { + if ((previousUserVar0 == 1 && userVar0 == 2) || (previousUserVar0 == 2 && userVar0 == 1)) wipeState = 3; //turn off if other PIR triggered + previousUserVar0 = userVar0; + + if (wipeState == 0) { + startWipe(); + wipeState = 1; + } else if (wipeState == 1) { //wiping + uint32_t cycleTime = 360 + (255 - effectSpeed)*75; //this is how long one wipe takes (minus 25 ms to make sure we switch in time) + if (millis() + strip.timebase > (cycleTime - 25)) { //wipe complete + effectCurrent = FX_MODE_STATIC; + timeStaticStart = millis(); + colorUpdated(CALL_MODE_NOTIFICATION); + wipeState = 2; + } + } else if (wipeState == 2) { //static + if (userVar1 > 0) //if U1 is not set, the light will stay on until second PIR or external command is triggered + { + if (millis() - timeStaticStart > userVar1*1000) wipeState = 3; + } + } else if (wipeState == 3) { //switch to wipe off + #ifdef STAIRCASE_WIPE_OFF + effectCurrent = FX_MODE_COLOR_WIPE; + strip.timebase = 360 + (255 - effectSpeed)*75 - millis(); //make sure wipe starts fully lit + colorUpdated(CALL_MODE_NOTIFICATION); + wipeState = 4; + #else + turnOff(); + #endif + } else { //wiping off + if (millis() + strip.timebase > (725 + (255 - effectSpeed)*150)) turnOff(); //wipe complete + } + } else { + wipeState = 0; //reset for next time + if (previousUserVar0) { + #ifdef STAIRCASE_WIPE_OFF + userVar0 = previousUserVar0; + wipeState = 3; + #else + turnOff(); + #endif + } + previousUserVar0 = 0; + } +} + + 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!")); + } + + uint16_t getId() + { + return USERMOD_ID_EXAMPLE; + } + + + void startWipe() + { + bri = briLast; //turn on + transitionDelayTemp = 0; //no transition + effectCurrent = FX_MODE_COLOR_WIPE; + resetTimebase(); //make sure wipe starts from beginning + + //set wipe direction + Segment& seg = strip.getSegment(0); + bool doReverse = (userVar0 == 2); + seg.setOption(1, doReverse); + + colorUpdated(CALL_MODE_NOTIFICATION); + } + + void turnOff() + { + #ifdef STAIRCASE_WIPE_OFF + transitionDelayTemp = 0; //turn off immediately after wipe completed + #else + transitionDelayTemp = 4000; //fade out slowly + #endif + bri = 0; + stateUpdated(CALL_MODE_NOTIFICATION); + wipeState = 0; + userVar0 = 0; + previousUserVar0 = 0; + } + + + + //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! +}; diff --git a/usermods/stairway_wipe_basic/wled06_usermod.ino b/usermods/stairway_wipe_basic/wled06_usermod.ino index 0cc85df7..c1264ebf 100644 --- a/usermods/stairway_wipe_basic/wled06_usermod.ino +++ b/usermods/stairway_wipe_basic/wled06_usermod.ino @@ -47,7 +47,7 @@ void userLoop() if (millis() + strip.timebase > (cycleTime - 25)) { //wipe complete effectCurrent = FX_MODE_STATIC; timeStaticStart = millis(); - colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); + colorUpdated(CALL_MODE_NOTIFICATION); wipeState = 2; } } else if (wipeState == 2) { //static @@ -59,7 +59,7 @@ void userLoop() #ifdef STAIRCASE_WIPE_OFF effectCurrent = FX_MODE_COLOR_WIPE; strip.timebase = 360 + (255 - effectSpeed)*75 - millis(); //make sure wipe starts fully lit - colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); + colorUpdated(CALL_MODE_NOTIFICATION); wipeState = 4; #else turnOff(); @@ -89,11 +89,11 @@ 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); - colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); + colorUpdated(CALL_MODE_NOTIFICATION); } void turnOff() @@ -104,7 +104,7 @@ void turnOff() transitionDelayTemp = 4000; //fade out slowly #endif bri = 0; - colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); + stateUpdated(CALL_MODE_NOTIFICATION); wipeState = 0; userVar0 = 0; previousUserVar0 = 0; diff --git a/usermods/usermod_rotary_brightness_color/README.md b/usermods/usermod_rotary_brightness_color/README.md new file mode 100644 index 00000000..06a3a0f8 --- /dev/null +++ b/usermods/usermod_rotary_brightness_color/README.md @@ -0,0 +1,42 @@ +# Rotary Encoder (Brightness and Color) + +V2 usermod that enables changing brightness and color using a rotary encoder +change between modes by pressing a button (many encoders have one included) + +it will wait for AUTOSAVE_SETTLE_MS milliseconds. 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. + +AutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes. + +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 + +define `USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` e.g. + +`#define USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` in my_config.h + +or add `-D USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` to `build_flags` in platformio_override.ini + +### Define Your Options + +Open Usermod Settings in WLED to change settings: + +`fadeAmount` - how many points to fade the Neopixel with each step of the rotary encoder (default 5) +`pin[3]` - pins to connect to the rotary encoder: +- `pin[0]` is pin A on your rotary encoder +- `pin[1]` is pin B on your rotary encoder +- `pin[2]` is the button on your rotary encoder (optional, set to -1 to disable the button and the rotary encoder will control brightness only) + +### PlatformIO requirements + +No special requirements. + +## Change Log + +2021-07 +* Upgraded to work with the latest WLED code, and make settings configurable in Usermod Settings diff --git a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h new file mode 100644 index 00000000..61b76ba1 --- /dev/null +++ b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h @@ -0,0 +1,189 @@ +#pragma once + +#include "wled.h" + +//v2 usermod that allows to change brightness and color using a rotary encoder, +//change between modes by pressing a button (many encoders have one included) +class RotaryEncoderBrightnessColor : public Usermod +{ +private: + //Private class members. You can declare variables and functions only accessible to your usermod here + unsigned long lastTime = 0; + unsigned long currentTime; + unsigned long loopTime; + + unsigned char select_state = 0; // 0 = brightness 1 = color + unsigned char button_state = HIGH; + unsigned char prev_button_state = HIGH; + CRGB fastled_col; + CHSV prim_hsv; + int16_t new_val; + + unsigned char Enc_A; + unsigned char Enc_B; + unsigned char Enc_A_prev = 0; + + // private class memebers 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 + +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() + { + //Serial.println("Hello from my usermod!"); + pinMode(pins[0], INPUT_PULLUP); + pinMode(pins[1], INPUT_PULLUP); + if(pins[2] >= 0) pinMode(pins[2], INPUT_PULLUP); + currentTime = millis(); + loopTime = currentTime; + } + + /* + * 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() + { + currentTime = millis(); // get the current elapsed time + + if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz + { + if(pins[2] >= 0) { + button_state = digitalRead(pins[2]); + if (prev_button_state != button_state) + { + if (button_state == LOW) + { + if (select_state == 1) + { + select_state = 0; + } + else + { + select_state = 1; + } + prev_button_state = button_state; + } + else + { + prev_button_state = button_state; + } + } + } + int Enc_A = digitalRead(pins[0]); // Read encoder pins + int Enc_B = digitalRead(pins[1]); + if ((!Enc_A) && (Enc_A_prev)) + { // A has gone from high to low + if (Enc_B == HIGH) + { // B is high so clockwise + if (select_state == 0) + { + if (bri + fadeAmount <= 255) + bri += fadeAmount; // increase the brightness, dont go over 255 + } + else + { + fastled_col.red = col[0]; + fastled_col.green = col[1]; + fastled_col.blue = col[2]; + prim_hsv = rgb2hsv_approximate(fastled_col); + new_val = (int16_t)prim_hsv.h + fadeAmount; + if (new_val > 255) + new_val -= 255; // roll-over if bigger than 255 + if (new_val < 0) + new_val += 255; // roll-over if smaller than 0 + prim_hsv.h = (byte)new_val; + hsv2rgb_rainbow(prim_hsv, fastled_col); + col[0] = fastled_col.red; + col[1] = fastled_col.green; + col[2] = fastled_col.blue; + } + } + else if (Enc_B == LOW) + { // B is low so counter-clockwise + if (select_state == 0) + { + if (bri - fadeAmount >= 0) + bri -= fadeAmount; // decrease the brightness, dont go below 0 + } + else + { + fastled_col.red = col[0]; + fastled_col.green = col[1]; + fastled_col.blue = col[2]; + prim_hsv = rgb2hsv_approximate(fastled_col); + new_val = (int16_t)prim_hsv.h - fadeAmount; + if (new_val > 255) + new_val -= 255; // roll-over if bigger than 255 + if (new_val < 0) + new_val += 255; // roll-over if smaller than 0 + prim_hsv.h = (byte)new_val; + hsv2rgb_rainbow(prim_hsv, fastled_col); + col[0] = fastled_col.red; + col[1] = fastled_col.green; + col[2] = fastled_col.blue; + } + } + //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 + colorUpdated(CALL_MODE_BUTTON); + updateInterfaces(CALL_MODE_BUTTON); + } + Enc_A_prev = Enc_A; // Store value of A for next time + loopTime = currentTime; // Updates loopTime + } + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject("rotEncBrightness"); + top["fadeAmount"] = fadeAmount; + JsonArray pinArray = top.createNestedArray("pin"); + pinArray.add(pins[0]); + pinArray.add(pins[1]); + pinArray.add(pins[2]); + } + + /* + * This example uses a more robust method of checking for missing values in the config, and setting back to defaults: + * - The getJsonValue() function copies the value to the variable only if the key requested is present, returning false with no copy if the value isn't present + * - 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: + * - 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) + * + * If configComplete is false, the default values are already set, and by returning false, WLED now knows it needs to save the defaults by calling addToConfig() + */ + bool readFromConfig(JsonObject& root) + { + // set defaults here, they will be set before setup() is called, and if any values parsed from ArduinoJson below are missing, the default will be used instead + fadeAmount = 5; + pins[0] = -1; + pins[1] = -1; + pins[2] = -1; + + JsonObject top = root["rotEncBrightness"]; + + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top["fadeAmount"], fadeAmount); + configComplete &= getJsonValue(top["pin"][0], pins[0]); + configComplete &= getJsonValue(top["pin"][1], pins[1]); + configComplete &= getJsonValue(top["pin"][2], pins[2]); + + return configComplete; + } +}; diff --git a/usermods/usermod_v2_auto_save/readme.md b/usermods/usermod_v2_auto_save/readme.md new file mode 100644 index 00000000..f54d87a7 --- /dev/null +++ b/usermods/usermod_v2_auto_save/readme.md @@ -0,0 +1,55 @@ +# Auto Save + +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_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()`. + +AutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes. + +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 + +Copy and update the example `platformio_override.ini.sample` +from the Rotary Encoder UI usermode 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_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 also configure auto-save parameters using Usermods settings page. + +### PlatformIO requirements + +No special requirements. + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-02 +* First public release +2021-04 +* 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 new file mode 100644 index 00000000..8283aeed --- /dev/null +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -0,0 +1,277 @@ +#pragma once + +#include "wled.h" + +// v2 Usermod to automatically save settings +// to configurable preset after a change to any of +// +// * brightness +// * effect speed +// * effect intensity +// * mode (effect) +// * palette +// +// but it will wait for configurable number of seconds, a "settle" +// period in case there are other changes (any change will +// extend the "settle" window). +// +// It can be configured to load auto saved preset at startup, +// during the first `loop()`. +// +// AutoSaveUsermod is standalone, but if FourLineDisplayUsermod +// is installed, it will notify the user of the saved changes. + +// format: "~ MM-DD HH:MM:SS ~" +#define PRESET_NAME_BUFFER_SIZE 25 + +class AutoSaveUsermod : public Usermod { + + private: + + bool firstLoop = true; + bool initDone = false; + 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; + + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + + #ifdef USERMOD_FOUR_LINE_DISPLAY + FourLineDisplayUsermod* display; + #endif + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _autoSaveEnabled[]; + static const char _autoSaveAfterSec[]; + static const char _autoSavePreset[]; + static const char _autoSaveApplyOnBoot[]; + + void inline saveSettings() { + char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; + updateLocalTime(); + sprintf_P(presetNameBuffer, + PSTR("~ %02d-%02d %02d:%02d:%02d ~"), + month(localTime), day(localTime), + hour(localTime), minute(localTime), second(localTime)); + cacheInvalidate++; // force reload of presets + savePreset(autoSavePreset, presetNameBuffer); + } + + void inline displayOverlay() { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display != nullptr) { + display->wakeDisplay(); + display->overlay("Settings", "Auto Saved", 1500); + } + #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 + // FourLineDisplayUsermod is available. + display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + #endif + initDone = true; + if (enabled && applyAutoSaveOnBoot) applyPreset(autoSavePreset); + knownBrightness = bri; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + knownMode = strip.getMainSegment().mode; + knownPalette = strip.getMainSegment().palette; + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() {} + + /* + * Da loop. + */ + void loop() { + if (!autoSaveAfterSec || !enabled || strip.isUpdating() || currentPreset>0) return; // setting 0 as autosave seconds disables autosave + + unsigned long now = millis(); + uint8_t currentMode = strip.getMainSegment().mode; + uint8_t currentPalette = strip.getMainSegment().palette; + + unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000; + if (knownBrightness != bri) { + knownBrightness = bri; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownEffectSpeed != effectSpeed) { + knownEffectSpeed = effectSpeed; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownEffectIntensity != effectIntensity) { + knownEffectIntensity = effectIntensity; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownMode != currentMode) { + knownMode = currentMode; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownPalette != currentPalette) { + knownPalette = currentPalette; + autoSaveAfter = wouldAutoSaveAfter; + } + + if (autoSaveAfter && now > autoSaveAfter) { + autoSaveAfter = 0; + // Time to auto save. You may have some flickry? + saveSettings(); + displayOverlay(); + } + } + + /* + * 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)); // 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(_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. + * 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 addToConfig(JsonObject& root) { + // we add JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_autoSaveEnabled)] = enabled; + top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam + top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam + top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot; + DEBUG_PRINTLN(F("Autosave 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 :) + * + * 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: {"Autosave": {"enabled": true, "autoSaveAfterSec": 10, "autoSavePreset": 250, ...}} + 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(_autoSaveEnabled)] | enabled; + autoSaveAfterSec = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec; + autoSaveAfterSec = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking + autoSavePreset = top[FPSTR(_autoSavePreset)] | autoSavePreset; + autoSavePreset = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking + applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot; + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(" config (re)loaded.")); + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return true; + } + + /* + * 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_AUTO_SAVE; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char AutoSaveUsermod::_name[] PROGMEM = "Autosave"; +const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled"; +const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec"; +const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset"; +const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot"; diff --git a/usermods/usermod_v2_four_line_display/readme.md b/usermods/usermod_v2_four_line_display/readme.md new file mode 100644 index 00000000..26250cb5 --- /dev/null +++ b/usermods/usermod_v2_four_line_display/readme.md @@ -0,0 +1,63 @@ +# I2C 4 Line Display Usermod + +First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod. + +Provides a four line display using either +128x32 or 128x64 OLED displays. +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. + +[See the pair of usermods in action](https://www.youtube.com/watch?v=tITQY80rIOA) + +## Installation + +Copy and update the example `platformio_override.ini.sample` +from the Rotary Encoder UI usermode 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_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 via the Usermods settings page, inluding GPIO pins. + +### PlatformIO requirements + +This usermod requires the `U8g2` and `Wire` libraries. See the +`platformio_override.ini.sample` found in the Rotary Encoder +UI usermod folder for how to include these using `platformio_override.ini`. + +## Configuration + +* `enabled` - enable/disable usermod +* `pin` - GPIO pins used for display; I2C displays use Clk & Data; SPI displays can use SCK, MOSI, CS, DC & RST +* `type` - display type in numeric format + * 1 = I2C SSD1306 128x32 + * 2 = I2C SH1106 128x32 + * 3 = I2C SSD1306 128x64 (4 double-height lines) + * 4 = I2C SSD1305 128x32 + * 5 = I2C SSD1305 128x64 (4 double-height lines) + * 6 = SPI SSD1306 128x32 + * 7 = SPI SSD1306 128x64 (4 double-height lines) +* `contrast` - set display contrast (higher contrast may reduce display lifetime) +* `refreshRateSec` - display refresh time in seconds +* `screenTimeOutSec` - screen saver time-out in seconds +* `flip` - flip/rotate display 180° +* `sleepMode` - enable/disable screen saver +* `clockMode` - enable/disable clock display in screen saver mode +* `i2c-freq-kHz` - I2C clock frequency in kHz (may help reduce dropped frames, range: 400-3400) + +## Change Log + +2021-02 +* First public release + +2021-04 +* Adaptation for runtime configuration. + +2021-11 +* 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 new file mode 100644 index 00000000..3fcf6612 --- /dev/null +++ b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h @@ -0,0 +1,742 @@ +#pragma once + +#include "wled.h" +#include // from https://github.com/olikraus/u8g2/ + +// +// Insired by the v1 usermod: ssd1306_i2c_oled_u8g2 +// +// 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. +// +// Make sure to enable NTP and set your time zone in WLED Config | Time. +// +// REQUIREMENT: You must add the following requirements to +// REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini +// REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) +// REQUIREMENT: * Wire +// + +//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 + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 19 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 26 + #endif +#else + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 12 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 16 + #endif +#endif + +#ifndef FLD_TYPE + #ifndef FLD_SPI_DEFAULT + #define FLD_TYPE SSD1306 + #else + #define FLD_TYPE SSD1306_SPI + #endif +#endif + +// When to time out to the clock or blank the screen +// if SLEEP_MODE_ENABLED. +#define SCREEN_TIMEOUT_MS 60*1000 // 1 min + +#define TIME_INDENT 0 +#define DATE_INDENT 2 + +// Minimum time between redrawing screen in ms +#define USER_LOOP_REFRESH_RATE_MS 1000 + +// Extra char (+1) for null +#define LINE_BUFFER_SIZE 16+1 + +typedef enum { + FLD_LINE_BRIGHTNESS = 0, + FLD_LINE_EFFECT_SPEED, + FLD_LINE_EFFECT_INTENSITY, + FLD_LINE_MODE, + FLD_LINE_PALETTE, + FLD_LINE_TIME +} Line4Type; + +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 +} DisplayType; + +class FourLineDisplayUsermod : public Usermod { + + private: + + bool initDone = false; + unsigned long lastTime = 0; + + // 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 + 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 + uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) + #endif + DisplayType type = FLD_TYPE; // display type + bool flip = false; // flip display 180° + uint8_t contrast = 10; // screen contrast + uint8_t lineHeight = 1; // 1 row or 2 rows + uint32_t refreshRate = USER_LOOP_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 enabled = true; + + // Next variables hold the previous known values to determine if redraw is + // required. + String knownSsid = ""; + IPAddress knownIp; + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + uint8_t knownMinute = 99; + uint8_t knownHour = 99; + + bool displayTurnedOff = false; + unsigned long lastUpdate = 0; + unsigned long lastRedraw = 0; + unsigned long overlayUntil = 0; + Line4Type lineType = FLD_LINE_BRIGHTNESS; + // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. + byte markLineNum = 0; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _contrast[]; + static const char _refreshRate[]; + static const char _screenTimeOut[]; + static const char _flip[]; + static const char _sleepMode[]; + static const char _clockMode[]; + static const char _busClkFrequency[]; + + // 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 + + public: + + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() { + if (type == NONE || !enabled) return; + + bool isHW; + PinOwner po = PinOwner::UM_FourLineDisplay; + if (type == SSD1306_SPI || type == SSD1306_SPI64) { + 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]==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 (!pinManager.allocateMultiplePins(pins, 2, po)) { type=NONE; return; } + } + + DEBUG_PRINTLN(F("Allocating display.")); + 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 + lineHeight = 1; + 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 + lineHeight = 2; + 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 + lineHeight = 2; + 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 + lineHeight = 1; + 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 + lineHeight = 2; + 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 + lineHeight = 1; + 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 + lineHeight = 2; + break; + default: + u8x8 = nullptr; + } + + if (nullptr == u8x8) { + DEBUG_PRINTLN(F("Display init failed.")); + pinManager.deallocateMultiplePins((const uint8_t*)ioPin, (type == SSD1306_SPI || type == SSD1306_SPI64) ? 5 : 2, po); + type = NONE; + return; + } + + initDone = true; + DEBUG_PRINTLN(F("Starting display.")); + /*if (!(type == SSD1306_SPI || type == SSD1306_SPI64))*/ u8x8->setBusClock(ioFrequency); // can be used for SPI too + u8x8->begin(); + setFlipMode(flip); + 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..."); + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() {} + + /** + * Da loop. + */ + void loop() { + if (!enabled || millis() - lastUpdate < (clockMode?1000:refreshRate) || strip.isUpdating()) return; + lastUpdate = millis(); + + 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); + } + 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; + } + } + + // Check if values which are shown on display changed from the last time. + if (forceRedraw || + (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) || + (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : Network.localIP())) || + (knownBrightness != bri) || + (knownEffectSpeed != effectSpeed) || + (knownEffectIntensity != effectIntensity) || + (knownMode != strip.getMainSegment().mode) || + (knownPalette != strip.getMainSegment().palette)) { + knownHour = 99; // force time update + lastRedraw = now; // update lastRedraw marker + } else if (sleepMode && !displayTurnedOff && ((now - lastRedraw)/1000)%5 == 0) { + // change line every 5s + showName = !showName; + switch (lineType) { + case FLD_LINE_BRIGHTNESS: + lineType = FLD_LINE_EFFECT_SPEED; + break; + case FLD_LINE_MODE: + lineType = FLD_LINE_BRIGHTNESS; + break; + case FLD_LINE_PALETTE: + lineType = clockMode ? FLD_LINE_MODE : FLD_LINE_BRIGHTNESS; + break; + case FLD_LINE_EFFECT_SPEED: + lineType = FLD_LINE_EFFECT_INTENSITY; + break; + case FLD_LINE_EFFECT_INTENSITY: + lineType = FLD_LINE_PALETTE; + break; + default: + lineType = FLD_LINE_MODE; + break; + } + knownHour = 99; // force time update + // do not update lastRedraw marker if just switching row contenet + } else { + // Nothing to change. + // Turn off display after 3 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. + sleepOrClock(true); + } else if (displayTurnedOff && clockMode) { + showTime(); + } + return; + } + + // Turn the display back on + if (displayTurnedOff) sleepOrClock(false); + + // Update last known values. + knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); + knownIp = apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); + knownBrightness = bri; + knownMode = strip.getMainSegment().mode; + knownPalette = strip.getMainSegment().palette; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + + // Do the actual drawing + String line; + // First row with Wifi name + drawGlyph(0, 0, 80, u8x8_font_open_iconic_embedded_1x1); // home icon + line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); + center(line, getCols()-2); + drawString(1, 0, line.c_str()); + // Print `~` char to indicate that SSID is longer, than our display + if (knownSsid.length() > (int)getCols()-1) { + drawString(getCols() - 1, 0, "~"); + } + + // Second row with IP or Psssword + 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) { + drawString(1, lineHeight, apPass); + } else { + // alternate IP address and server name + line = knownIp.toString(); + if (showName && strcmp(serverDescription, "WLED") != 0) { + line = serverDescription; + } + center(line, getCols()-1); + drawString(1, lineHeight, line.c_str()); + } + + // draw third and fourth row + drawLine(2, clockMode ? lineType : FLD_LINE_MODE); + drawLine(3, clockMode ? FLD_LINE_TIME : lineType); + + drawGlyph(0, 2*lineHeight, 66 + (bri > 0 ? 3 : 0), u8x8_font_open_iconic_weather_2x2); // sun/moon icon + //if (markLineNum>1) drawGlyph(2, markLineNum*lineHeight, 66, u8x8_font_open_iconic_arrow_1x1); // arrow icon + } + + void drawLine(uint8_t line, Line4Type lineType) { + char lineBuffer[LINE_BUFFER_SIZE]; + uint8_t printedChars; + switch(lineType) { + case FLD_LINE_BRIGHTNESS: + sprintf_P(lineBuffer, PSTR("Brightness %3d"), bri); + drawString(2, line*lineHeight, lineBuffer); + break; + case FLD_LINE_EFFECT_SPEED: + sprintf_P(lineBuffer, PSTR("FX Speed %3d"), effectSpeed); + drawString(2, line*lineHeight, lineBuffer); + break; + case FLD_LINE_EFFECT_INTENSITY: + sprintf_P(lineBuffer, PSTR("FX Intens. %3d"), effectIntensity); + drawString(2, line*lineHeight, lineBuffer); + break; + case FLD_LINE_MODE: + printedChars = extractModeName(knownMode, JSON_mode_names, lineBuffer, LINE_BUFFER_SIZE-1); + for (;printedChars < getCols()-2 && printedChars < LINE_BUFFER_SIZE-3; printedChars++) lineBuffer[printedChars]=' '; + lineBuffer[printedChars] = 0; + drawString(2, line*lineHeight, lineBuffer); + break; + case FLD_LINE_PALETTE: + printedChars = extractModeName(knownPalette, JSON_palette_names, lineBuffer, LINE_BUFFER_SIZE-1); + for (;printedChars < getCols()-2 && printedChars < LINE_BUFFER_SIZE-3; printedChars++) lineBuffer[printedChars]=' '; + lineBuffer[printedChars] = 0; + drawString(2, line*lineHeight, lineBuffer); + break; + case FLD_LINE_TIME: + default: + showTime(false); + break; + } + } + + /** + * 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 wakeDisplay() { + if (type == NONE || !enabled) return false; + knownHour = 99; + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + redraw(true); + return true; + } + return false; + } + + /** + * Allows you to show up to two lines as overlay for a + * period of time. + * Clears the screen and prints on the middle two lines. + */ + void overlay(const char* line1, const char *line2, long showHowLong) { + if (type == NONE || !enabled) return; + + if (displayTurnedOff) { + // Turn the display back on (includes clear()) + sleepOrClock(false); + } else { + 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 setLineType(byte lT) { + lineType = (Line4Type) lT; + } + + /** + * Line 3 or 4 (last two lines) can be marked with an + * arrow in the first column. Pass 2 or 3 to this to + * specify which line to mark with an arrow. + * Any other values are ignored. + */ + void setMarkLine(byte newMarkLineNum) { + if (newMarkLineNum == 2 || newMarkLineNum == 3) { + markLineNum = newMarkLineNum; + } + else { + markLineNum = 0; + } + } + + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool enabled) { + clear(); + if (enabled) { + if (clockMode) showTime(); + else setPowerSave(1); + displayTurnedOff = true; + } else { + setPowerSave(0); + displayTurnedOff = false; + } + } + + /** + * 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(bool fullScreen = true) { + if (type == NONE || !enabled) return; + char lineBuffer[LINE_BUFFER_SIZE]; + + updateLocalTime(); + byte minuteCurrent = minute(localTime); + byte hourCurrent = hour(localTime); + byte secondCurrent = second(localTime); + if (knownMinute == minuteCurrent && knownHour == hourCurrent) { + // Time hasn't changed. + if (!fullScreen) return; + } + knownMinute = minuteCurrent; + knownHour = hourCurrent; + + byte currentMonth = month(localTime); + sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(currentMonth), day(localTime)); + if (fullScreen) + draw2x2String(DATE_INDENT, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays + else + drawString(2, lineHeight*3, lineBuffer); + + byte showHour = hourCurrent; + boolean isAM = false; + if (useAMPM) { + if (showHour == 0) { + showHour = 12; + isAM = true; + } + else if (showHour > 12) { + showHour -= 12; + isAM = false; + } + else { + isAM = true; + } + } + + sprintf_P(lineBuffer, (secondCurrent%2 || !fullScreen) ? PSTR("%2d:%02d") : PSTR("%2d %02d"), (useAMPM ? showHour : hourCurrent), minuteCurrent); + // For time, we always use LINE_HEIGHT of 2 since + // we are printing it big. + if (fullScreen) { + draw2x2String(TIME_INDENT+2, lineHeight*2, lineBuffer); + sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); + if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*2, (isAM ? "AM" : "PM"), true); + else drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line + } else { + drawString(9+(useAMPM?0:2), lineHeight*3, lineBuffer); + if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*3, (isAM ? "AM" : "PM"), true); + } + } + + /* + * 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.")); + //} + + /* + * 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() + //} + + /* + * 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 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(_refreshRate)] = refreshRate/1000; + top[FPSTR(_screenTimeOut)] = screenTimeout/1000; + top[FPSTR(_sleepMode)] = (bool) sleepMode; + top[FPSTR(_clockMode)] = (bool) clockMode; + 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 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/1000) * 1000; + screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; + sleepMode = top[FPSTR(_sleepMode)] | sleepMode; + clockMode = top[FPSTR(_clockMode)] | clockMode; + 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; + 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; + return true; + } else type = newType; + setup(); + needsRedraw |= true; + } + if (!(type == SSD1306_SPI || type == SSD1306_SPI64)) u8x8->setBusClock(ioFrequency); // can be used for SPI too + setContrast(contrast); + setFlipMode(flip); + if (needsRedraw && !wakeDisplay()) redraw(true); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_enabled)].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_FOUR_LINE_DISP; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char FourLineDisplayUsermod::_name[] PROGMEM = "4LineDisplay"; +const char FourLineDisplayUsermod::_enabled[] PROGMEM = "enabled"; +const char FourLineDisplayUsermod::_contrast[] PROGMEM = "contrast"; +const char FourLineDisplayUsermod::_refreshRate[] PROGMEM = "refreshRateSec"; +const char FourLineDisplayUsermod::_screenTimeOut[] PROGMEM = "screenTimeOutSec"; +const char FourLineDisplayUsermod::_flip[] PROGMEM = "flip"; +const char FourLineDisplayUsermod::_sleepMode[] PROGMEM = "sleepMode"; +const char FourLineDisplayUsermod::_clockMode[] PROGMEM = "clockMode"; +const char FourLineDisplayUsermod::_busClkFrequency[] PROGMEM = "i2c-freq-kHz"; diff --git a/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c b/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c new file mode 100644 index 00000000..5495f919 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c @@ -0,0 +1,477 @@ +#pragma once + +//WLED custom fonts, curtesy of @Benji (https://github.com/Proto-molecule) + + +/* + Fontname: wled_logo_akemi_4x4 + Copyright: Benji (https://github.com/proto-molecule) + Glyphs: 3/3 + BBX Build Mode: 3 + * this logo ...WLED/images/wled_logo_akemi.png + * encode map = 1, 2, 3 +*/ +const uint8_t u8x8_wled_logo_akemi_4x4[388] U8X8_FONT_SECTION("u8x8_wled_logo_akemi_4x4") = + "\1\3\4\4\0\0\0\0\0\0\0\0\0\340\360\10\350\10\350\210\270\210\350\210\270\350\10\360\340\0\0\0" + "\0\0\200\200\0\0@\340\300\340@\0\0\377\377\377\377\377\377\37\37\207\207\371\371\371\377\377\377\0\0\374" + "\374\7\7\371\0\0\6\4\15\34x\340\200\177\177\377\351yy\376\356\357\217\177\177\177o\377\377\0\70\77" + "\277\376~\71\0\0\0\0\0\0\0\1\3\3\3\1\0\0\37\77\353\365\77\37\0\0\0\0\5\7\2\3" + "\7\4\0\0\300\300\300\300\200\200\200\0\0\0\0\0\0\0\200\200\300\300\300\300\200\200\0\0\0\0\0\0" + "\0\200\200\300\371\37\37\371\371\7\7\377\374\0\0\0\374\377\377\37\37\341\341\377\377\377\377\374\0\0\0\374" + "\377\7\7\231\371\376>\371\371>~\377\277\70\0\270\377\177\77\376\376\71\371\371\71\177\377\277\70\0\70\377" + "\177>\376\371\377\377\0\77\77\0\0\4\7\2\7\5\0\0\0\377\377\0\77\77\0\0\0\5\7\2\7\5" + "\0\0\377\377\300\300\300\200\200\0\0\0\0\0\0\0\200\200\300\300\300\300\300\200\200\0\0\0\0\0\0\0" + "\0\0\0\0\231\231\231\371\377\377\374\0\0\0\374\377\347\347\371\1\1\371\371\7\7\377\374\0\0\0@\340" + "\300\340@\0\71\371\371\71\177\377\277\70\0\70\277\377\177\71\371\370\70\371\371~\376\377\77\70\200\340x\34" + "\15\4\6\0\0\77\77\0\0\0\5\7\2\7\5\0\0\0\377\377\0\77\77\0\0\1\3\3\1\1\0\0" + "\0\0\0"; + + +/* + Fontname: wled_logo_akemi_5x5 + Copyright: Benji (https://github.com/proto-molecule) + Glyphs: 3/3 + BBX Build Mode: 3 + * this logo ...WLED/images/wled_logo_akemi.png + * encoded = 1, 2, 3 +*/ +/* +const uint8_t u8x8_wled_logo_akemi_5x5[604] U8X8_FONT_SECTION("u8x8_wled_logo_akemi_5x5") = + "\1\3\5\5\0\0\0\0\0\0\0\0\0\0\0\0\340\340\374\14\354\14\354\14|\14\354\14||\14\354" + "\14\374\340\340\0\0\0\0\0\0\0\200\0\0\0\200\200\0\200\200\0\0\0\0\377\377\377\376\377\376\377\377" + "\377\377\77\77\307\307\307\307\306\377\377\377\0\0\0\360\374>\77\307\0\0\61cg\357\347\303\301\200\0\0" + "\377\377\377\317\317\317\317\360\360\360\374\374\377\377\377\377\377\377\377\377\0\0\200\377\377\340\340\37\0\0\0\0" + "\0\0\1\3\17\77\374\360\357\357\177\36\14\17\357\377\376\376>\376\360\357\17\17\14>\177o\340\300\343c" + "{\77\17\3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\17\37\37\362\375\37\37\17\0\0" + "\0\0\1\1\1\0\1\1\1\0\0\0\200\300\300\300\300\200\200\0\0\0\0\0\0\0\0\0\0\0\200\200" + "\300\300\300\300\200\200\0\0\0\0\0\0\0\0\0\0\0\0\200\200\307\307\377\377\307\307\307\77>\374\360\0" + "\0\0\360\374\376\377\377\377\7\7\7\377\377\377\377\376\374\360\0\0\0\0\360\374\36\37\37\343\37\37\340\340" + "\37\37\37\340\340\377\377\200\0\200\377\377\377\340\340\340\37\37\37\37\37\37\37\377\377\377\200\0\0\200\377\377" + "\340\340\340\34\377\377\3\3\377\377\3\17\77{\343\303\300\303\343s\77\37\3\377\377\3\3\377\377\3\17\77" + "{\343\303\300\300\343{\37\17\3\377\377\377\377\0\0\37\37\0\0\1\1\1\1\0\1\1\1\1\0\0\377" + "\377\0\0\37\37\0\0\1\1\1\1\0\0\1\1\1\0\0\377\377\300\300\300\200\200\0\0\0\0\0\0\0" + "\0\0\0\0\200\200\300\300\300\300\200\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\343\343\343\343" + "\343\377\376\374\360\0\0\0\360\374\376\77\77\307\307\7\7\307\307\307\77>\374\360\0\0\0\0\0\200\200\0" + "\200\200\0\0\34\34\34\37\37\377\377\377\377\200\0\200\377\377\377\377\37\37\37\0\0\37\37\37\340\340\377\377" + "\200\0\0\0\1\303\347\357gc\61\0\3\3\377\377\3\7\37\177s\343\300\303s{\37\17\7\3\377\377" + "\3\3\377\377\3\37\77scp<\36\17\3\1\0\0\0\0\0\0\0\37\37\0\0\0\1\1\1\0\1" + "\1\1\0\0\0\0\377\377\0\0\37\37\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; +*/ + +/* + Fontname: wled_logo_2x2 + Copyright: Benji (https://github.com/proto-molecule) + Glyphs: 4/4 + BBX Build Mode: 3 + * this logo https://cdn.discordapp.com/attachments/706623245935444088/927361780613799956/wled_scaled.png + * encode map = 1, 2, 3, 4 +*/ +const uint8_t u8x8_wled_logo_2x2[133] U8X8_FONT_SECTION("u8x8_wled_logo_2x2") = + "\1\4\2\2\0\0\0\0\0\200\200\360\360\16\16\16\16\0\0\0\340\340\340\340\340\37\37\1\1\0\0\0" + "\0\0\0\0\360\360\16\16\16\200\200\16\16\16\360\360\0\0\0\200\37\37\340\340\340\37\37\340\340\340\37\37" + "\0\0\0\37\200~~\0\0\0\0\0\0\0\360\360\216\216\216\216\37\340\340\340\340\340\340\340\0\0\37\37" + "\343\343\343\343\16\16\0\0ppp\16\16\376\376\16\16\16\360\360\340\340\0\0\0\0\0\340\340\377\377\340" + "\340\340\37\37"; + + +/* + Fontname: wled_logo_4x4 + Copyright: Created with Fony 1.4.7 + Glyphs: 4/4 + BBX Build Mode: 3 + * this logo https://cdn.discordapp.com/attachments/706623245935444088/927361780613799956/wled_scaled.png + * encode map = 1, 2, 3, 4 +*/ +/* +const uint8_t u8x8_wled_logo_4x4[517] U8X8_FONT_SECTION("u8x8_wled_logo_4x4") = + "\1\4\4\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\374\374\374\374\374\374\374\374\374" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\300\300\300\300\300\377\377\377\377\377\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\17\17\17\17\17\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\370\370\370\370\370\370\370\370\370\7\7\7\7\7\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\374\374\374\374\374\0\0\0\0\0\374\374\374\374\374\0\0\0\0\0\0\0" + "\0\0\0\0\0\377\377\377\377\377\0\0\0\0\0\300\300\300\300\300\0\0\0\0\0\377\377\377\377\377\0\0" + "\0\0\300\300\0\377\377\377\377\377\0\0\0\0\0\377\377\377\377\377\0\0\0\0\0\377\377\377\377\377\0\0" + "\0\0\377\377\0\7\7\7\7\7\370\370\370\370\370\7\7\7\7\7\370\370\370\370\370\7\7\7\7\7\0\0" + "\0\0\7\7\0\0\0\374\374\374\374\374\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\374\374\374" + "\374\374\374\374\300\300\300\77\77\77\77\77\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\300\300\300" + "\300\300\300\300\377\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\37\37\37" + "\37\37\37\37\7\7\7\370\370\370\370\370\370\370\370\370\370\370\370\370\0\0\0\0\7\7\7\7\7\370\370\370" + "\370\370\370\370\374\374\374\374\374\374\0\0\0\0\0\0\0\0\374\374\374\374\374\374\374\374\374\374\374\374\374\374" + "\0\0\0\0\300\300\0\0\0\0\0\0\0\77\77\77\77\77\0\0\0\0\377\377\377\377\377\0\0\0\0\377" + "\377\377\377\377\37\37\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\0\0\0\0\377" + "\377\377\377\377\370\370\370\370\370\370\0\0\0\0\0\0\0\0\370\370\370\370\377\377\377\377\377\370\370\370\370\377" + "\7\7\7\7"; +*/ + + +/* + Fontname: 4LineDisplay_WLED_icons_1x + Copyright: Benji (https://github.com/proto-molecule) + Glyphs: 13/13 + BBX Build Mode: 3 + * 1 = sun + * 2 = skip forward + * 3 = fire + * 4 = custom palette + * 5 = puzzle piece + * 6 = moon + * 7 = brush + * 8 = contrast + * 9 = power-standby + * 10 = star + * 11 = heart + * 12 = Akemi + *----------- + * 20 = wifi + * 21 = media-play +*/ +const uint8_t u8x8_4LineDisplay_WLED_icons_1x1[172] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_1x1") = + "\1\25\1\1\0B\30<<\30B\0~<\30\0~<\30\0p\374\77\216\340\370\360\0||>\36\14\64 \336\67" + ";\336 \64\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\2\1\11\311" + "\311\1\2\0\0~<<\30\30\0"; + + +/* + Fontname: 4LineDisplay_WLED_icons_2x1 + Copyright: Benji (https://github.com/proto-molecule) + Glyphs: 11/11 + BBX Build Mode: 3 + * 1 = sun + * 2 = skip forward + * 3 = fire + * 4 = custom palette + * 5 = puzzle piece + * 6 = moon + * 7 = brush + * 8 = contrast + * 9 = power-standby + * 10 = star + * 11 = heart + * 12 = Akemi +*/ +const uint8_t u8x8_4LineDisplay_WLED_icons_2x1[196] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_2x1") = + "\1\14\2\1\20\20BB\30\30<\275\275<\30\30BB\20\20\377~<<\70\30\20\0\377~<<" + "\70\30\20\0\60p\370\374\77>\236\214\300\340\370\360\360\340\0\0\34" + "\66\66<\34\374\374\374\374~\77\77~\374\374\374\374 pp \30<~~\377\370\360\360\340\340\340\340" + "@@ \0\200\300\340\360\360p`\10\34\34\16\6\6\3\0\0\70|~\376\376\377\377\377\201\201\203\202" + "\302Fl\70\70xL\204\200\200\217\217\200\200\204Lx\70\0\0\10\10\30\330x|\77\77|x\330\30" + "\10\10\0\0\14\36\37\77\77\177~\374\374~\177\77\77\37\36\14\24\64 \60>\26\367\33\375\36>\60" + " \64\24"; + + +/* + Fontname: 4LineDisplay_WLED_icons_2x + Copyright: + Glyphs: 11/11 + BBX Build Mode: 3 + * 1 = sun + * 2 = skip forward + * 3 = fire + * 4 = custom palette + * 5 = puzzle piece + * 6 = moon + * 7 = brush + * 8 = contrast + * 9 = power-standby + * 10 = star + * 11 = heart + * 12 = Akemi +*/ +const uint8_t u8x8_4LineDisplay_WLED_icons_2x2[389] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_2x2") = + "\1\14\2\2\200\200\14\14\300\340\360\363\363\360\340\300\14\14\200\200\1\1\60\60\3\7\17\317\317\17\7\3" + "\60\60\1\1\374\370\360\340\340\300\200\0\374\370\360\340\340\300\200\0\77\37\17\7\7\3\1\0\77\37\17\7" + "\7\3\1\0\0\200\340\360\377\376\374\360\0\0\300\200\0\0\0\0\17\77\177\377\17\7\301\340\370\374\377\377" + "\377|\0\0\360\370\234\236\376\363\363\377\377\363\363\376><\370\360\3\17\77yy\377\377\377\377\317\17\17" + "\17\17\7\3\360\360\360\360\366\377\377\366\360\360\360\360\0\0\0\0\377\377\377\377\237\17\17\237\377\377\377\377" + "\6\17\17\6\340\370\374\376\377\340\200\0\0\0\0\0\0\0\0\0\3\17\37\77\177\177\177\377\376|||" + "\70\30\14\0\0\0\0\0\0\0\0``\360\370|<\36\7\2\0\300\360\376\377\177\77\36\0\1\1\0" + "\0\0\0\0\340\370\374\376\376\377\377\377\3\3\7\6\16<\370\340\7\37\77\177\177\377\377\377\300\300\340`" + "p<\37\7\300\340p\30\0\0\377\377\0\0\30p\340\300\0\0\17\37\70`\340\300\300\300\300\340`\70" + "\37\17\0\0\0@\300\300\300\300\340\374\374\340\300\300\300\300@\0\0\0\0\1s\77\37\17\17\37\77s" + "\1\0\0\0\360\370\374\374\374\374\370\360\360\370\374\374\374\374\370\360\0\1\3\7\17\37\77\177\177\77\37\17" + "\7\3\1\0\200\200\0\0\0\360\370\374<\334\330\360\0\0\200\200\2\2\14\30\24\37\6~\7\177\7\37" + "\24\30\16\2"; + +/* + Fontname: 4LineDisplay_WLED_icons_3x + Copyright: Benji (https://github.com/proto-molecule) + Glyphs: 11/11 + BBX Build Mode: 3 + * 1 = sun + * 2 = skip forward + * 3 = fire + * 4 = custom palette + * 5 = puzzle piece + * 6 = moon + * 7 = brush + * 8 = contrast + * 9 = power-standby + * 10 = star + * 11 = heart + * 12 = Akemi +*/ +const uint8_t u8x8_4LineDisplay_WLED_icons_3x3[868] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_3x3") = + "\1\14\3\3\0\0\34\34\34\0\200\300\300\340\347\347\347\340\300\300\200\0\34\34\34\0\0\0\34\34\34\0" + "\0>\377\377\377\377\377\377\377\377\377\377\377>\0\0\34\34\34\0\0\0\16\16\16\0\0\1\1\3ss" + "s\3\1\1\0\0\34\34\34\0\0\0\370\360\340\300\300\200\0\0\0\0\0\0\370\360\340\300\300\200\0\0" + "\0\0\0\0\377\377\377\377\377\377\377\376~<\70\20\377\377\377\377\377\377\377\376~<\70\20\37\17\17\7" + "\3\1\1\0\0\0\0\0\37\17\17\7\3\1\1\0\0\0\0\0\0\0\0\0\0\300\361\376\374\370\360\300" + "\0\0\0\0\0\0\0\0\0\0\0\0\300\370\374\376\377\377\377\377\377\177\77\17\6\0\200\342\374\370\360\340" + "\200\0\0\0\1\17\37\77\177\377\7\3\0\200\360\370\374\376\377\377\377\377\377\377\77\0\0\0\0\200\340\360" + "\370\370\374\316\206\206\317\377\377\377\317\206\206\316\374\374\370\360\340\200<\377\377\371\360py\377\377\377\377\377" + "\377\377\377\377\377\377\363\341\341\363\377\177\0\1\7\17\34\70x|\377\377\377\377\367\363c\3\3\3\3\1" + "\1\1\0\0\300\300\300\300\300\300\300\316\377\377\377\316\300\300\300\300\300\300\0\0\0\0\0\0\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\300\300\340\340\340\300\377\377\377\377\377\377\377\307\3\3\3\307" + "\377\377\377\377\377\377\1\1\3\3\3\1\0\300\340\370\374\374\376\377\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0>\377\377\377\377\377\377\377\377\374\360\340\300\300\200\200\0\0\0\0\0\0\200\200\0\1\7\17" + "\37\37\77\177\177\177\177\377\377\377\177\177\177\77\77\37\17\7\3\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\200\200\300\340\340\360\370\374|>\17\6\0\0\0\0\0\340\340\360\360\360\342\303\7\17\37\77\37\7\3\1" + "\0\0\0\0\0\200\340\360\377\377\377\377\177\77\37\17\0\0\0\0\0\0\0\0\0\0\0\0\0\200\340\360" + "\370\374\374\376\376\376\377\377\7\7\7\6\16\16\34\70\360\340\300\0|\377\377\377\377\377\377\377\377\377\377\377" + "\0\0\0\0\0\0\0\0\0\377\377\377\0\3\7\17\37\77\177\177\377\377\377\377\340\340\340\340pp\70<" + "\37\17\3\0\0\0\200\300\340\340\300\0\0\377\377\377\0\0\300\340\340\300\200\0\0\0\0\0\370\376\377\17" + "\3\0\0\0\0\17\17\17\0\0\0\0\0\3\17\377\376\370\0\0\0\7\17\37~\376\376\377\377\377\377\377\376\376~>\36\16\6\6\2\0\0\0\0" + "\0\300x<\37\17\17\7\3\7\17\17\37>\177\177\377\377\377\377\377\377\371p\60\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0<\376\377\377\377\377\376<\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0" + "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377~~\377\377" + "\377\377~<\377\377\377\377\377\377\377\377\303\1\0\0\0\0\1\303\377\377\377\377\377\377\377\377\0\0\0\0" + "\0\0\0\0\0\0\200\340\360\370\374\374\376\376\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\370\377\377\377\377\377\377\377\377\377\376\360\300\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\7\77\377\377\377\377\377\377\377\377\377\377\377\377\377\376\374\370\370\360\360\360\340\340\340\340\340\340" + "\340\340\60\0\0\0\0\1\3\7\17\37\37\77\77\77\177\177\177\177\177\177\177\177\77\77\77\37\37\17\7\3" + "\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\200\300\340\340\360\370\374\374" + "~\77\16\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\30\34>~\377\377\377\377\177\77\37\7\3\0" + "\0\0\0\0\0\0\0\0\0\360\374\376\377\377\377\377\377\376\374\370\0\0\0\3\3\1\0\0\0\0\0\0" + "\0\0\0\0@@\340\370\374\377\377\377\177\177\177\77\37\17\7\1\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\200\300\340\360\370\374\374\376\376\376\377\377\377\377\17\17\17\37\36\36>|\374\370\360\340" + "\300\200\0\0\360\376\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\1\3\37" + "\377\377\376\360\17\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\200\300\370" + "\377\377\177\17\0\0\1\3\7\17\37\77\77\177\177\177\377\377\377\377\360\360\360\370xx|>\77\37\17\7" + "\3\1\0\0\0\0\0\0\0\200\300\200\0\0\0\0\377\377\377\377\0\0\0\0\200\300\200\0\0\0\0\0" + "\0\0\0\0\300\360\374\376\177\37\7\3\3\0\0\0\377\377\377\377\0\0\0\3\3\7\37\177\376\374\360\300" + "\0\0\0\0\77\377\377\377\340\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\340\377\377\377\77" + "\0\0\0\0\0\0\3\7\17\37><|x\370\360\360\360\360\360\360\370x|<>\37\17\7\3\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\340\374\374\340\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\20\60p\360\360\360\360\360\360\360\360\370\377\377\377\377\377\377\370\360\360\360\360\360\360\360\360" + "p\60\20\0\0\0\0\0\0\0\1\3\7\317\377\377\377\377\377\377\377\377\377\377\377\377\317\7\3\1\0\0" + "\0\0\0\0\0\0\0\0\0\0\0p>\37\17\17\7\3\1\0\0\1\3\7\17\17\37>p\0\0\0" + "\0\0\0\0\0\200\300\340\340\360\360\360\360\360\360\340\340\300\200\0\0\200\300\340\340\360\360\360\360\360\360\340" + "\340\300\200\0~\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376\376\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377~\0\1\3\7\17\37\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\17" + "\7\3\1\0\0\0\0\0\0\0\0\0\0\1\3\7\17\37\77\177\177\77\37\17\7\3\1\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\300\340\340\360\360\360\360\340\340\300\200\0\0\0\0\0\0" + "\0\0\0\0\0@\340\300\340@\0\0\0\376\377\377\177\177\177\237\207\347\371\371\371\377\376\0\0\0\0@" + "\340\300\340@\2\4\4\35x\340\200\0\30\237\377\177\36\376\376\37\37\377\377\37\177\377\237\30\0\200\340x" + "\34\5\4\2\0\0\0\0\0\1\3\3\3\1\0\0\0\17\17\0\0\17\17\0\0\0\1\3\3\3\1\0" + "\0\0\0"; +*/ + +/* + Fontname: 4LineDisplay_WLED_icons_6x + Copyright: Benji (https://github.com/proto-molecule) + Glyphs: 11/11 + BBX Build Mode: 3 + * 1 = sun + * 2 = skip forward + * 3 = fire + * 4 = custom palette + * 5 = puzzle piece + * 6 = moon + * 7 = brush + * 8 = contrast + * 9 = power-standby + * 10 = star + * 11 = heart + * 12 = Akemi +*/ +// you can replace this (wasteful) font by using 3x3 variant with draw2x2Glyph() +const uint8_t u8x8_4LineDisplay_WLED_icons_6x6[3460] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_6x6") = + "\1\14\6\6\0\0\0\0\0\0\200\300\300\300\300\200\0\0\0\0\0\0\0\0\0\36\77\77\77\77\36\0" + "\0\0\0\0\0\0\0\0\200\300\300\300\300\200\0\0\0\0\0\0\0\0\0\0\0\0\7\17\17\17\17\7" + "\0\0\0\0\200\300\340\340\340\360\360\360\360\360\360\340\340\340\300\200\0\0\0\0\7\17\17\17\17\7\0\0" + "\0\0\0\0\300\340\340\340\340\300\0\0\0\0\0\0\340\374\376\377\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\376\374\340\0\0\0\0\0\0\300\340\340\340\340\300\3\7\7\7\7\3\0\0\0\0\0\0" + "\7\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\7\0\0\0\0\0\0\3\7" + "\7\7\7\3\0\0\0\0\0\0\340\360\360\360\360\340\0\0\0\0\1\3\7\7\7\17\17\17\17\17\17\7" + "\7\7\3\1\0\0\0\0\340\360\360\360\360\340\0\0\0\0\0\0\0\0\0\0\0\0\1\3\3\3\3\1" + "\0\0\0\0\0\0\0\0\0x\374\374\374\374x\0\0\0\0\0\0\0\0\0\1\3\3\3\3\1\0\0" + "\0\0\0\0\300\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\200\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\376\376\374\370\360\360\340\300\200" + "\200\0\0\0\0\0\0\0\0\0\0\0\377\377\377\376\376\374\370\360\360\340\300\200\200\0\0\0\0\0\0\0" + "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376\374\374\370\360\340\340\300\200\0\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\376\374\374\370\360\340\340\300\200\0\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\177\77\77\37\17\7\7\3\1\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\77\37\17\7" + "\7\3\1\0\377\377\377\177\177\77\37\17\17\7\3\1\1\0\0\0\0\0\0\0\0\0\0\0\377\377\377\177" + "\177\77\37\17\17\7\3\1\1\0\0\0\0\0\0\0\0\0\0\0\3\1\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\3\1\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\1\376\374\374\370\360\340\300\200\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\200\340\360\374" + "\377\377\377\377\377\377\377\377\377\376\370\300\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\300\340\360\374\376\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\37\0\0\0\0" + "\0\0\4\370\360\360\340\300\200\0\0\0\0\0\0\0\0\0\0\0\370\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\177\77\37\7\3\0\0\0\0\0\200\300\360\374\377\377\377\377\377\377\377\376\370\340\0\0\0" + "\0\0\0\0\3\37\177\377\377\377\377\377\377\377\377\377\77\17\7\1\0\0\0\0\0\200\300\360\370\374\376\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\1\3\7\17\37\77\77\177\200" + "\0\0\0\0\0\0\340\374\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\17\1\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\200\300\340\340\360\360\370|<>>>~\377\377\377\377\377\377\377\177" + "\77\36\36\36\36<|\370\370\360\360\340\340\200\0\0\0\0\0\0\0\0\300\360\374\376\377\377\377\377\377\377" + "\377\360\340\300\300\300\300\340\360\377\377\377\377\377\377\370\360\340\340\340\340\360\370\377\377\377\377\377\377\377\377\377" + "\374\360\340\200\360\377\377\377\377\377\207\3\1\1\1\1\3\207\377\377\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\207\3\1\1\1\1\3\207\377\377\377\377\377\17\377\377\377\377\377\377\377\376~>>" + "\77\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376\376\376\376\377\377\377" + "\177\77\37\7\0\0\3\17\77\177\377\377\360\340\300\300\300\300\340\360\377\377\377\377\377\377\377\377\377\377\77\17" + "\17\7\7\7\7\7\7\7\7\7\3\3\3\3\1\0\0\0\0\0\0\0\0\0\0\0\0\1\3\7\17\37" + "\37\77\77\177\177\177\377\377\377\377\377\377\377\377\377~\30\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\370\374\376\377\377\377\377\377\377\376\374\360\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\360\360\360\360\360\360\360\360\360\360\360\360" + "\360\363\377\377\377\377\377\377\377\377\363\360\360\360\360\360\360\360\360\360\360\360\360\360\0\0\0\0\0\0\0\0" + "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\374\374\376\376\377\377\377\377" + "\377\376\374\360\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\17\17\17\17\17\17\37\77\177\377\377\377\377" + "\377\377\377\377\377\377\377\377\3\3\7\7\17\17\17\17\7\7\3\0\377\377\377\377\377\377\377\377\377\377\377\377" + "\360\300\0\0\0\0\0\0\0\0\300\360\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\200\300\340\360\360\370\374\374\376\376\7\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\300\360\374\376\377\377\377\377\377\377\377" + "\377\377\377\340\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\374\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\374\360\300\200\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\17\177\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\376\374\370\360\360\340\340\300\300\300\200\200\200\200\0\0\0\0\0\0\200\200" + "\200\200\0\0\0\0\1\7\37\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\7\1\0\0\0\0\0\0\0\0\0\0\1\3\3\7" + "\17\17\37\37\37\77\77\77\77\177\177\177\177\177\177\77\77\77\77\37\37\37\17\17\7\3\3\1\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\200\200\300\340\360\360\370\374\374\376\377~\34\10\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\200\300\300\340\360\360\370\374\376\376\377\377\377\377\377\377\177\77\17\7\3" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\4\6\17\17\37\77\177\377" + "\377\377\377\377\377\377\77\37\7\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\370\374\376" + "\376\377\377\377\377\377\377\376\376\374\370\340\0\0\0\0\3\17\7\3\1\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\200\360\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\17\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`px\374\376\377\377\377\377\377\377" + "\177\177\177\77\77\37\17\7\3\1\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\200\300\340\360\360\370\374\374\374\376\376\376\377\377\377\377\377\77\77\77\77" + "\177~~\376\374\374\374\370\360\360\340\300\200\0\0\0\0\0\0\0\0\0\340\360\374\376\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\1\1\3\7\17\37\177\377\377\376\374" + "\360\340\0\0\370\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\1\17\377\377\377\377\377\370\37\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\360\377\377" + "\377\377\377\37\0\0\7\17\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0" + "\0\0\0\0\0\200\200\300\340\360\370\376\377\377\177\77\17\7\0\0\0\0\0\0\0\0\0\1\3\7\17\17" + "\37\77\77\77\177\177\177\377\377\377\377\377\374\374\374\374\376~~\177\77\77\77\37\17\17\7\3\1\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\377\377\377\377\377\377\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\200\300\340\360\370\374\376\376|" + "x \0\0\0\0\377\377\377\377\377\377\0\0\0\0 x|\376\376\374\370\360\340\300\200\0\0\0\0\0" + "\0\0\0\0\300\370\376\377\377\377\177\17\7\1\0\0\0\0\0\0\0\0\377\377\377\377\377\377\0\0\0\0" + "\0\0\0\0\1\7\37\177\377\377\377\376\370\200\0\0\0\0\0\0\177\377\377\377\377\377\200\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\200\377\377\377\377\377\177\0\0" + "\0\0\0\0\0\7\37\177\377\377\377\374\370\340\300\200\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\200\200\300\340\370\374\377\377\377\177\37\7\0\0\0\0\0\0\0\0\0\0\0\0\1\3\7\17\37\37\77" + "\77\177~~~\374\374\374\374\374\374\374\374~~~\177\77\77\37\37\17\7\3\1\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\340\374\374\340\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\300\370\377\377\377\377\377\377\370\300\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\4\14\34<<|\374\374\374\374\374\374\374\374\374\374\374\376\377\377\377\377\377\377\377\377\377" + "\377\376\374\374\374\374\374\374\374\374\374\374\374|<<\34\14\4\0\0\0\0\0\0\0\0\0\1\3\3\7" + "\17\37\77\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\77\37\17\7\3\3\1\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\370\377\377\377\377\377\377\177\77\37\17\17\37\77\177" + "\377\377\377\377\377\377\370\300\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0p>" + "\37\17\17\7\3\1\0\0\0\0\0\0\0\0\0\0\0\0\1\3\7\17\17\37>p\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\200\200\200\300\300\300\300\300\300\200\200\200\0\0\0\0\0\0\0\0\0\0" + "\0\0\200\200\200\300\300\300\300\300\300\200\200\200\0\0\0\0\0\0\200\360\370\374\376\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\376\374\370\360\200\200\360\370\374\376\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376" + "\374\370\360\200\37\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\37\0\0\1\3\7\17\37\77\177\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\17\7" + "\3\1\0\0\0\0\0\0\0\0\0\0\0\0\1\3\7\17\37\77\177\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\177\77\37\17\7\3\1\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\3\7\17\37\77\77\37\17\7\3\1\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\300\300\300\300\300\300\300" + "\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\340\370\370\376\376\377\377\377\377\377\377\377\377\77\77\77>\376\370\370\340\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0 p\360\340\360p \0\0\0\0\0\0\377\377\377\377\177\177\177\177\177\207\207\340\340\377" + "\377\377\377\377\377\377\377\0\0\0\0\0 p\360\340\360p \0\6\4\14\14\15|x\360\200\200\0\0" + "pp\177\177\377\377\374|\374\374\374\177\177\177\377\377\377\177\377\377\377\377\177pp\0\0\200\200\360x}" + "\14\14\4\6\0\0\0\0\0\0\0\3\37\37|ppp\34\34\37\3\3\0\377\377\377\0\0\0\377\377" + "\377\0\3\3\37\37\34ppp~\37\37\3\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\7\7\7\0\0\0\7\7\7\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0"; + + + +/* + Fontname: akemi_8x8 + Copyright: Benji (https://github.com/proto-molecule) + Glyphs: 1/1 + BBX Build Mode: 3 + * 12 = Akemi +*/ +/* +const uint8_t u8x8_akemi_8x8[516] U8X8_FONT_SECTION("u8x8_akemi_8x8") = + "\14\14\10\10\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" + "\200\200\200\200\200\200\200\200\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\340\340\370\370\376\376\376\376" + "\377\377\377\377\377\377\377\377\376\376\376\376\370\370\340\340\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\376\376\377\377\377\377\377\377\377\377" + "\377\377\377\377\37\37\37\343\343\343\343\343\343\377\377\377\376\376\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\30\30~~\370\370~~\30\30\0\0\0\0\0\0\0\377\377\377\377\377\77\77\77\77\77" + "\77\300\300\300\370\370\370\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\30\0f\0\200\0\0" + "\0\0\0\0\6\6\30\30\30\31\371\370\370\340\340\0\0\0\0\0\340\340\377\377\377\377\377\376\376\376\376\376" + "\376\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\371\346\346\6\6\6\6\6\0\340\340\340\341\0\0" + "\0\0\0\0\0\0\0\0\0\0\1\1\37\37\377\376\376\340\340\200\201\201\341\341\177\177\37\37\1\1\377\377" + "\377\377\1\1\1\1\377\377\377\377\1\1\37\37\177\177\341\341\201\201\200\200\370\370\376\376\37\37\1\1\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\1\7\7\7\7\7\7\1\1\0\0\0\0\0\0\377\377" + "\377\377\0\0\0\0\377\377\377\377\0\0\0\0\0\0\1\1\7\7\7\7\7\7\1\1\0\0\0\0\0\0" + "\0\0\0"; +*/ \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display_ALT/readme.md b/usermods/usermod_v2_four_line_display_ALT/readme.md new file mode 100644 index 00000000..ea9f4361 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/readme.md @@ -0,0 +1,45 @@ +# I2C 4 Line Display Usermod ALT + +Thank you to the authors of the original version of these usermods. It would not have been possible without them! +"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 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. +The original "usermod_v2_auto_save" will not work with the display just yet. + +Press the encoder to cycle through the options: + *Brightness + *Speed + *Intensity + *Palette + *Effect + *Main Color (only if display is used) + *Saturation (only if display is used) + +Press and hold the encoder to display Network Info + if AP is active, it will display AP, SSID and password + +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 + + +### PlatformIO requirements + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-10 +* 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 new file mode 100644 index 00000000..5a99c3cd --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -0,0 +1,1378 @@ +#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 behviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! +#endif + +// +// 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 works best, by far, when coupled +// with RotaryEncoderUI ALT Usermod. +// +// Make sure to enable NTP and set your time zone in WLED Config | Time. +// +// REQUIREMENT: You must add the following requirements to +// REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini +// 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 + +#ifdef ARDUINO_ARCH_ESP32 + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 19 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 26 + #endif +#else + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 12 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 16 + #endif +#endif + +#ifndef FLD_TYPE + #ifndef FLD_SPI_DEFAULT + #define FLD_TYPE SSD1306 + #else + #define FLD_TYPE SSD1306_SPI + #endif +#endif + +// When to time out to the clock or blank the screen +// if SLEEP_MODE_ENABLED. +#define SCREEN_TIMEOUT_MS 60*1000 // 1 min + +// Minimum time between redrawing screen in ms +#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 + 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[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[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 + + DisplayType type = FLD_TYPE; // display type + bool flip = false; // flip display 180° + uint8_t contrast = 10; // screen contrast + uint8_t lineHeight = 1; // 1 row or 2 rows + 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 + bool enabled = true; + bool contrastFix = false; + + // Next variables hold the previous known values to determine if redraw is + // required. + String knownSsid = apSSID; + IPAddress knownIp = IPAddress(4, 3, 2, 1); + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + uint8_t knownMinute = 99; + uint8_t knownHour = 99; + byte brightness100; + byte fxspeed100; + byte fxintensity100; + bool knownnightlight = nightlightActive; + bool wificonnected = interfacesInited; + bool powerON = true; + + bool displayTurnedOff = false; + unsigned long nextUpdate = 0; + unsigned long lastRedraw = 0; + unsigned long overlayUntil = 0; + + // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. + byte markLineNum = 255; + byte markColNum = 255; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _contrast[]; + static const char _refreshRate[]; + static const char _screenTimeOut[]; + static const char _flip[]; + static const char _sleepMode[]; + static const char _clockMode[]; + static const char _showSeconds[]; + static const char _busClkFrequency[]; + static const char _contrastFix[]; + + // 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 + + // some displays need this to properly apply contrast + void setVcomh(bool highContrast); + void startDisplay(); + + /** + * 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); + + /** + * 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(); + + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool enabled); + + public: + + // 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(); + + /** + * Da loop. + */ + void loop(); + + //function to update lastredraw + inline void updateRedrawTime() { lastRedraw = millis(); } + + /** + * Redraw the screen (but only if things have changed + * or if forceRedraw). + */ + void redraw(bool forceRedraw); + + 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); + + //Draw the arrow for the current setting beiong 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); + + /** + * 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 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); + + /** + * Allows you to show Akemi WLED logo overlay for a period of time. + * Clears the screen and prints. + */ + 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); + + 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); + + void onUpdateBegin(bool init); + + /* + * 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); + + void appendConfigData(); + + /* + * 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 addToConfig(JsonObject& root); + + /* + * 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 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_FOUR_LINE_DISP; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char FourLineDisplayUsermod::_name[] PROGMEM = "4LineDisplay"; +const char FourLineDisplayUsermod::_enabled[] PROGMEM = "enabled"; +const char FourLineDisplayUsermod::_contrast[] PROGMEM = "contrast"; +const char FourLineDisplayUsermod::_refreshRate[] PROGMEM = "refreshRate-ms"; +const char FourLineDisplayUsermod::_screenTimeOut[] PROGMEM = "screenTimeOutSec"; +const char FourLineDisplayUsermod::_flip[] PROGMEM = "flip"; +const char FourLineDisplayUsermod::_sleepMode[] PROGMEM = "sleepMode"; +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 beiong 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 // 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; + } + + 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; + } + + } 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 00000000..0619bf85 --- /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 exsisting build enviroment: +``` +[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 adress 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 00000000..0e19cc80 --- /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 practive 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 new file mode 100644 index 00000000..c24322f3 --- /dev/null +++ b/usermods/usermod_v2_mode_sort/readme.md @@ -0,0 +1,33 @@ +# Mode Sort + +v2 usermod that provides data about modes and +palettes to other usermods. Notably it provides: +* A direct method for a mode or palette name +* Ability to retrieve mode and palette names in + alphabetical order + +```char **getModesQStrings()``` + +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()``` + +A byte array designating the indexes of names of the +modes in alphabetical order. "Solid" will always remain +at the top of the list. + +```char **getPalettesQStrings()``` + +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()``` + +A byte array designating the indexes of names of the +palettes in alphabetical order. "Default" and those +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 new file mode 100644 index 00000000..092206bb --- /dev/null +++ b/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h @@ -0,0 +1,244 @@ +#pragma once + +#include "wled.h" + +// +// v2 usermod that provides data about modes and +// palettes to other usermods. Notably it provides: +// * A direct method for a mode or palette name +// * Ability to retrieve mode and palette names in +// alphabetical order +// +// char **getModesQStrings() +// Provides an array of char* (pointers) to the names of the +// palettes within 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 +// modes in alphabetical order. "Solid" will always remain +// at the front 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 +// 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 +// palettes in alphabetical order. "Default" and those +// starting with "(" will always remain at the front of the list. +// + +// Number of modes at the start of the list to not sort +#define MODE_SORT_SKIP_COUNT 1 + +// Which list is being sorted +char **listBeingSorted = nullptr; + +/** + * Modes and palettes are stored as strings that + * end in a quote character. Compare two of them. + * We are comparing directly within either + * JSON_mode_names or JSON_palette_names. + */ +int re_qstringCmp(const void *ap, const void *bp) { + char *a = listBeingSorted[*((byte *)ap)]; + char *b = listBeingSorted[*((byte *)bp)]; + int i = 0; + do { + char aVal = pgm_read_byte_near(a + i); + if (aVal >= 97 && aVal <= 122) { + // Lowercase + aVal -= 32; + } + char bVal = pgm_read_byte_near(b + i); + if (bVal >= 97 && bVal <= 122) { + // Lowercase + bVal -= 32; + } + // Relly 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. + if (aVal == bVal) { + // Same value, probably shouldn't happen + // with this dataset + return 0; + } + else if (aVal == '"' || aVal == '\0') { + return -1; + } + else { + return 1; + } + } + if (aVal == bVal) { + // Same characters. Move to the next. + i++; + continue; + } + // We're done + if (aVal < bVal) { + return -1; + } + else { + return 1; + } + } while (true); + // We shouldn't get here. + return 0; +} + +class ModeSortUsermod : public Usermod { +private: + + // Pointers the start of the mode names within JSON_mode_names + char **modes_qstrings = nullptr; + + // Array of mode indexes in alphabetical order. + byte *modes_alpha_indexes = nullptr; + + // Pointers the start of the palette names within JSON_palette_names + char **palettes_qstrings = nullptr; + + // Array of palette indexes in alphabetical order. + byte *palettes_alpha_indexes = nullptr; + +public: + /** + * 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() { + // Sort the modes and palettes on startup + // as they are guarantted to change. + sortModesAndPalettes(); + } + + char **getModesQStrings() { + return modes_qstrings; + } + + byte *getModesAlphaIndexes() { + return modes_alpha_indexes; + } + + char **getPalettesQStrings() { + return palettes_qstrings; + } + + byte *getPalettesAlphaIndexes() { + return palettes_alpha_indexes; + } + + /** + * This Usermod doesn't have anything for loop. + */ + void loop() {} + + /** + * 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); + + palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); + palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); + + int skipPaletteCount = 1; + while (true) { + // How many palette names start with '*' and should not be sorted? + // (Also skipping the first one, 'Default'). + if (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') { + skipPaletteCount++; + } + else { + break; + } + } + re_sortModes(palettes_qstrings, palettes_alpha_indexes, strip.getPaletteCount(), skipPaletteCount); + } + + 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; + } + return modeStrings; + } + + /** + * Sort either the modes or the palettes using quicksort. + */ + void re_sortModes(char **modeNames, byte *indexes, int count, int numSkip) { + listBeingSorted = modeNames; + qsort(indexes + numSkip, count - numSkip, sizeof(byte), re_qstringCmp); + listBeingSorted = nullptr; + } + + /* + * 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) {} + + /* + * 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_MODE_SORT; + } +}; 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 00000000..9f01b3eb --- /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 indivdual 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 00000000..a690c1b1 --- /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; // Adress for the one place of the hours + int baseHH = 7; // Adress for the tens place of the hours + int baseM = 133; // Adress for the one place of the minutes + int baseMM = 97; // Adress for the tens place of the minutes + int colon1 = 79; // Adress for the first colon led + int colon2 = 80; // Adress for the second colon led + + // Matrix for the illumination of the numbers + // Note: These only define the increments of the base adress. 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/platformio_override.ini.sample b/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample new file mode 100644 index 00000000..4b537a8f --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample @@ -0,0 +1,48 @@ +[platformio] +default_envs = d1_mini +; default_envs = esp32dev + +[env:esp32dev] +board = esp32dev +platform = espressif32@3.2 +build_unflags = ${common.build_unflags} +build_flags = + ${common.build_flags_esp32} + -D USERMOD_MODE_SORT + -D USERMOD_FOUR_LINE_DISPLAY -D FLD_PIN_SCL=22 -D FLD_PIN_SDA=21 + -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=18 -D ENCODER_CLK_PIN=5 -D ENCODER_SW_PIN=19 + -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 + -D LEDPIN=16 -D BTNPIN=13 +upload_speed = 460800 +lib_ignore = + ESPAsyncTCP + ESPAsyncUDP + +[env:d1_mini] +board = d1_mini +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +upload_speed = 460800 +board_build.ldscript = ${common.ldscript_4m1m} +build_unflags = ${common.build_unflags} +build_flags = + ${common.build_flags_esp8266} + -D USERMOD_MODE_SORT + -D USERMOD_FOUR_LINE_DISPLAY -D FLD_PIN_SCL=5 -D FLD_PIN_SDA=4 + -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=12 -D ENCODER_CLK_PIN=14 -D ENCODER_SW_PIN=13 + -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 + -D LEDPIN=3 -D BTNPIN=0 +monitor_filters = esp8266_exception_decoder + +[env] +lib_deps = + fastled/FastLED @ 3.3.2 + NeoPixelBus @ 2.6.0 + ESPAsyncTCP @ 1.2.0 + ESPAsyncUDP + AsyncTCP @ 1.0.3 + IRremoteESP8266 @ 2.7.3 + https://github.com/lorol/LITTLEFS.git + https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.0 + U8g2@~2.27.2 + Wire diff --git a/usermods/usermod_v2_rotary_encoder_ui/readme.md b/usermods/usermod_v2_rotary_encoder_ui/readme.md new file mode 100644 index 00000000..5e4f3cff --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui/readme.md @@ -0,0 +1,39 @@ +# Rotary Encoder UI Usermod + +First, thanks to the authors of other Rotary Encoder usermods. + +This usermod starts to provide a relatively complete on-device +UI when paired with the Four Line Display usermod. I strongly +encourage you to try them together. + +[See the pair of usermods in action](https://www.youtube.com/watch?v=tITQY80rIOA) + +## Installation + +Copy and update the example `platformio_override.ini.sample` to the root directory of your particular build. +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_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 + +No special requirements. + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-02 +* First public release 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 new file mode 100644 index 00000000..02bc0ccd --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h @@ -0,0 +1,496 @@ +#pragma once + +#include "wled.h" + +// +// Inspired by the v1 usermods +// * rotary_encoder_change_brightness +// * rotary_encoder_change_effect +// +// v2 usermod that provides a rotary encoder-based UI. +// +// This usermod allows you to control: +// +// * Brightness +// * Selected Effect +// * Effect Speed +// * Effect Intensity +// * Palette +// +// Change between modes by pressing a button. +// +// Dependencies +// * This usermod REQURES the ModeSortUsermod +// * This Usermod works best coupled with +// FourLineDisplayUsermod. +// + +#ifndef ENCODER_DT_PIN +#define ENCODER_DT_PIN 12 +#endif + +#ifndef ENCODER_CLK_PIN +#define ENCODER_CLK_PIN 14 +#endif + +#ifndef ENCODER_SW_PIN +#define ENCODER_SW_PIN 13 +#endif + +#ifndef USERMOD_FOUR_LINE_DISPLAY +// These constants won't be defined if we aren't using FourLineDisplay. +#define FLD_LINE_BRIGHTNESS 0 +#define FLD_LINE_MODE 0 +#define FLD_LINE_EFFECT_SPEED 0 +#define FLD_LINE_EFFECT_INTENSITY 0 +#define FLD_LINE_PALETTE 0 +#endif + + +// The last UI state +#define LAST_UI_STATE 4 + + +class RotaryEncoderUIUsermod : public Usermod { +private: + int fadeAmount = 10; // Amount to change every step (brightness) + unsigned long currentTime; + unsigned long loopTime; + 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 + unsigned char select_state = 0; // 0: brightness, 1: effect, 2: effect speed + unsigned char button_state = HIGH; + unsigned char prev_button_state = HIGH; + +#ifdef USERMOD_FOUR_LINE_DISPLAY + FourLineDisplayUsermod *display; +#else + void* display = nullptr; +#endif + + byte *modes_alpha_indexes = nullptr; + byte *palettes_alpha_indexes = nullptr; + + unsigned char Enc_A; + unsigned char Enc_B; + unsigned char Enc_A_prev = 0; + + bool currentEffectAndPaletteInitialized = false; + uint8_t effectCurrentIndex = 0; + uint8_t effectPaletteIndex = 0; + + bool initDone = false; + bool enabled = true; + + // 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[]; + +public: + /* + * 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() + { + 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 + // 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.... + pinA = pinB = pinC = -1; + enabled = false; + return; + } + + #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; + + ModeSortUsermod *modeSortUsermod = (ModeSortUsermod*) usermods.lookup(USERMOD_ID_MODE_SORT); + modes_alpha_indexes = modeSortUsermod->getModesAlphaIndexes(); + palettes_alpha_indexes = modeSortUsermod->getPalettesAlphaIndexes(); + +#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->setLineType(FLD_LINE_BRIGHTNESS); + display->setMarkLine(3); + } +#endif + + 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. + * + * 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) return; + + currentTime = millis(); // get the current elapsed time + + // 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 (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz + { + button_state = digitalRead(pinC); + if (prev_button_state != button_state) + { + if (button_state == LOW) + { + prev_button_state = button_state; + + char newState = select_state + 1; + if (newState > LAST_UI_STATE) newState = 0; + + bool changedState = true; + if (display != nullptr) { + switch(newState) { + case 0: + changedState = changeState("Brightness", FLD_LINE_BRIGHTNESS, 3); + break; + case 1: + changedState = changeState("Select FX", FLD_LINE_MODE, 2); + break; + case 2: + changedState = changeState("FX Speed", FLD_LINE_EFFECT_SPEED, 3); + break; + case 3: + changedState = changeState("FX Intensity", FLD_LINE_EFFECT_INTENSITY, 3); + break; + case 4: + changedState = changeState("Palette", FLD_LINE_PALETTE, 3); + break; + } + } + if (changedState) { + select_state = newState; + } + } + else + { + prev_button_state = button_state; + } + } + int Enc_A = digitalRead(pinA); // Read encoder pins + int Enc_B = digitalRead(pinB); + if ((!Enc_A) && (Enc_A_prev)) + { // A has gone from high to low + if (Enc_B == HIGH) + { // B is high so clockwise + switch(select_state) { + case 0: + changeBrightness(true); + break; + case 1: + changeEffect(true); + break; + case 2: + changeEffectSpeed(true); + break; + case 3: + changeEffectIntensity(true); + break; + case 4: + changePalette(true); + break; + } + } + else if (Enc_B == LOW) + { // B is low so counter-clockwise + switch(select_state) { + case 0: + changeBrightness(false); + break; + case 1: + changeEffect(false); + break; + case 2: + changeEffectSpeed(false); + break; + case 3: + changeEffectIntensity(false); + break; + case 4: + changePalette(false); + break; + } + } + } + Enc_A_prev = Enc_A; // Store value of A for next time + loopTime = currentTime; // Updates loopTime + } + } + + void findCurrentEffectAndPalette() { + currentEffectAndPaletteInitialized = true; + for (uint8_t i = 0; i < strip.getModeCount(); i++) { + //byte value = modes_alpha_indexes[i]; + if (modes_alpha_indexes[i] == effectCurrent) { + effectCurrentIndex = i; + break; + } + } + + for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { + //byte value = palettes_alpha_indexes[i]; + if (palettes_alpha_indexes[i] == strip.getSegment(0).palette) { + effectPaletteIndex = i; + break; + } + } + } + + boolean changeState(const char *stateName, byte lineThreeMode, byte markedLine) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display != nullptr) { + if (display->wakeDisplay()) { + // Throw away wake up input + return false; + } + display->overlay("Mode change", stateName, 1500); + display->setLineType(lineThreeMode); + display->setMarkLine(markedLine); + } + #endif + return true; + } + + void lampUdated() { + colorUpdated(CALL_MODE_BUTTON); + updateInterfaces(CALL_MODE_BUTTON); + } + + void changeBrightness(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + bri = (bri + fadeAmount <= 255) ? (bri + fadeAmount) : 255; + } + else { + bri = (bri - fadeAmount >= 0) ? (bri - fadeAmount) : 0; + } + lampUdated(); + } + + void changeEffect(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + effectCurrentIndex = (effectCurrentIndex + 1 >= strip.getModeCount()) ? 0 : (effectCurrentIndex + 1); + } + else { + effectCurrentIndex = (effectCurrentIndex - 1 < 0) ? (strip.getModeCount() - 1) : (effectCurrentIndex - 1); + } + effectCurrent = modes_alpha_indexes[effectCurrentIndex]; + lampUdated(); + } + + void changeEffectSpeed(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + effectSpeed = (effectSpeed + fadeAmount <= 255) ? (effectSpeed + fadeAmount) : 255; + } + else { + effectSpeed = (effectSpeed - fadeAmount >= 0) ? (effectSpeed - fadeAmount) : 0; + } + lampUdated(); + } + + void changeEffectIntensity(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + effectIntensity = (effectIntensity + fadeAmount <= 255) ? (effectIntensity + fadeAmount) : 255; + } + else { + effectIntensity = (effectIntensity - fadeAmount >= 0) ? (effectIntensity - fadeAmount) : 0; + } + lampUdated(); + } + + void changePalette(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + effectPaletteIndex = (effectPaletteIndex + 1 >= strip.getPaletteCount()) ? 0 : (effectPaletteIndex + 1); + } + else { + effectPaletteIndex = (effectPaletteIndex - 1 < 0) ? (strip.getPaletteCount() - 1) : (effectPaletteIndex - 1); + } + effectPalette = palettes_alpha_indexes[effectPaletteIndex]; + lampUdated(); + } + + /* + * 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 + } + */ + + /* + * 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; + } + + /* + * 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!")); + } + + /** + * 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; + 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; + + enabled = top[FPSTR(_enabled)] | enabled; + + 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) { + 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(); + } + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_enabled)].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"; +const char RotaryEncoderUIUsermod::_enabled[] PROGMEM = "enabled"; +const char RotaryEncoderUIUsermod::_DT_pin[] PROGMEM = "DT-pin"; +const char RotaryEncoderUIUsermod::_CLK_pin[] PROGMEM = "CLK-pin"; +const char RotaryEncoderUIUsermod::_SW_pin[] PROGMEM = "SW-pin"; diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md new file mode 100644 index 00000000..51636238 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -0,0 +1,45 @@ +# Rotary Encoder UI Usermod ALT + +Thank you to the authors of the original version of these usermods. It would not have been possible without them! +"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 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. +The original "usermod_v2_auto_save" will not work with the display just yet. + +Press the encoder to cycle through the options: + *Brightness + *Speed + *Intensity + *Palette + *Effect + *Main Color (only if display is used) + *Saturation (only if display is used) + +Press and hold the encoder to display Network Info + if AP is active, it will display the AP, SSID and Password + +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.
+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 + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-10 +* 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 new file mode 100644 index 00000000..b142f903 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -0,0 +1,1165 @@ +#pragma once + +#include "wled.h" + +// +// Inspired by the original v2 usermods +// * usermod_v2_rotaty_encoder_ui +// +// v2 usermod that provides a rotary encoder-based UI. +// +// This usermod allows you to control: +// +// * Brightness +// * Selected Effect +// * Effect Speed +// * Effect Intensity +// * Palette +// +// Change between modes by pressing a button. +// +// Dependencies +// * This Usermod works best coupled with +// FourLineDisplayUsermod. +// +// If FourLineDisplayUsermod is used the folowing options are also enabled +// +// * main color +// * saturation of main color +// * display network (long press buttion) +// + +#ifdef USERMOD_MODE_SORT + #error "Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini" +#endif + +#ifndef ENCODER_DT_PIN +#define ENCODER_DT_PIN 18 +#endif + +#ifndef ENCODER_CLK_PIN +#define ENCODER_CLK_PIN 5 +#endif + +#ifndef ENCODER_SW_PIN +#define ENCODER_SW_PIN 19 +#endif + +#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 11 +#else + #define LAST_UI_STATE 4 +#endif + +// Number of modes at the start of the list to not sort +#define MODE_SORT_SKIP_COUNT 1 + +// Which list is being sorted +static const char **listBeingSorted; + +/** + * Modes and palettes are stored as strings that + * end in a quote character. Compare two of them. + * We are comparing directly within either + * JSON_mode_names or JSON_palette_names. + */ +static int re_qstringCmp(const void *ap, const void *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); + if (aVal >= 97 && aVal <= 122) { + // Lowercase + aVal -= 32; + } + char bVal = pgm_read_byte_near(b + i); + if (bVal >= 97 && bVal <= 122) { + // Lowercase + bVal -= 32; + } + // Relly 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. + if (aVal == bVal) { + // Same value, probably shouldn't happen + // with this dataset + return 0; + } + else if (aVal == '"' || aVal == '\0') { + return -1; + } + else { + return 1; + } + } + if (aVal == bVal) { + // Same characters. Move to the next. + i++; + continue; + } + // We're done + if (aVal < bVal) { + return -1; + } + else { + return 1; + } + } while (true); + // We shouldn't get here. + return 0; +} + + +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: + + const int8_t fadeAmount; // Amount to change every step (brightness) + unsigned long loopTime; + + unsigned long buttonPressedTime; + unsigned long buttonWaitTime; + bool buttonPressedBefore; + bool buttonLongPressed; + + int8_t pinA; // DT from encoder + int8_t pinB; // CLK from encoder + int8_t pinC; // SW from encoder + + unsigned char select_state; // 0: brightness, 1: effect, 2: effect speed, ... + + 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 mode names within JSON_mode_names + const char **modes_qstrings; + + // Array of mode indexes in alphabetical order. + byte *modes_alpha_indexes; + + // Pointers the start of the palette names within JSON_palette_names + const char **palettes_qstrings; + + // Array of palette indexes in alphabetical order. + byte *palettes_alpha_indexes; + + struct { // reduce memory footprint + bool Enc_A : 1; + bool Enc_B : 1; + bool Enc_A_prev : 1; + }; + + bool currentEffectAndPaletteInitialized; + uint8_t effectCurrentIndex; + uint8_t effectPaletteIndex; + uint8_t knownMode; + uint8_t knownPalette; + + byte presetHigh; + byte presetLow; + + bool applyToAll; + + bool initDone; + bool enabled; + + bool usePcf8574; + int8_t pinIRQ; + + // 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(); + +#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); + } +} + +/** + * 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()); + palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); // only use internal palettes + + // 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); +} + +byte *RotaryEncoderUIUsermod::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 '"'. + */ +const char **RotaryEncoderUIUsermod::re_findModeStrings(const char json[], int numModes) { + const char **modeStrings = (const char **)malloc(sizeof(const 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; + } + return modeStrings; +} + +/** + * Sort either the modes or the palettes using quicksort. + */ +void RotaryEncoderUIUsermod::re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip) { + if (!modeNames) return; + listBeingSorted = modeNames; + qsort(indexes + numSkip, count - numSkip, sizeof(byte), re_qstringCmp); + listBeingSorted = nullptr; +} + + +// public methods + + +/* + * 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 RotaryEncoderUIUsermod::setup() +{ + DEBUG_PRINTLN(F("Usermod Rotary Encoder init.")); + + if (usePcf8574) { + if (i2c_sda < 0 || i2c_scl < 0 || pinA < 0 || pinB < 0 || pinC < 0) { + DEBUG_PRINTLN(F("I2C and/or PCF8574 pins unused, disabling.")); + enabled = false; + return; + } else { + if (pinIRQ >= 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)) { + pinA = pinB = pinC = -1; + enabled = false; + return; + } + + #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); + } + + loopTime = millis(); + + currentCCT = (approximateKelvinFromRGB(RGBW32(col[0], col[1], col[2], col[3])) - 1900) >> 5; + + if (!initDone) sortModesAndPalettes(); + +#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 + + initDone = true; + Enc_A = readPin(pinA); // Read encoder pins + Enc_B = readPin(pinB); + Enc_A_prev = Enc_A; +} + +/* + * 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) { + DEBUG_PRINTLN(F("Current mode or palette changed.")); + currentEffectAndPaletteInitialized = 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; + } + } else if (!buttonPressed && buttonPressedBefore) { + bool doublePress = buttonWaitTime; + buttonWaitTime = 0; + if (!buttonLongPressed) { + if (doublePress) { + toggleOnOff(); + lampUdated(); + } else { + buttonWaitTime = currentTime; + } + } + 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 { + // finde 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 + } + 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 + } + } + if (changedState) select_state = newState; + } + + 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; + } + } + 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 + } +} + +void RotaryEncoderUIUsermod::displayNetworkInfo() { + #ifdef USERMOD_FOUR_LINE_DISPLAY + display->networkOverlay(PSTR("NETWORK INFO"), 10000); + #endif +} + +void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { + DEBUG_PRINTLN(F("Finding current mode and palette.")); + currentEffectAndPaletteInitialized = true; + for (uint8_t i = 0; i < strip.getModeCount(); i++) { + if (modes_alpha_indexes[i] == effectCurrent) { + effectCurrentIndex = i; + break; + } + } + DEBUG_PRINTLN(F("Found current mode.")); + + for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { + if (palettes_alpha_indexes[i] == effectPalette) { + effectPaletteIndex = i; + break; + } + } + DEBUG_PRINTLN(F("Found palette.")); +} + +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 + display->redraw(true); + return false; + } + 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 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); + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateBrightness(); +#endif +} + + +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; 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 +} + + +void RotaryEncoderUIUsermod::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; iupdateIntensity(); +#endif +} + + +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; + } + for (byte i=0; ioverlay(lineBuffer, 500, 10); // use star +#endif +} + + +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((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 +} + + +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 RotaryEncoderUIUsermod::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; ioverlay(lineBuffer, 500, 8); // use contrast +#endif +} + +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 + sprintf(str, "%d", currentPreset); + display->overlay(str, 500, 11); // use heart + #endif + } +} + +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 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 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 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 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.")); +} + +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)); + 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); + 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(_pcfINTpin)].isNull(); +} + + +// strings to reduce flash memory usage (used more than twice) +const char RotaryEncoderUIUsermod::_name[] PROGMEM = "Rotary-Encoder"; +const char RotaryEncoderUIUsermod::_enabled[] PROGMEM = "enabled"; +const char RotaryEncoderUIUsermod::_DT_pin[] PROGMEM = "DT-pin"; +const char RotaryEncoderUIUsermod::_CLK_pin[] PROGMEM = "CLK-pin"; +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 00000000..1dde2223 --- /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 alternatative 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 00000000..058b8318 --- /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 desribed in 4 mask with LED numbers (single dots for minutes, minutes, hours and "clock/Uhr"). + * There are 2 parameters to chnage 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 00000000..fc0ae5fc --- /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 00000000..071bea9f --- /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 00000000..a83b9fe7 --- /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 00000000..a0e0a8b8 --- /dev/null +++ b/usermods/wizlights/readme.md @@ -0,0 +1,35 @@ +# Controlling Wiz lights + +Enabless 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 causse 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 00000000..08d20493 --- /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/Word Clock Baffle.stl b/usermods/word-clock-matrix/Word Clock Baffle.stl new file mode 100644 index 00000000..ed34a68f Binary files /dev/null and b/usermods/word-clock-matrix/Word Clock Baffle.stl differ diff --git a/usermods/word-clock-matrix/readme.md b/usermods/word-clock-matrix/readme.md new file mode 100644 index 00000000..cfaa93e2 --- /dev/null +++ b/usermods/word-clock-matrix/readme.md @@ -0,0 +1,19 @@ +## Word clock usermod + +By @bwente + +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 00000000..58256300 --- /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 stencil.svg b/usermods/word-clock-matrix/word clock stencil.svg new file mode 100644 index 00000000..32e3e656 --- /dev/null +++ b/usermods/word-clock-matrix/word clock stencil.svg @@ -0,0 +1,846 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/usermods/word-clock-matrix/word-clock-matrix.cpp b/usermods/word-clock-matrix/word-clock-matrix.cpp new file mode 100644 index 00000000..67c5b1e4 --- /dev/null +++ b/usermods/word-clock-matrix/word-clock-matrix.cpp @@ -0,0 +1,305 @@ +#include "wled.h" +/* + * This v1 usermod 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) + * If you just need 8 bytes, use 2551-2559 (you do not need to increase EEPSIZE) + * + * Consider the v2 usermod API if you need a more advanced feature set! + */ + + +uint8_t minuteLast = 99; +int dayBrightness = 128; +int nightBrightness = 16; + +//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) + +//gets called once at boot. Do all initialization that doesn't depend on network here +void userSetup() +{ +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) + 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++) + { + 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); + } +} + +//gets called every time WiFi is (re-)connected. Initialize own network interfaces here +void userConnected() +{ +} + +void selectWordSegments(bool state) +{ + for (int i = 1; i < 10; i++) + { + //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(255); + 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; + } + } + else + { //temperature display + } + + //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 userLoop() +{ + 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(255); + } + } +} diff --git a/wled00.sln b/wled00.sln deleted file mode 100644 index b2f12b83..00000000 --- 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/.vs/wled00/v15/.suo b/wled00/.vs/wled00/v15/.suo deleted file mode 100644 index 5bbdedd7..00000000 Binary files a/wled00/.vs/wled00/v15/.suo and /dev/null differ diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 1d7cbaab..d9200cfe 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -24,90 +24,134 @@ Modified heavily for WLED */ +#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 attac & 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 : 500; //update faster if in transition +uint16_t mode_static(void) { + SEGMENT.fill(SEGCOLOR(0)); + return 350; } +static const char _data_FX_MODE_STATIC[] PROGMEM = "Solid"; /* * Blink/strobe function * Alternate between color1 and color2 * if(strobe == true) then create a strobe effect - * NOTE: Maybe re-rework without timer */ -uint16_t WS2812FX::blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { - uint16_t stateTime = SEGENV.aux1; +uint16_t blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { uint32_t cycleTime = (255 - SEGMENT.speed)*20; - uint32_t onTime = 0; - uint32_t offTime = cycleTime; + uint32_t onTime = FRAMETIME; + if (!strobe) onTime += ((cycleTime * SEGMENT.intensity) >> 8); + cycleTime += FRAMETIME*2; + uint32_t it = strip.now / cycleTime; + uint32_t rem = strip.now % cycleTime; - if (!strobe) { - onTime = (cycleTime * SEGMENT.intensity) >> 8; - offTime = cycleTime - onTime; + bool on = false; + if (it != SEGENV.step //new iteration, force on state for one frame, even if set time is too brief + || rem <= onTime) { + on = true; } - stateTime = ((SEGENV.aux0 & 1) == 0) ? onTime : offTime; - stateTime += 20; + SEGENV.step = it; //save previous iteration - if (now - SEGENV.step > stateTime) - { - SEGENV.aux0++; - SEGENV.aux1 = stateTime; - SEGENV.step = now; - } - - uint32_t color = ((SEGENV.aux0 & 1) == 0) ? color1 : color2; + 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"; /* @@ -115,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) { @@ -133,11 +177,11 @@ uint16_t WS2812FX::color_wipe(bool rev, bool useRandomColors) { SEGENV.step = 3; } if (SEGENV.step == 1) { //if flag set, change to new random color - SEGENV.aux1 = get_random_wheel_index(SEGENV.aux0); + SEGENV.aux1 = SEGMENT.get_random_wheel_index(SEGENV.aux0); SEGENV.step = 2; } if (SEGENV.step == 3) { - SEGENV.aux0 = get_random_wheel_index(SEGENV.aux1); + SEGENV.aux0 = SEGMENT.get_random_wheel_index(SEGENV.aux1); SEGENV.step = 0; } } @@ -148,19 +192,19 @@ 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); + 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; @@ -170,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; @@ -222,49 +271,71 @@ uint16_t WS2812FX::mode_random_color(void) { if (it != SEGENV.step) //new color { SEGENV.aux1 = SEGENV.aux0; - SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); //aux0 will store our random color wheel index + SEGENV.aux0 = SEGMENT.get_random_wheel_index(SEGENV.aux0); //aux0 will store our random color wheel index 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. + * to new random colors. */ -uint16_t WS2812FX::mode_dynamic(void) { +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; } - for (uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_wheel(SEGENV.data[i])); + if (SEGMENT.check1) { + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.blendPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i]), 16); + } + } else { + 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;;!"; + + +/* + * effect "Dynamic" with smooth color-fading + */ +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); @@ -272,54 +343,56 @@ uint16_t WS2812FX::mode_breath(void) { } 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; @@ -329,77 +402,81 @@ 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;;!"; /* - * theater chase function + * Alternating pixels running function. */ -uint16_t WS2812FX::theater_chase(uint32_t color1, uint32_t color2, bool do_palette) { - byte gap = 2 + ((255 - SEGMENT.intensity) >> 5); - uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*2; - uint32_t it = now / cycleTime; - if (it != SEGENV.step) //new color - { - SEGENV.aux0 = (SEGENV.aux0 +1) % gap; - SEGENV.step = it; +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); } - for(uint16_t i = 0; i < SEGLEN; i++) { - if((i % gap) == SEGENV.aux0) { - if (do_palette) - { - setPixelColor(i, color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); - } else { - setPixelColor(i, color1); - } - } else { - setPixelColor(i, color2); - } + if (it != SEGENV.step) { + SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1)); + SEGENV.step = it; } return FRAMETIME; } @@ -409,70 +486,93 @@ uint16_t WS2812FX::theater_chase(uint32_t color1, uint32_t color2, bool do_palet * Theatre-style crawling lights. * Inspired by the Adafruit examples. */ -uint16_t WS2812FX::mode_theater_chase(void) { - return theater_chase(SEGCOLOR(0), SEGCOLOR(1), true); +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 theater_chase(color_wheel(SEGENV.step), SEGCOLOR(1), false); +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) { +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++) { - uint8_t s = 0; - uint8_t a = i*x_scale - counter; + for (int i = 0; i < SEGLEN; i++) { + uint16_t a = i*x_scale - counter; if (saw) { + a &= 0xFF; if (a < 16) { a = 192 + a*8; } else { a = map(a,16,255,64,192); } + a = 255 - a; } - s = sin8(a); - setPixelColor(i, color_blend(color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), s)); + uint8_t s = dual ? sin_gap(a) : sin8(a); + 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), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), t); + ca = color_blend(ca, cb, 127); + } + SEGMENT.setPixelColor(i, ca); } + return FRAMETIME; } +/* + * Running lights in opposite directions. + * Idea: Make the gap width controllable with a third slider in the future + */ +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 @@ -492,44 +592,49 @@ 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 +uint16_t dissolve(uint32_t color) { + //bool wa = (SEGCOLOR(1) != 0 && strip.getBrightness() < 255); //workaround, can't compare getPixel to color if not full brightness + if (SEGENV.call == 0) { + SEGMENT.fill(SEGCOLOR(1)); + } - for (uint16_t j = 0; j <= SEGLEN / 15; j++) - { + 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 + for (size_t times = 0; times < 10; times++) //attempt to spawn a new pixel 10 times { uint16_t i = random16(SEGLEN); 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 (SEGMENT.getPixelColor(i) == SEGCOLOR(1) /*|| wa*/) { + if (color == SEGCOLOR(0)) { + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); + } else { + SEGMENT.setPixelColor(i, color); + } 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 (SEGMENT.getPixelColor(i) != SEGCOLOR(1)) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); break; } } } } } - if (SEGENV.call > (255 - SEGMENT.speed) + 15) - { + if (SEGENV.step > (255 - SEGMENT.speed) + 15U) { SEGENV.aux0 = !SEGENV.aux0; - SEGENV.call = 0; + SEGENV.step = 0; + } else { + SEGENV.step++; } return FRAMETIME; @@ -539,109 +644,124 @@ 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 white pixels randomly. + * 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(random8(5) == 0) { - SEGENV.aux0 = random16(SEGLEN); // aux0 stores the random led index - setPixelColor(SEGENV.aux0, SEGCOLOR(1)); - return 20; + if (strip.now - SEGENV.aux0 > SEGENV.step) { + if(random8((255-SEGMENT.intensity) >> 4) == 0) { + SEGMENT.setPixelColor(random16(SEGLEN), SEGCOLOR(1)); //flash + } + SEGENV.step = strip.now; + SEGENV.aux0 = 255-SEGMENT.speed; } - return 20 + (uint16_t)(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(random8(5) < 2) { - 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)); + } } - return 20; + SEGENV.step = strip.now; + SEGENV.aux0 = 255-SEGMENT.speed; } - return 20 + (uint16_t)(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)); } - //blink(SEGCOLOR(0), SEGCOLOR(1), true, true); - uint16_t delay = 50 + 20*(uint16_t)(255-SEGMENT.speed); - uint16_t count = 2 * ((SEGMENT.speed / 10) + 1); - if(SEGENV.step < count) { - if((SEGENV.step & 1) == 0) { - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, SEGCOLOR(0)); - } - delay = 20; + 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) { + SEGMENT.fill(SEGCOLOR(0)); + SEGENV.aux0 = 15; } else { - delay = 50; + SEGENV.aux0 = 50; } } - SEGENV.step = (SEGENV.step + 1) % (count + 1); - return delay; + + if (strip.now - SEGENV.aux0 > SEGENV.step) { + SEGENV.aux1++; + if (SEGENV.aux1 > count) SEGENV.aux1 = 0; + 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) { +uint16_t mode_android(void) { - 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)); } if (SEGENV.aux1 > ((float)SEGMENT.intensity/255.0)*(float)SEGLEN) @@ -668,30 +788,32 @@ uint16_t WS2812FX::mode_android(void) { 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 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); @@ -699,9 +821,9 @@ uint16_t WS2812FX::chase(uint32_t color1, uint32_t color2, uint32_t color3, bool if (a < SEGENV.step) //we hit the start again, choose new color for Chase random { SEGENV.aux1 = SEGENV.aux0; //store previous random color - SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); + SEGENV.aux0 = SEGMENT.get_random_wheel_index(SEGENV.aux0); } - color1 = color_wheel(SEGENV.aux0); + color1 = SEGMENT.color_wheel(SEGENV.aux0); } SEGENV.step = a; @@ -716,41 +838,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; @@ -760,133 +882,136 @@ 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) { - uint32_t cols[]{0x00FF0000,0x00EEBB00,0x0000EE00,0x000077CC,0x00FF0000,0x00EEBB00,0x0000EE00}; - if (SEGMENT.intensity < 127) //pastel (easter) colors +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 (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 (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 { cols[0] = 0x00FF8040; cols[1] = 0x00E5D241; cols[2] = 0x0077FF77; cols[3] = 0x0077F0F0; - for (uint8_t i = 4; i < 7; i++) cols[i] = cols[i-4]; } + for (size_t i = numColors; i < numColors*2 -1U; i++) cols[i] = cols[i-numColors]; - uint32_t cycleTime = 50 + (15 * (uint32_t)(255 - SEGMENT.speed)); - uint32_t it = now / cycleTime; + uint32_t cycleTime = 50 + (8 * (uint32_t)(255 - SEGMENT.speed)); + uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { if (SEGMENT.speed > 0) SEGENV.aux0++; - if (SEGENV.aux0 > 3) SEGENV.aux0 = 0; + if (SEGENV.aux0 >= numColors) SEGENV.aux0 = 0; SEGENV.step = it; } - uint16_t i = 0; - for (i; i < SEGLEN -3; i+=4) + for (int i = 0; i < SEGLEN; i+= numColors) { - setPixelColor(i, cols[SEGENV.aux0]); - setPixelColor(i+1, cols[SEGENV.aux0+1]); - setPixelColor(i+2, cols[SEGENV.aux0+2]); - setPixelColor(i+3, cols[SEGENV.aux0+3]); - } - if(i < SEGLEN) - { - setPixelColor(i, cols[SEGENV.aux0]); - - if(i+1 < SEGLEN) - { - setPixelColor(i+1, cols[SEGENV.aux0+1]); - - if(i+2 < SEGLEN) - { - setPixelColor(i+2, cols[SEGENV.aux0+2]); - } - } + 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); @@ -894,8 +1019,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; @@ -905,165 +1030,116 @@ 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) { - SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); + if (SEGENV.aux1 == 0) { + SEGENV.aux0 = SEGMENT.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) { - uint8_t pxw = 1 + (SEGMENT.intensity >> 5); - uint32_t cycleTime = 35 + (255 - SEGMENT.speed); - uint32_t it = now / cycleTime; - if (SEGMENT.speed == 0) it = 0; - - for(uint16_t i = 0; i < SEGLEN; i++) { - if((i + SEGENV.aux0) % (pxw*2) < pxw) { - if (color1 == SEGCOLOR(0)) - { - setPixelColor(SEGLEN -i -1, color_from_palette(SEGLEN -i -1, true, PALETTE_SOLID_WRAP, 0)); - } else - { - setPixelColor(SEGLEN -i -1, color1); - } - } else { - setPixelColor(SEGLEN -i -1, color2); - } - } - - if (it != SEGENV.step ) - { - SEGENV.aux0 = (SEGENV.aux0 +1) % (pxw*2); - 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)); } +static const char _data_FX_MODE_RUNNING_COLOR[] PROGMEM = "Chase 2@!,Width;!,!;!"; /* - * Alternating red/blue pixels running. + * Random colored pixels running. ("Stream") */ -uint16_t WS2812FX::mode_running_red_blue(void) { - return running(RED, BLUE); -} - - -/* - * Alternating red/green pixels running. - */ -uint16_t WS2812FX::mode_merry_christmas(void) { - return running(RED, GREEN); -} - - -/* - * Alternating orange/purple pixels running. - */ -uint16_t WS2812FX::mode_halloween(void) { - return running(PURPLE, ORANGE); -} - - -/* - * Random colored pixels running. - */ -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; - if (SEGENV.aux1 == it) return FRAMETIME; + uint32_t it = strip.now / cycleTime; + if (SEGENV.call == 0) SEGENV.aux0 = random16(); // random seed for PRNG on start - for(uint16_t i=SEGLEN-1; i > 0; i--) { - setPixelColor( i, getPixelColor( i - 1)); - } + uint8_t zoneSize = ((255-SEGMENT.intensity) >> 4) +1; + uint16_t PRNG16 = SEGENV.aux0; - if(SEGENV.step == 0) { - SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); - setPixelColor(0, color_wheel(SEGENV.aux0)); - } - - SEGENV.step++; - if (SEGENV.step > ((255-SEGMENT.intensity) >> 4)) - { - SEGENV.step = 0; + uint8_t z = it % zoneSize; + bool nzone = (!z && it != SEGENV.aux1); + for (int i=SEGLEN-1; i > 0; i--) { + if (nzone || z >= zoneSize) { + uint8_t lastrand = PRNG16 >> 8; + int16_t diff = 0; + while (abs(diff) < 42) { // make sure the difference between adjacent colors is big enough + PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next zone, next 'random' number + diff = (PRNG16 >> 8) - lastrand; + } + if (nzone) { + SEGENV.aux0 = PRNG16; // save next starting seed + nzone = false; + } + z = 0; + } + 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 larson_scanner(bool dual) { + 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); } } @@ -1072,118 +1148,164 @@ 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) { + SEGMENT.fill(SEGCOLOR(1)); 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 occured } } 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 right - uint32_t ctemp = getPixelColor(SEGLEN -1); - for(uint16_t i = SEGLEN -1; i > 0; 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) & 0xFF; - byte r = (SEGCOLOR(0) >> 16) & 0xFF; - byte g = (SEGCOLOR(0) >> 8) & 0xFF; - byte b = (SEGCOLOR(0) & 0xFF); + 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 gradient_base(bool loading) { + 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 @@ -1192,7 +1314,7 @@ uint16_t WS2812FX::gradient_base(bool loading) { int p1 = pp-SEGLEN; int p2 = pp+SEGLEN; - for(uint16_t i = 0; i < SEGLEN; i++) + for (int i = 0; i < SEGLEN; i++) { if (loading) { @@ -1201,7 +1323,7 @@ uint16_t WS2812FX::gradient_base(bool loading) { 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)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), val)); } return FRAMETIME; @@ -1211,153 +1333,246 @@ 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, bool all) -{ - uint16_t counter = now * ((SEGMENT.speed >> 2) +1); - uint16_t idexR = (counter * SEGLEN) >> 16; - if (idexR >= SEGLEN) idexR = 0; +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 = strip.now / map(SEGMENT.speed, 0, 255, delay<<4, delay); + uint16_t offset = it % SEGLEN; - uint16_t topindex = SEGLEN >> 1; - uint16_t idexB = (idexR > topindex) ? idexR - topindex : idexR + topindex; - if (SEGENV.call == 0) SEGENV.aux0 = idexR; - if (idexB >= SEGLEN) idexB = 0; //otherwise overflow on odd number of LEDs - - if (all) { //different algo, ensuring immediate fill - if (idexB > idexR) { - fill(color2); - for (uint16_t i = idexR; i < idexB; i++) setPixelColor(i, color1); - } else { - fill(color1); - for (uint16_t i = idexB; i < idexR; i++) setPixelColor(i, color2); - } - } else { //regular dot-only mode - uint8_t size = 1 + SEGMENT.intensity >> 3; - if (size > SEGLEN/2) size = 1+ SEGLEN/2; - for (uint8_t i=0; i <= size; i++) { - setPixelColor(idexR+i, color1); - setPixelColor(idexB+i, color2); - } - if (SEGENV.aux0 != idexR) { - uint8_t gap = (SEGENV.aux0 < idexR)? idexR - SEGENV.aux0:SEGLEN - SEGENV.aux0 + idexR; - for (uint8_t i = 0; i <= gap ; i++) { - if ((idexR - i) < 0) idexR = SEGLEN-1 + i; - if ((idexB - i) < 0) idexB = SEGLEN-1 + i; - setPixelColor(idexR-i, color1); - setPixelColor(idexB-i, color2); - } - SEGENV.aux0 = idexR; - } + uint16_t width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip + if (!width) width = 1; + for (int i = 0; i < width; i++) { + uint16_t indexR = (offset + i) % SEGLEN; + uint16_t indexB = (offset + i + (SEGLEN>>1)) % SEGLEN; + SEGMENT.setPixelColor(indexR, color1); + SEGMENT.setPixelColor(indexB, color2); } - return FRAMETIME; } -//American Police Light with all LEDs Red and Blue -uint16_t WS2812FX::mode_police_all() -{ - return police_base(RED, BLUE, true); -} - - //Police Lights Red and Blue -uint16_t WS2812FX::mode_police() -{ - fill(SEGCOLOR(1)); - - return police_base(RED, BLUE, false); -} - - -//Police All with custom colors -uint16_t WS2812FX::mode_two_areas() -{ - return police_base(SEGCOLOR(0), SEGCOLOR(1), true); -} +//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() +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, false); + return police_base(SEGCOLOR(0), color2); } +static const char _data_FX_MODE_TWO_DOTS[] PROGMEM = "Two Dots@!,Dot size,,,,,Overlay;1,2,Bg;!"; + + +/* + * Fairy, inspired by https://www.youtube.com/watch?v=zeOw5MZWq24 + */ +//4 bytes +typedef struct Flasher { + uint16_t stateStart; + uint8_t stateDur; + bool stateOn; +} flasher; + +#define FLASHERS_PER_ZONE 6 +#define MAX_SHIMMER 92 + +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 = 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 strip.getPixelColor + * Warning: Uses 4 bytes of segment data per pixel + */ +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 = 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) { - uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*2; - uint32_t it = now / cycleTime; - uint8_t width = (1 + SEGMENT.intensity/32) * 3; //value of 1-8 for each colour - uint8_t index = it % width; +uint16_t tricolor_chase(uint32_t color1, uint32_t color2) { + uint32_t cycleTime = 50 + ((255 - SEGMENT.speed)<<1); + 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++) { - if(index > width-1) index = 0; + for (int i = 0; i < SEGLEN; i++, index++) { + if (index > (width*3)-1) index = 0; uint32_t color = color1; - if(index > width*2/3-1) color = color_from_palette(i, true, PALETTE_SOLID_WRAP, 1); - else if(index > width/3-1) color = color2; + 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; } -/* - * Alternating white/red/black pixels running. PLACEHOLDER - */ -uint16_t WS2812FX::mode_circus_combustus(void) { - return tricolor_chase(RED, WHITE); -} - - /* * 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); @@ -1372,51 +1587,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;!"; /* @@ -1424,9 +1640,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; @@ -1447,46 +1662,46 @@ uint16_t WS2812FX::mode_tricolor_fade(void) } byte stp = prog; // % 256 - uint32_t color = 0; - 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 { @@ -1499,42 +1714,45 @@ uint16_t WS2812FX::mode_multi_comet(void) SEGENV.step = it; return FRAMETIME; } +static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet"; /* - * 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); -} - - -/* - * Running random pixels + * 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) -{ - uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); - uint32_t it = now / cycleTime; - if (SEGENV.step == it) return FRAMETIME; - - for(uint16_t i = SEGLEN -1; i > 0; i--) { - setPixelColor(i, getPixelColor(i-1)); +uint16_t mode_random_chase(void) { + if (SEGENV.call == 0) { + SEGENV.step = RGBW32(random8(), random8(), random8(), 0); + SEGENV.aux0 = random16(); } - uint32_t color = getPixelColor(0); - if (SEGLEN > 1) color = getPixelColor( 1); - 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(); - setPixelColor(0, r, g, b); + 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 = strip.now / cycleTime; + uint32_t color = SEGENV.step; + random16_set_seed(SEGENV.aux0); - SEGENV.step = it; + 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); + 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(); + } + } + + SEGENV.aux1 = it & 0xFFFF; + + 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 { int16_t pos; int8_t size; @@ -1545,8 +1763,7 @@ 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; @@ -1556,15 +1773,15 @@ uint16_t WS2812FX::mode_oscillate(void) if (SEGENV.call == 0) { - oscillators[0] = {SEGLEN/4, SEGLEN/8, 1, 1}; - oscillators[1] = {SEGLEN/4*3, SEGLEN/8, 1, 2}; - oscillators[2] = {SEGLEN/4*2, SEGLEN/8, -1, 1}; + oscillators[0] = {(int16_t)(SEGLEN/4), (int8_t)(SEGLEN/8), 1, 1}; + oscillators[1] = {(int16_t)(SEGLEN/4*3), (int8_t)(SEGLEN/8), 1, 2}; + oscillators[2] = {(int16_t)(SEGLEN/4*2), (int8_t)(SEGLEN/8), -1, 1}; } 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); @@ -1581,66 +1798,70 @@ 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 = random16(SEGLEN -1 -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) + 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); - if (SEGENV.step == 0) + if (SEGENV.aux1 == 0) //init, leader flash { - SEGENV.aux0 = random8(3, 3 + SEGMENT.intensity/20); //number of flashes - bri = 52; - SEGENV.aux1 = 1; + SEGENV.aux1 = random8(4, 4 + SEGMENT.intensity/20); //number of flashes + SEGENV.aux1 *= 2; + + bri = 52; //leader has lower brightness + SEGENV.aux0 = 200; //200ms delay after leader } - fill(SEGCOLOR(1)); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - if (SEGENV.aux1) { + if (SEGENV.aux1 > 3 && !(SEGENV.aux1 & 0x01)) { //flash on even number >2 for (int i = ledstart; i < ledstart + ledlen; i++) { - if (SEGMENT.palette == 0) - { - setPixelColor(i,bri,bri,bri,bri); - } else { - 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--; + + SEGENV.step = millis(); + //return random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds + } else { + if (millis() - SEGENV.step > SEGENV.aux0) { + SEGENV.aux1--; + if (SEGENV.aux1 < 2) SEGENV.aux1 = 0; + + SEGENV.aux0 = (50 + random8(100)); //delay between flashes + if (SEGENV.aux1 == 2) { + SEGENV.aux0 = (random8(255 - SEGMENT.speed) * 100); // delay between strikes + } + SEGENV.step = millis(); } - SEGENV.aux1 = 0; - SEGENV.step++; - return random8(4, 10); // each flash only lasts 4-10 milliseconds } - - SEGENV.aux1 = 1; - if (SEGENV.step == 1) return (200); // longer delay until next flash after the leader - - if (SEGENV.step <= SEGENV.aux0) return (50 + random8(100)); // shorter delay between strokes - - SEGENV.step = 0; - return (random8(255 - SEGMENT.speed) * 100); // delay between strikes + 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; @@ -1656,9 +1877,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; @@ -1669,54 +1889,57 @@ 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(); + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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) { - 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 @@ -1735,7 +1958,7 @@ 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 +// 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, @@ -1747,50 +1970,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 WS2812FX::mode_fire_2012() -{ - uint32_t it = now >> 5; //div 32 - - if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed - +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 + + struct virtualStrip { + static void runStrip(uint16_t stripNr, byte* heat, uint32_t it) { + + const uint8_t ignition = max(3,SEGLEN/10); // ignition area: 10% of segment length or minimum 3 pixels + + // 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 1; k--) { - heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3; - } - - // Step 3. Randomly ignite new 'sparks' of heat near the bottom - if (random8() <= SEGMENT.intensity) { - uint8_t y = random8(7); - if (y < SEGLEN) heat[y] = qadd8(heat[y], random8(160,255)); - } SEGENV.step = it; - } - // Step 4. Map from heat cells to LED colors - for (uint16_t j = 0; j < SEGLEN; j++) { - CRGB color = ColorFromPalette(currentPalette, MIN(heat[j],240), 255, LINEARBLEND); - setPixelColor(j, color.red, color.green, color.blue); - } return FRAMETIME; } +static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,,Boost;;!;1;sx=64,ix=160,m12=1"; // bars // ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb // This function draws color waves with an ever-changing, // widely-varying set of parameters, using a color palette. -uint16_t WS2812FX::mode_colorwaves() -{ +uint16_t mode_colorwaves() { uint16_t duration = 10 + SEGMENT.speed; uint16_t sPseudotime = SEGENV.step; uint16_t sHue16 = SEGENV.aux0; @@ -1800,15 +2038,13 @@ uint16_t WS2812FX::mode_colorwaves() uint8_t msmultiplier = beatsin88(147, 23, 60); uint16_t hue16 = sHue16;//gHue * 256; - // uint16_t hueinc16 = beatsin88(113, 300, 1500); uint16_t hueinc16 = beatsin88(113, 60, 300)*SEGMENT.intensity*10/255; // Use the Intensity Slider for the hues 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; uint16_t h16_128 = hue16 >> 7; @@ -1819,156 +2055,148 @@ 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() { + //CRGB fastled_col; + 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++) { + //fastled_col = ColorFromPalette(SEGPALETTE, stp + (i * 2), beat - stp + (i * 10)); + //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + 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; + +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 (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 - - + 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 + 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 - 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++) { + 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 - uint16_t shift_x = SEGENV.step >> 6; // x as a function of time - uint16_t shift_y = SEGENV.step/42; - - 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; - 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 = 8 + (SEGMENT.speed/4), fadeDownAmount = 5 + (SEGMENT.speed/7); + 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; @@ -1976,106 +2204,109 @@ uint16_t WS2812FX::mode_colortwinkle() if (fadeUp) { CRGB incrementalColor = fastled_col; - incrementalColor.nscale8_video( fadeUpAmount); + incrementalColor.nscale8_video(fadeUpAmount); fastled_col += incrementalColor; 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); + fastled_col.nscale8(255 - fadeDownAmount); + SEGMENT.setPixelColor(i, fastled_col); } } - for (uint16_t j = 0; j <= SEGLEN / 50; j++) - { + for (uint16_t j = 0; j <= SEGLEN / 50; j++) { if (random8() <= SEGMENT.intensity) { - for (uint8_t times = 0; times < 5; times++) //attempt to spawn a new pixel 5 times - { + 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 } } } } - return FRAMETIME; + 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; 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); + uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); uint16_t in = counter * SEGLEN >> 16; // fade all leds to colors[1] in LEDs one step - for (uint16_t i = 0; i < SEGLEN; i++) { + for (int i = 0; i < SEGLEN; i++) { if (random8() <= 255 - SEGMENT.intensity) { byte meteorTrailDecay = 128 + random8(127); trail[i] = scale8(trail[i], meteorTrailDecay); - setPixelColor(i, color_from_palette(trail[i], false, true, 255)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, false, 0, trail[i])); } } // draw meteor - for(int j = 0; j < meteorSize; j++) { + for (int 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)); + SEGMENT.setPixelColor(index, SEGMENT.color_from_palette(index, true, false, 0, 255)); } return FRAMETIME; } +static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail length;!;!"; // 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; @@ -2084,36 +2315,37 @@ uint16_t WS2812FX::mode_meteor_smooth() { uint16_t in = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); // fade all leds to colors[1] in LEDs one step - for (uint16_t i = 0; i < SEGLEN; i++) { + for (int 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)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, false, 0, trail[i])); } } // draw meteor - for(int j = 0; j < meteorSize; j++) { + for (int j = 0; j < meteorSize; j++) { uint16_t index = in + j; - if(in + j >= SEGLEN) { - index = (in + j - SEGLEN); + if (index >= SEGLEN) { + index -= SEGLEN; } - setPixelColor(index, color_blend(getPixelColor(index), color_from_palette(240, false, true, 255), 48)); trail[index] = 240; + SEGMENT.setPixelColor(index, SEGMENT.color_from_palette(index, true, false, 0, 255)); } SEGENV.step += SEGMENT.speed +1; return FRAMETIME; } +static const char _data_FX_MODE_METEOR_SMOOTH[] PROGMEM = "Meteor Smooth@!,Trail length;!;!"; //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) { @@ -2128,17 +2360,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 @@ -2152,86 +2385,90 @@ typedef struct Ripple { uint16_t pos; } ripple; -uint16_t WS2812FX::ripple_base(bool rainbow) +#ifdef ESP8266 + #define MAX_RIPPLES 56 +#else + #define MAX_RIPPLES 100 +#endif +uint16_t ripple_base() { - uint16_t maxRipples = 1 + (SEGLEN >> 2); - if (maxRipples > 100) maxRipples = 100; + 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()) { + uint16_t cx = rippleorigin >> 8; + uint16_t cy = rippleorigin & 0xFF; + uint8_t mag = scale8(cubicwave8((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) { 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 mode_ripple(void) { + if (SEGLEN == 1) return mode_static(); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); + return ripple_base(); } +static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,,,,,Overlay;,!;!;12"; -uint16_t WS2812FX::mode_ripple_rainbow(void) { - return ripple_base(true); + +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 @@ -2244,10 +2481,10 @@ uint16_t WS2812FX::mode_ripple_rainbow(void) { // incandescent bulbs change color as they get dim down. #define COOL_LIKE_INCANDESCENT 1 -CRGB 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 / (32 - (SEGMENT.speed >> 3)); + uint16_t ticks = ms / SEGENV.aux0; uint8_t fastcycle8 = ticks; uint16_t slowcycle16 = (ticks >> 8) + salt; slowcycle16 += sin8(slowcycle16); @@ -2281,7 +2518,7 @@ CRGB WS2812FX::twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) uint8_t hue = slowcycle8 - salt; CRGB c; if (bright > 0) { - c = ColorFromPalette(currentPalette, hue, bright, NOBLEND); + c = ColorFromPalette(SEGPALETTE, hue, bright, NOBLEND); if(COOL_LIKE_INCANDESCENT == 1) { // 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 @@ -2304,7 +2541,7 @@ CRGB WS2812FX::twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) // "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 @@ -2312,12 +2549,12 @@ uint16_t WS2812FX::twinklefox_base(bool cat) // numbers that it generates is (paradoxically) stable. uint16_t PRNG16 = 11337; + // Calculate speed + if (SEGMENT.speed > 100) SEGENV.aux0 = 3 + ((255 - SEGMENT.speed) >> 3); + else SEGENV.aux0 = 22 + ((100 - SEGMENT.speed) >> 1); + // Set up the background color, "bg". - // if AUTO_SELECT_BACKGROUND_COLOR == 1, and the first two colors of - // the current palette are identical, then a deeply faded version of - // that color is used for the background color - 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 @@ -2329,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 @@ -2349,49 +2586,55 @@ 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;;!"; -uint16_t WS2812FX::mode_twinklecat() + +uint16_t mode_twinklecat() { return twinklefox_base(true); } +static const char _data_FX_MODE_TWINKLECAT[] PROGMEM = "Twinklecat@!,Twinkle rate;;!"; //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; } @@ -2399,30 +2642,32 @@ uint16_t WS2812FX::mode_halloween_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); + uint32_t c = color_blend(SEGMENT.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); + 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; } @@ -2430,18 +2675,19 @@ uint16_t WS2812FX::mode_halloween_eyes() 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; @@ -2451,20 +2697,22 @@ uint16_t WS2812FX::mode_static_pattern() 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) { @@ -2475,27 +2723,29 @@ 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)); } } } @@ -2505,222 +2755,231 @@ uint16_t WS2812FX::spots_base(uint16_t threshold) //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 -//Spark type is used for popcorn and 1D fireworks typedef struct Ball { unsigned long lastBounceTime; float impactVelocity; float height; } 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); + 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 = int(((SEGMENT.intensity * (maxNumBalls - 0.8f)) / 255) + 1); - - float gravity = -9.81; // standard value of gravity - float impactVelocityStart = sqrt( -2 * gravity); + uint8_t numBalls = SEGMENT.intensity/16 + 1; unsigned long time = millis(); if (SEGENV.call == 0) { - for (uint8_t i = 0; i < maxNumBalls; i++) balls[i].lastBounceTime = time; + 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; + 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. + } } + float cfac = float(scale8(8, 255-SEGMENT.speed) +1)*20000.0f; // this uses the Aircoookie conversion factor for scaling time using speed slider + bool hasCol2 = SEGCOLOR(2); - fill(hasCol2 ? BLACK : SEGCOLOR(1)); + if (!SEGMENT.check2) SEGMENT.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; + 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<-.5 || thisHeight> 1.5){ + 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 (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; + 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 + .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))); } else if (hasCol2) { color = SEGCOLOR(i % NUM_COLORS); } - uint16_t pos = round(balls[i].height * (SEGLEN - 1)); - setPixelColor(pos, color); - } - - return FRAMETIME; -} -/* -* bouncing balls on a track track Effect modified from Air Cookie's bouncing balls -*/ -// modified for balltrack mode -typedef struct Ballt { - unsigned long lastBounceUpdate; - float mass; // could fix this to be = 1. if memory is an issue - float velocity; - float height; -} ballt; - -uint16_t WS2812FX::ball_track(bool collide) { - //allocate segment data - uint16_t maxNumBalls = 16; - uint16_t dataSize = sizeof(ballt) * maxNumBalls; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - - Ballt* balls = reinterpret_cast(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 = int(((SEGMENT.intensity * (maxNumBalls - 0.8f)) / 255) + 1); - - unsigned long time = millis(); - - if (SEGENV.call == 0) { - for (uint8_t i = 0; i < maxNumBalls; i++) { - balls[i].lastBounceUpdate = time; - balls[i].velocity = 20.*float(random16(1000, 10000))/10000.;// number from 1 to 10 - if(random16(0,10000)<5000)balls[i].velocity=-balls[i].velocity; -// balls[i].velocity = 0; -// while(abs(balls[i].velocity)<.5){ // at the start make sure they are all moving -// balls[i].velocity=10*(-.5-float(random16(0, 10000)) / 10000.0); // time units are ms -// } - balls[i].height=(float(random16(0, 10000)) / 10000.0); // from 0. to 1. - balls[i].mass=(float(random16(1000, 10000)) / 10000.0); // from .5 to 1. - } - } - - float cfac = float((255-SEGMENT.speed)*8/256 +1)*20000.; // this uses the Air cookie conversion factor for scaling time using speed slider - - bool hasCol2 = SEGCOLOR(2); - fill(hasCol2 ? BLACK : SEGCOLOR(1)); - - for (uint8_t i = 0; i < numBalls; i++) { - float timeSinceLastUpdate = float((time - 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<-.5 || thisHeight> 1.5){ - balls[i].height=(float(random16(0, 10000)) / 10000.0); // from 0. to 1. - thisHeight=balls[i].height; - balls[i].lastBounceUpdate = time; - } -// check if reached ends of the strip - if ((thisHeight <= 0. && balls[i].velocity<0)||(thisHeight >= 1. && balls[i].velocity>0)){ - balls[i].velocity=-balls[i].velocity; // reverse velocity - balls[i].lastBounceUpdate = time; - balls[i].height=thisHeight; - } -// check for collisions - if(collide){ - for(uint8_t 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)&&(tcollided1) thisHeight=1; + if (thisHeight < 0.0f) thisHeight = 0.0f; + if (thisHeight > 1.0f) thisHeight = 1.0f; uint16_t pos = round(thisHeight * (SEGLEN - 1)); - setPixelColor(pos, color); - balls[i].lastBounceUpdate=time; - balls[i].height=thisHeight; + SEGMENT.setPixelColor(pos, color); + balls[i].lastBounceUpdate = strip.now; + balls[i].height = thisHeight; } return FRAMETIME; } - -uint16_t WS2812FX::mode_balltrack(void) { - return ball_track(false); -} -uint16_t WS2812FX::mode_balltrack_collide(void) { - return ball_track(true); -} +static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay;!,!,!;!;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) { - 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; @@ -2729,115 +2988,146 @@ 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) { - return sinelon_base(true, true); + +uint16_t mode_sinelon_rainbow(void) { + return sinelon_base(false, true); +} +static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!"; + + +// utility function that will add random glitter to SEGMENT +void glitter_base(uint8_t intensity, uint32_t col = ULTRAWHITE) { + if (intensity > random8()) { + if (SEGMENT.is2D()) { + SEGMENT.setPixelColorXY(random16(SEGMENT.virtualWidth()),random16(SEGMENT.virtualHeight()), col); + } else { + SEGMENT.setPixelColor(random16(SEGLEN), col); + } + } } - -//Rainbow with glitter, inspired by https://gist.github.com/kriegsman/062e10f7f07ba8518af6 -uint16_t WS2812FX::mode_glitter() +//Glitter with palette background, inspired by https://gist.github.com/kriegsman/062e10f7f07ba8518af6 +uint16_t mode_glitter() { - mode_palette(); - - if (SEGMENT.intensity > random8()) - { - setPixelColor(random16(SEGLEN), ULTRAWHITE); - } - + 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 +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 = 24; + uint16_t strips = SEGMENT.nrOfVStrips(); uint16_t dataSize = sizeof(spark) * maxNumPopcorn; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Spark* popcorn = reinterpret_cast(SEGENV.data); - float gravity = -0.0001 - (SEGMENT.speed/200000.0); // m/s/s - gravity *= SEGLEN; - bool hasCol2 = SEGCOLOR(2); - fill(hasCol2 ? BLACK : SEGCOLOR(1)); + if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); - uint8_t numPopcorn = SEGMENT.intensity*maxNumPopcorn/255; - if (numPopcorn == 0) numPopcorn = 1; + struct virtualStrip { + static void runStrip(uint16_t stripNr, Spark* popcorn) { + float gravity = -0.0001 - (SEGMENT.speed/200000.0); // m/s/s + gravity *= SEGLEN; - for(uint8_t i = 0; i < numPopcorn; i++) { - bool isActive = popcorn[i].pos >= 0.0f; + uint8_t numPopcorn = SEGMENT.intensity*maxNumPopcorn/255; + if (numPopcorn == 0) numPopcorn = 1; - if (isActive) { // if kernel is active, update its position - popcorn[i].pos += popcorn[i].vel; - popcorn[i].vel += gravity; - uint32_t col = color_wheel(popcorn[i].colIndex); - if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); + 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 ledIndex = popcorn[i].pos; - if (ledIndex < SEGLEN) setPixelColor(ledIndex, col); - } 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); - 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.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); } } } - } + }; + + for (int stripNr=0; stripNr> 1; + uint8_t rndval = valrange >> 1; //max 127 //step (how much to move closer to target per frame) coarsely set by speed uint8_t speedFactor = 4; @@ -2851,7 +3141,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 @@ -2874,9 +3164,9 @@ uint16_t WS2812FX::candle(bool multi) } if (newTarget) { - s_target = random8(rndval) + random8(rndval); + s_target = random8(rndval) + random8(rndval); //between 0 and rndval*2 -2 = 252 if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + random8(rndval); - uint8_t offset = (255 - valrange) >> 1; + uint8_t offset = (255 - valrange); s_target += offset; uint8_t dif = (s_target > s) ? s_target - s : s - s_target; @@ -2886,31 +3176,34 @@ uint16_t WS2812FX::candle(bool multi) } 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; } } - return FRAMETIME; + 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"; /* @@ -2918,9 +3211,12 @@ uint16_t WS2812FX::mode_candle_multi() / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ / Speed sets frequency of new starbursts, intensity is the intensity of the burst */ -#define STARBURST_MAX_FRAG 12 - -//each needs 64 byte +#ifdef ESP8266 + #define STARBURST_MAX_FRAG 8 //52 bytes / star +#else + #define STARBURST_MAX_FRAG 10 //60 bytes / star +#endif +//each needs 20+STARBURST_MAX_FRAG*4 bytes typedef struct particle { CRGB color; uint32_t birth =0; @@ -2930,9 +3226,16 @@ 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 = 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); - if (numStars > 15) numStars = 15; + if (numStars > maxStars) numStars = maxStars; uint16_t dataSize = sizeof(star) * numStars; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -2954,7 +3257,7 @@ uint16_t WS2812FX::mode_starburst(void) { uint16_t startPos = random16(SEGLEN-1); float multiplier = (float)(random8())/255.0 * 1.0; - stars[j].color = col_to_crgb(color_wheel(random8())); + 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; @@ -2969,7 +3272,7 @@ uint16_t WS2812FX::mode_starburst(void) { } } - fill(SEGCOLOR(1)); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); for (int j=0; j 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; + float particleSize = (1.0f - fade) * 2.0f; - for (uint8_t index=0; index < STARBURST_MAX_FRAG*2; index++) { + 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) { @@ -3026,60 +3329,77 @@ uint16_t WS2812FX::mode_starburst(void) { if (start == end) end++; 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); } } } } 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 numSparks = 2 + (SEGLEN >> 1); - if (numSparks > 80) numSparks = 80; + uint16_t maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 + 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 + ((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); - fill(BLACK); + 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; + } - 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; } // 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 } @@ -3090,48 +3410,56 @@ uint16_t WS2812FX::mode_exploding_fireworks(void) * 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].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[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; + *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; + 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 < SEGLEN) { + 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 } @@ -3139,184 +3467,303 @@ uint16_t WS2812FX::mode_exploding_fireworks(void) SEGENV.aux0--; if (SEGENV.aux0 < 4) { SEGENV.aux0 = 0; //back to flare - SEGENV.step = (SEGMENT.intensity > random8()); //decide firing side } } - SEGMENT.setOption(SEG_OPTION_REVERSED, actuallyReverse); - 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 - uint16_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); + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - float gravity = -0.001 - (SEGMENT.speed/50000.0); - gravity *= SEGLEN; - int sourcedrop = 12; + struct virtualStrip { + static void runStrip(uint16_t stripNr, Spark* drops) { - for (int j=0;j> 6); // 255>>6 = 3 - setPixelColor(SEGLEN-1,color_blend(BLACK,SEGCOLOR(0), sourcedrop));// water source - if (drops[j].colIndex==1) { - if (drops[j].col>255) drops[j].col=255; - setPixelColor(int(drops[j].pos),color_blend(BLACK,SEGCOLOR(0),drops[j].col)); + float gravity = -0.0005 - (SEGMENT.speed/50000.0); + gravity *= SEGLEN-1; + int sourcedrop = 12; - 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; - - for (int i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets - setPixelColor(int(drops[j].pos)+i,color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling + for (int j=0;j 2) { // during bounce, some water is on the floor - setPixelColor(0,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; + 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)); - } else { + drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling - if (drops[j].colIndex==2) { // init bounce - drops[j].vel = -drops[j].vel/4;// reverse velocity with damping - drops[j].pos += drops[j].vel; + 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 (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 + } } - drops[j].col = sourcedrop*2; - drops[j].colIndex = 5; // bouncing } } } - } + }; + + for (int stripNr=0; stripNr(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 + } + + if (drop->step == 0) { // init brick + // speed calcualtion: 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 (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 + } + } + } + }; + + 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(6,0, (255 - SEGMENT.intensity)|0x01 )); - CRGB color = ColorFromPalette(currentPalette, colorIndex, thisBright, LINEARBLEND); - setPixelColor(i, color.red, color.green, color.blue); + uint8_t colorIndex = cubicwave8((i*(2+ 3*(SEGMENT.speed >> 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(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. */ -uint16_t WS2812FX::mode_percent(void) { +uint16_t mode_percent(void) { - uint8_t percent = MAX(0, MIN(200, SEGMENT.intensity)); - uint16_t active_leds = (percent < 100) ? SEGLEN * percent / 100.0 + uint8_t percent = SEGMENT.intensity; + percent = constrain(percent, 0, 200); + 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 aproximation 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" @@ -3343,8 +3790,26 @@ 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 = strip.now; + CRGBPalette16 pacifica_palette_1 = { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117, 0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x14554B, 0x28AA50 }; @@ -3356,16 +3821,17 @@ uint16_t WS2812FX::mode_pacifica() 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 = (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); @@ -3380,12 +3846,12 @@ 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)); @@ -3408,156 +3874,131 @@ uint16_t WS2812FX::mode_pacifica() 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); } + strip.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); - } - 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 } return FRAMETIME; } - -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(i*20, 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 - uint16_t dataSize = sizeof(CRGBPalette16) * 2; //allocate space for 2 Palettes + uint16_t dataSize = sizeof(CRGBPalette16) * 2; //allocate space for 2 Palettes (2 * 16 * 3 = 96 bytes) if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed CRGBPalette16* palettes = reinterpret_cast(SEGENV.data); @@ -3576,50 +4017,52 @@ 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; } @@ -3630,42 +4073,3821 @@ 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)); - uint16_t numBirds = SEGLEN >> 2; - uint16_t span = SEGMENT.intensity << 8; + 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/numBirds; - int megumin = sin16(counter) + 0x8000; - uint32_t bird = (megumin * SEGLEN) >> 16; - uint32_t c = color_from_palette((i * 255)/ numBirds, false, true, 0); - setPixelColor(bird, c); + counter -= span; + uint16_t megumin = sin16(counter) + 0x8000; + 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 { + float speed; + uint8_t colorIdx; + int16_t position; + unsigned long lastUpdateTime; + uint8_t width; + uint8_t type; +} spotlight; + +#define SPOT_TYPE_SOLID 0 +#define SPOT_TYPE_GRADIENT 1 +#define SPOT_TYPE_2X_GRADIENT 2 +#define SPOT_TYPE_2X_DOT 3 +#define SPOT_TYPE_3X_DOT 4 +#define SPOT_TYPE_4X_DOT 5 +#define SPOT_TYPES_COUNT 6 +#ifdef ESP8266 + #define SPOT_MAX_COUNT 17 //Number of simultaneous waves +#else + #define SPOT_MAX_COUNT 49 //Number of simultaneous waves +#endif + +/* + * Spotlights moving back and forth that cast dancing shadows. + * Shine this through tree branches/leaves or other close-up objects that cast + * interesting shadows onto a ceiling or tarp. + * + * By Steve Pomeroy @xxv + */ +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; + + uint16_t dataSize = sizeof(spotlight) * numSpotlights; + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + Spotlight* spotlights = reinterpret_cast(SEGENV.data); + + SEGMENT.fill(BLACK); + + unsigned long time = millis(); + bool respawn = false; + + for (size_t i = 0; i < numSpotlights; i++) { + if (!initialize) { + // advance the position of the spotlight + int16_t delta = (float)(time - spotlights[i].lastUpdateTime) * + (spotlights[i].speed * ((1.0 + SEGMENT.speed)/100.0)); + + if (abs(delta) >= 1) { + spotlights[i].position += delta; + spotlights[i].lastUpdateTime = time; + } + + respawn = (spotlights[i].speed > 0.0 && spotlights[i].position > (SEGLEN + 2)) + || (spotlights[i].speed < 0.0 && spotlights[i].position < -(spotlights[i].width + 2)); + } + + if (initialize || respawn) { + spotlights[i].colorIdx = random8(); + spotlights[i].width = random8(1, 10); + + spotlights[i].speed = 1.0/random8(4, 50); + + if (initialize) { + spotlights[i].position = random16(SEGLEN); + spotlights[i].speed *= random8(2) ? 1.0 : -1.0; + } else { + if (random8(2)) { + spotlights[i].position = SEGLEN + spotlights[i].width; + spotlights[i].speed *= -1.0; + }else { + spotlights[i].position = -spotlights[i].width; + } + } + + spotlights[i].lastUpdateTime = time; + spotlights[i].type = random8(SPOT_TYPES_COUNT); + } + + 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) { + SEGMENT.blendPixelColor(start, color, 128); + } + } else { + switch (spotlights[i].type) { + case SPOT_TYPE_SOLID: + for (size_t j = 0; j < spotlights[i].width; j++) { + if ((start + j) >= 0 && (start + j) < SEGLEN) { + SEGMENT.blendPixelColor(start + j, color, 128); + } + } + break; + + case SPOT_TYPE_GRADIENT: + for (size_t j = 0; j < spotlights[i].width; j++) { + if ((start + j) >= 0 && (start + j) < SEGLEN) { + SEGMENT.blendPixelColor(start + j, color, cubicwave8(map(j, 0, spotlights[i].width - 1, 0, 255))); + } + } + break; + + case SPOT_TYPE_2X_GRADIENT: + for (size_t j = 0; j < spotlights[i].width; j++) { + if ((start + j) >= 0 && (start + j) < SEGLEN) { + SEGMENT.blendPixelColor(start + j, color, cubicwave8(2 * map(j, 0, spotlights[i].width - 1, 0, 255))); + } + } + break; + + case SPOT_TYPE_2X_DOT: + for (size_t j = 0; j < spotlights[i].width; j += 2) { + if ((start + j) >= 0 && (start + j) < SEGLEN) { + SEGMENT.blendPixelColor(start + j, color, 128); + } + } + break; + + case SPOT_TYPE_3X_DOT: + for (size_t j = 0; j < spotlights[i].width; j += 3) { + if ((start + j) >= 0 && (start + j) < SEGLEN) { + SEGMENT.blendPixelColor(start + j, color, 128); + } + } + break; + + case SPOT_TYPE_4X_DOT: + for (size_t j = 0; j < spotlights[i].width; j += 4) { + if ((start + j) >= 0 && (start + j) < SEGLEN) { + SEGMENT.blendPixelColor(start + j, color, 128); + } + } + break; + } + } + } + + 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 mode_washing_machine(void) { + int speed = tristate_square8(strip.now >> 7, 90, 15); + + SEGENV.step += (speed * 2048) / (512 - SEGMENT.speed); + + for (int i = 0; i < SEGLEN; i++) { + uint8_t col = sin8(((SEGMENT.intensity / 25 + 1) * 255 * i / SEGLEN) + (SEGENV.step >> 7)); + 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 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 = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8; + + for (int i = 0; i < pixelLen; i++) { + 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++) { + 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 + Modified and adapted to WLED by Def3nder, based on "Fake TV Light for Engineers" by Phillip Burgess https://learn.adafruit.com/fake-tv-light-for-engineers/arduino-sketch +*/ +//43 bytes +typedef struct TvSim { + uint32_t totalTime = 0; + uint32_t fadeTime = 0; + uint32_t startTime = 0; + uint32_t elapsed = 0; + uint32_t pixelNum = 0; + uint16_t sliderValues = 0; + uint32_t sceeneStart = 0; + uint32_t sceeneDuration = 0; + uint16_t sceeneColorHue = 0; + uint8_t sceeneColorSat = 0; + uint8_t sceeneColorBri = 0; + uint8_t actualColorR = 0; + uint8_t actualColorG = 0; + uint8_t actualColorB = 0; + uint16_t pr = 0; // Prev R, G, B + uint16_t pg = 0; + uint16_t pb = 0; +} tvSim; + +uint16_t mode_tv_simulator(void) { + uint16_t nr, ng, nb, r, g, b, i, hue; + uint8_t sat, bri, j; + + if (!SEGENV.allocateData(sizeof(tvSim))) return mode_static(); //allocation failed + TvSim* tvSimulator = reinterpret_cast(SEGENV.data); + + uint8_t colorSpeed = map(SEGMENT.speed, 0, UINT8_MAX, 1, 20); + uint8_t colorIntensity = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 30); + + i = SEGMENT.speed << 8 | SEGMENT.intensity; + if (i != tvSimulator->sliderValues) { + tvSimulator->sliderValues = i; + SEGENV.aux1 = 0; + } + + // create a new sceene + if (((millis() - tvSimulator->sceeneStart) >= tvSimulator->sceeneDuration) || SEGENV.aux1 == 0) { + tvSimulator->sceeneStart = millis(); // remember the start of the new sceene + tvSimulator->sceeneDuration = random16(60* 250* colorSpeed, 60* 750 * colorSpeed); // duration of a "movie sceene" which has similar colors (5 to 15 minutes with max speed slider) + tvSimulator->sceeneColorHue = random16( 0, 768); // random start color-tone for the sceene + tvSimulator->sceeneColorSat = random8 ( 100, 130 + colorIntensity); // random start color-saturation for the sceene + 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; + + // calculate R,G,B from HSV + // Source: https://blog.adafruit.com/2012/03/14/constant-brightness-hsb-to-rgb-algorithm/ + { // just to create a local scope for the variables + uint8_t temp[5], n = (hue >> 8) % 3; + uint8_t x = ((((hue & 255) * sat) >> 8) * bri) >> 8; + uint8_t s = ( (256 - sat) * bri) >> 8; + temp[0] = temp[3] = s; + temp[1] = temp[4] = x + s; + temp[2] = bri - x; + tvSimulator->actualColorR = temp[n + 2]; + tvSimulator->actualColorG = temp[n + 1]; + tvSimulator->actualColorB = temp[n ]; + } + } + // Apply gamma correction, further expand to 16/16/16 + nr = (uint8_t)gamma8(tvSimulator->actualColorR) * 257; // New R/G/B + ng = (uint8_t)gamma8(tvSimulator->actualColorG) * 257; + nb = (uint8_t)gamma8(tvSimulator->actualColorB) * 257; + + if (SEGENV.aux0 == 0) { // initialize next iteration + SEGENV.aux0 = 1; + + // randomize total duration and fade duration for the actual color + tvSimulator->totalTime = random16(250, 2500); // Semi-random pixel-to-pixel time + tvSimulator->fadeTime = random16(0, tvSimulator->totalTime); // Pixel-to-pixel transition time + if (random8(10) < 3) tvSimulator->fadeTime = 0; // Force scene cut 30% of time + + tvSimulator->startTime = millis(); + } // end of initialization + + // how much time is elapsed ? + tvSimulator->elapsed = millis() - tvSimulator->startTime; + + // fade from prev volor to next color + if (tvSimulator->elapsed < tvSimulator->fadeTime) { + 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() + r = nr; + g = ng; + b = nb; + } + + // set strip color + for (i = 0; i < SEGLEN; i++) { + 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 + if ( tvSimulator->elapsed >= tvSimulator->totalTime) { + tvSimulator->pr = nr; // Prev RGB = new RGB + tvSimulator->pg = ng; + tvSimulator->pb = nb; + SEGENV.aux0 = 0; + } + + return FRAMETIME; +} +static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;"; + + +/* + Aurora effect +*/ + +//CONFIG +#ifdef ESP8266 + #define W_MAX_COUNT 9 //Number of simultaneous waves +#else + #define W_MAX_COUNT 20 //Number of simultaneous waves +#endif +#define W_MAX_SPEED 6 //Higher number, higher speed +#define W_WIDTH_FACTOR 6 //Higher number, smaller waves + +//24 bytes +class AuroraWave { + private: + uint16_t ttl; + CRGB basecolor; + float basealpha; + uint16_t age; + uint16_t width; + float center; + bool goingleft; + float speed_factor; + bool alive = true; + + public: + void init(uint32_t segment_length, CRGB color) { + ttl = random(500, 1501); + basecolor = color; + basealpha = random(60, 101) / (float)100; + age = 0; + width = random(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier + if (!width) width = 1; + center = random(101) / (float)100 * segment_length; + goingleft = random(0, 2) == 0; + speed_factor = (random(10, 31) / (float)100 * W_MAX_SPEED / 255); + alive = true; + } + + CRGB getColorForLED(int ledIndex) { + if(ledIndex < center - width || ledIndex > center + width) return 0; //Position out of range of this wave + + CRGB rgb; + + //Offset of this led from center of wave + //The further away from the center, the dimmer the LED + float offset = ledIndex - center; + if (offset < 0) offset = -offset; + float offsetFactor = offset / width; + + //The age of the wave determines it brightness. + //At half its maximum age it will be the brightest. + float ageFactor = 0.1; + if((float)age / ttl < 0.5) { + ageFactor = (float)age / (ttl / 2); + } else { + ageFactor = (float)(ttl - age) / ((float)ttl * 0.5); + } + + //Calculate color based on above factors and basealpha value + float factor = (1 - offsetFactor) * ageFactor * basealpha; + rgb.r = basecolor.r * factor; + rgb.g = basecolor.g * factor; + rgb.b = basecolor.b * factor; + + return rgb; + }; + + //Change position and age of wave + //Determine if its sill "alive" + void update(uint32_t segment_length, uint32_t speed) { + if(goingleft) { + center -= speed_factor * speed; + } else { + center += speed_factor * speed; + } + + age++; + + if(age > ttl) { + alive = false; + } else { + if(goingleft) { + if(center + width < 0) { + alive = false; + } + } else { + if(center - width > segment_length) { + alive = false; + } + } + } + }; + + bool stillAlive() { + return alive; + }; +}; + +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); + SEGENV.aux0 = SEGMENT.intensity; + + if(!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) { // 26 on 32 segment ESP32, 9 on 16 segment ESP8266 + return mode_static(); //allocation failed + } + + waves = reinterpret_cast(SEGENV.data); + + 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++) { + //Update values of wave + waves[i].update(SEGLEN, SEGMENT.speed); + + if(!(waves[i].stillAlive())) { + //If a wave dies, reinitialize it starts over. + waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random(0, 3)))); + } + } + + uint8_t backlight = 1; //dimmer backlight if less active colors + if (SEGCOLOR(0)) backlight++; + if (SEGCOLOR(1)) backlight++; + if (SEGCOLOR(2)) backlight++; + //Loop through LEDs to determine color + 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++) { + CRGB rgb = waves[j].getColorForLED(i); + + if(rgb != CRGB(0)) { + mixedRgb += rgb; + } + } + + 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; + + // initialize on first call + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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) { + SEGMENT.fill(BLACK); + 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(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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); + } + + uint16_t xscale = SEGMENT.intensity*4; + uint32_t yscale = SEGMENT.speed*8; + uint8_t indexx = 0; + + SEGPALETTE = CRGBPalette16( CRGB(0,0,0), CRGB(0,0,0), CRGB(0,0,0), CRGB(0,0,0), + 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+millis()/4); // We're moving along our Perlin map. + SEGMENT.setPixelColorXY(j, i, ColorFromPalette(SEGPALETTE, min(i*(indexx)>>4, 255), 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;;!;2"; + + +////////////////////////////// +// 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(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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; +} // 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. + 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 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; + for (int row=rows-1; row>=0; row--) { + for (int col=0; col>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(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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) { + SEGMENT.fill(BLACK); + 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 * 8 / ((SEGMENT.speed>>5)+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; + SEGMENT.fill(BLACK); + random16_set_seed(strip.now); + lighter->angleSpeed = random8(0,20) - 10; + 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; + } + } + + 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(20); + + // 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;!;!;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; + int letterHeight; + 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; + } + const bool zero = SEGMENT.check3; + const int yoffset = map(SEGMENT.intensity, 0, 255, -rows/2, rows/2) + (rows-letterHeight)/2; + 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]; + + if (!strlen(text) + || !strncmp_P(text,PSTR("#DATE"),5) + || !strncmp_P(text,PSTR("#DDMM"),5) + || !strncmp_P(text,PSTR("#MMDD"),5) + || !strncmp_P(text,PSTR("#TIME"),5) + || !strncmp_P(text,PSTR("#HHMM"),5)) { // fallback if empty segment name: display date and time + char sec[5]; + byte AmPmHour = hour(localTime); + boolean isitAM = true; + if (useAMPM) { + if (AmPmHour > 11) { AmPmHour -= 12; isitAM = false; } + if (AmPmHour == 0) { AmPmHour = 12; } + } + if (useAMPM) sprintf_P(sec, PSTR(" %2s"), (isitAM ? "AM" : "PM")); + else sprintf_P(sec, PSTR(":%02d"), second(localTime)); + 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 sprintf_P(text, zero?PSTR("%s %02d, %04d %02d:%02d%s"):PSTR("%s %d, %d %d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec); + } + const int numberOfLetters = strlen(text); + + if (SEGENV.step < millis()) { + if ((numberOfLetters * letterWidth) > cols) ++SEGENV.aux0 %= (numberOfLetters * letterWidth) + cols; // offset + else SEGENV.aux0 = (cols + (numberOfLetters * letterWidth))/2; + ++SEGENV.aux1 &= 0xFF; // color shift + SEGENV.step = millis() + map(SEGMENT.speed, 0, 255, 10*FRAMETIME_FIXED, 2*FRAMETIME_FIXED); + if (!SEGMENT.check2) { + for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++ ) + SEGMENT.blendPixelColorXY(x, y, SEGCOLOR(1), 255 - (SEGMENT.custom1>>1)); + } + } + for (int i = 0; i < numberOfLetters; i++) { + if (int(cols) - int(SEGENV.aux0) + letterWidth*(i+1) < 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], int(cols) - int(SEGENV.aux0) + letterWidth*i, yoffset, letterWidth, letterHeight, col1, col2); + } + + return FRAMETIME; +} +static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,,Gradient,Overlay,0;!,!,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; + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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]; + + // printUmData(); + + 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(); + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + + 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. + + 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 "sensitivty" upscaling + + float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 32, 0, (float)SEGLEN/2.0); // 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. + + 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.0; + segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivty" upscaling + + float mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0); // 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. + + 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 "sensitivty" 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. + 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. +// 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 sensitity/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. + + 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/(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. + 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. + // 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. + 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 roughtly 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,Sensivity;;;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; // log10(0) is "forbidden" (throws exception) + + uint16_t fadeRate = 2*SEGMENT.speed - SEGMENT.speed*SEGMENT.speed/255; // Get to 255 as quick as you can. + + if (SEGENV.call == 0) SEGMENT.fill(BLACK); + int fadeoutDelay = (256 - SEGMENT.speed) / 64; + if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(fadeRate); + + for (int i=0; i < SEGMENT.intensity/32+1; i++) { + uint16_t locn = random16(0,SEGLEN); + 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 + 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. + 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 roughtly 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. + + 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.125; // divide by 8, to compensate for later "sensitivty" upscaling + + float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0,32, 0, (float)SEGLEN/2.0); // 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,Sensivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin + + +////////////////////// +// ** Noisemove // +////////////////////// +uint16_t mode_noisemove(void) { // Noisemove. 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); + } + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + if (SEGENV.call == 0) SEGMENT.fill(BLACK); + //SEGMENT.fade_out(224); // Just in case something doesn't get faded. + 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; + + if (SEGENV.call == 0) SEGMENT.fill(BLACK); + 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; // 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 (SEGENV.call == 0) SEGMENT.fill(BLACK); + + 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; + } + + if (SEGENV.call == 0) SEGMENT.fill(BLACK); + 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; + + 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); + CRGB pix = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); + SEGMENT.setPixelColorXY(x, y, pix); + } + } + + 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); + CRGB pix = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); + SEGMENT.setPixelColorXY(x, y, pix); + } + } + + 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 +#define USE_GET_MILLISECOND_TIMER #include "FastLED.h" #define DEFAULT_BRIGHTNESS (uint8_t)127 #define DEFAULT_MODE (uint8_t)0 #define DEFAULT_SPEED (uint8_t)128 +#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)) +#endif +#ifndef MAX #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 (1000/WLED_FPS) +#define FRAMETIME_FIXED (1000/WLED_FPS) +//#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 */ -#define MAX_NUM_SEGMENTS 10 - -/* How much data bytes all segments combined may allocate */ #ifdef ESP8266 -#define MAX_SEGMENT_DATA 2048 + #define MAX_NUM_SEGMENTS 16 + /* How much data bytes all segments combined may allocate */ + #define MAX_SEGMENT_DATA 5120 #else -#define MAX_SEGMENT_DATA 8192 + #ifndef MAX_NUM_SEGMENTS + #define MAX_NUM_SEGMENTS 32 + #endif + #if defined(ARDUINO_ARCH_ESP32S2) + #define MAX_SEGMENT_DATA 24576 + #else + #define MAX_SEGMENT_DATA 32767 + #endif #endif -#define LED_SKIP_AMOUNT 1 -#define MIN_SHOW_DELAY 15 +/* 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 / strip.getMaxSegments()) + +#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) gamma32(_segments[_segment_index].colors[x]) -#define SEGENV _segment_runtimes[_segment_index] -#define SEGLEN _virtualSegmentLength -#define SEGACT SEGMENT.stop -#define SPEED_FORMULA_L 5 + (50*(255 - SEGMENT.speed))/SEGLEN -#define RESET_RUNTIME memset(_segment_runtimes, 0, sizeof(_segment_runtimes)) +#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 strip._currentPalette +#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 @@ -81,24 +108,25 @@ #define ORANGE (uint32_t)0xFF3000 #define PINK (uint32_t)0xFF1493 #define ULTRAWHITE (uint32_t)0xFFFFFFFF +#define DARKSLATEGRAY (uint32_t)0x2F4F4F +#define DARKSLATEGREY (uint32_t)0x2F4F4F // options // bit 7: segment is in transition mode -// bits 3-6: TBD +// 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 SEGMENT_ON (uint8_t)0x04 -#define REVERSE (uint8_t)0x02 -#define SELECTED (uint8_t)0x01 -#define IS_TRANSITIONAL ((SEGMENT.options & TRANSITIONAL) == TRANSITIONAL) -#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 114 +#define NO_OPTIONS (uint16_t)0x0000 +#define TRANSPOSED (uint16_t)0x0400 // rotated 90deg & reversed +#define REVERSE_Y_2D (uint16_t)0x0200 +#define MIRROR_Y_2D (uint16_t)0x0100 +#define TRANSITIONAL (uint16_t)0x0080 +#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 @@ -119,7 +147,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 @@ -138,22 +166,22 @@ #define FX_MODE_TRAFFIC_LIGHT 35 #define FX_MODE_COLOR_SWEEP_RANDOM 36 #define FX_MODE_RUNNING_COLOR 37 -#define FX_MODE_RUNNING_RED_BLUE 38 +#define FX_MODE_AURORA 38 #define FX_MODE_RUNNING_RANDOM 39 #define FX_MODE_LARSON_SCANNER 40 #define FX_MODE_COMET 41 #define FX_MODE_FIREWORKS 42 #define FX_MODE_RAIN 43 -#define FX_MODE_MERRY_CHRISTMAS 44 +#define FX_MODE_TETRIX 44 //was Merry Christmas prior to 0.12.0 (use "Chase 2" with Red/Green) #define FX_MODE_FIRE_FLICKER 45 #define FX_MODE_GRADIENT 46 #define FX_MODE_LOADING 47 -#define FX_MODE_POLICE 48 -#define FX_MODE_POLICE_ALL 49 +#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_TWO_AREAS 51 -#define FX_MODE_CIRCUS_COMBUSTUS 52 -#define FX_MODE_HALLOWEEN 53 +#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 // removed in 0.14! #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 #define FX_MODE_TRICOLOR_FADE 56 @@ -203,7 +231,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 @@ -212,505 +240,683 @@ #define FX_MODE_PHASEDNOISE 109 #define FX_MODE_FLOW 110 #define FX_MODE_CHUNCHUN 111 -#define FX_MODE_BALLTRACK 112 -#define FX_MODE_BALLTRACK_COLLIDE 113 -class WS2812FX { - typedef uint16_t (WS2812FX::*mode_ptr)(void); +#define FX_MODE_DANCING_SHADOWS 112 +#define FX_MODE_WASHING_MACHINE 113 +// #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 // candidate for removal (check3 in dynamic) - // pre show callback - typedef void (*show_callback) (void); +// 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 - // segment parameters +// 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: - typedef struct Segment { // 24 bytes - uint16_t start; - uint16_t stop; //segment invalid if stop == 0 - 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]; - void setOption(uint8_t n, bool val) - { - if (val) { - options |= 0x01 << n; - } else - { - options &= ~(0x01 << n); - } - } - bool getOption(uint8_t n) - { - return ((options >> n) & 0x01); - } - bool isSelected() - { - return getOption(0); - } - bool isActive() - { - return stop > start; - } - uint16_t length() - { - return stop - start; - } - uint16_t groupLength() - { - return grouping + spacing; - } - uint16_t virtualLength() - { - uint16_t groupLen = groupLength(); - return (length() + groupLen -1) / groupLen; - } - } segment; + 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 transitional: 1; // 6 : transitional (there is transition occuring) + bool reverse_y : 1; // 7 : reversed Y (2D) + bool mirror_y : 1; // 8 : mirrored Y (2D) + bool transpose : 1; // 9 : transposed (2D, swapped X & Y) + uint8_t map1D2D : 3; // 10-12 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) + uint8_t soundSim : 1; // 13 : 0-1 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; - // segment runtime parameters - typedef struct Segment_runtime { // 28 bytes - unsigned long next_time; - uint32_t step; - uint32_t call; - uint16_t aux0; - uint16_t aux1; - byte* data = nullptr; - bool allocateData(uint16_t len){ - if (data && _dataLen == len) return true; //already allocated - deallocateData(); - if (WS2812FX::_usedSegmentData + len > MAX_SEGMENT_DATA) return false; //not enough memory - data = new (std::nothrow) byte[len]; - if (!data) return false; //allocation failed - WS2812FX::_usedSegmentData += len; - _dataLen = len; - memset(data, 0, len); - return true; - } - void deallocateData(){ - delete[] data; - data = nullptr; - WS2812FX::_usedSegmentData -= _dataLen; - _dataLen = 0; - } - void reset(){next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; deallocateData();} - private: - uint16_t _dataLen = 0; - } segment_runtime; + // 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) - WS2812FX() { - //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_RUNNING_RED_BLUE] = &WS2812FX::mode_running_red_blue; - _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_MERRY_CHRISTMAS] = &WS2812FX::mode_merry_christmas; - _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_POLICE_ALL] = &WS2812FX::mode_police_all; - _mode[FX_MODE_TWO_DOTS] = &WS2812FX::mode_two_dots; - _mode[FX_MODE_TWO_AREAS] = &WS2812FX::mode_two_areas; - _mode[FX_MODE_CIRCUS_COMBUSTUS] = &WS2812FX::mode_circus_combustus; - _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_BALLTRACK] = &WS2812FX::mode_balltrack; - _mode[FX_MODE_BALLTRACK_COLLIDE] = &WS2812FX::mode_balltrack_collide; - _brightness = DEFAULT_BRIGHTNESS; - currentPalette = CRGBPalette16(CRGB::Black); - targetPalette = CloudColors_p; - ablMilliampsMax = 850; - currentMilliamps = 0; - timebase = 0; - bus = new NeoPixelWrapper(); - resetSegments(); + 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; + + // transition data, valid only if transitional==true, holds values during transition (72 bytes) + struct Transition { + uint32_t _colorT[NUM_COLORS]; + 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 belnds possible) + uint8_t _modeP; // previous mode/effect + //uint16_t _aux0, _aux1; // previous mode/effect runtime data + //uint32_t _step, _call; // previous mode/effect runtime data + //byte *_data; // previous mode/effect runtime data + unsigned long _start; // must accommodate millis() + uint16_t _dur; + Transition(uint16_t dur=750) + : _briT(255) + , _cctT(127) + , _palT(CRGBPalette16(CRGB::Black)) + , _prevPaletteBlends(0) + , _modeP(FX_MODE_STATIC) + , _start(millis()) + , _dur(dur) + {} + Transition(uint16_t d, uint8_t b, uint8_t c, const uint32_t *o) + : _briT(b) + , _cctT(c) + , _palT(CRGBPalette16(CRGB::Black)) + , _prevPaletteBlends(0) + , _modeP(FX_MODE_STATIC) + , _start(millis()) + , _dur(d) + { + for (size_t i=0; i> n) & 0x01); } + inline bool isSelected(void) const { return selected; } + 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; } + + 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 handleTransition(void); + uint16_t progress(void); //transition progression between 0-65535 + uint8_t currentBri(uint8_t briNew, bool useCct = false); + uint8_t currentMode(uint8_t modeNew); + uint32_t currentColor(uint8_t slot, uint32_t colorNew); + CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); + CRGBPalette16 ¤tPalette(CRGBPalette16 &tgt, uint8_t paletteID); + + // 1D strip + uint16_t virtualLength(void) const; + void setPixelColor(int n, uint32_t c); // set relative pixel within segment with color + 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); + uint8_t get_random_wheel_index(uint8_t pos); + 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(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); + 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) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0)); } // 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) {} + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB color) {} + 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; + + public: + + 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 + _currentPalette(CRGBPalette16(CRGB::Black)), + _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; + _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 - init(bool supportWhite, uint16_t countPixels, bool skipFirst), +#ifdef WLED_DEBUG + printSize(), +#endif + finalizeInit(), service(void), - blur(uint8_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), - setBrightness(uint8_t b), + 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), setTransitionMode(bool t), - trigger(void), - setSegment(uint8_t n, uint16_t start, uint16_t stop, uint8_t grouping = 0, uint8_t spacing = 0), + 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(), - setPixelColor(uint16_t n, uint32_t c), - setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0), + makeAutoSegments(bool forceReset = false), + fixInvalidSegments(), + setPixelColor(int n, uint32_t c), show(void), - setRgbwPwm(void); + 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 - reverseMode = false, - gammaCorrectBri = false, - gammaCorrectCol = true, - applyToAllSelected = true, - segmentsAreIdentical(Segment* a, Segment* b), - setEffectConfig(uint8_t m, uint8_t s, uint8_t i, uint8_t p); + checkSegmentAlignment(void), + hasRGBWBus(void), + hasCCTBus(void), + // return true if the strip is being sent pixel updates + 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 - mainSegment = 0, - rgbwMode = RGBW_MODE_DUAL, - paletteFade = 0, - paletteBlend = 0, - colorOrder = 0, - milliampsPerLed = 55, - getBrightness(void), - getMode(void), - getSpeed(void), - getModeCount(void), - getPaletteCount(void), - getMaxSegments(void), - //getFirstSelectedSegment(void), - getMainSegmentId(void), - gamma8(uint8_t), - get_random_wheel_index(uint8_t); + paletteFade, + paletteBlend, + milliampsPerLed, + cctBlending, + getActiveSegmentsNum(void), + getFirstSelectedSegId(void), + getLastActiveSegmentId(void), + 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); + 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,uint8_t), - gamma32(uint32_t), - getLastShow(void), - getPixelColor(uint16_t), - getColor(void); + getPixelColor(uint16_t); - WS2812FX::Segment& - getSegment(uint8_t n); + inline uint32_t getLastShow(void) { return _lastShow; } + inline uint32_t segColor(uint8_t i) { return _colors_t[i]; } - WS2812FX::Segment_runtime - getSegmentRuntime(void); + const char * + getModeData(uint8_t id = 0) { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } - WS2812FX::Segment* - getSegments(void); + const char ** + getModeDataSrc(void) { return &(_modeData[0]); } // vectors use arrays for underlying data - // 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_running_red_blue(void), - mode_running_random(void), - mode_larson_scanner(void), - mode_comet(void), - mode_fireworks(void), - mode_rain(void), - mode_merry_christmas(void), - mode_halloween(void), - mode_fire_flicker(void), - mode_gradient(void), - mode_loading(void), - mode_police(void), - mode_police_all(void), - mode_two_dots(void), - mode_two_areas(void), - mode_circus_combustus(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_balltrack(void), - mode_balltrack_collide(void); + 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]); } + + // 2D support (panels) + bool + isMatrix; + +#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 + + void + setUpMatrix(), + setPixelColorXY(int x, int y, uint32_t c); + + // outsmart the compiler :) by correctly overloading + inline 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 + inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + + uint32_t + getPixelColorXY(uint16_t, uint16_t); + + // end 2D support + + void loadCustomPalettes(void); // loads custom palettes from JSON + CRGBPalette16 _currentPalette; // palette used for current effect (includes transition) + 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; + + std::vector _segments; + friend class Segment; private: - NeoPixelWrapper *bus; + uint16_t _length; + uint8_t _brightness; + uint16_t _transitionDur; - uint32_t crgb_to_col(CRGB fastled); - CRGB col_to_crgb(uint32_t); - CRGBPalette16 currentPalette; - CRGBPalette16 targetPalette; + uint8_t _targetFps; + uint16_t _frametime; + uint16_t _cumulativeFps; - uint32_t now; - uint16_t _length, _lengthRaw, _virtualSegmentLength; - uint16_t _rand16seed; - uint8_t _brightness; - static uint16_t _usedSegmentData; - - void load_gradient_palette(uint8_t); - void handle_palette(void); - void fill(uint32_t); - - bool - _useRgbw = false, - _skipFirstMode, - _triggered; - - mode_ptr _mode[MODE_COUNT]; // SRAM footprint: 4 bytes per element - - show_callback _callback = nullptr; - - // mode helper functions - uint16_t - blink(uint32_t, uint32_t, bool strobe, bool), - candle(bool), - color_wipe(bool, bool), - scan(bool), - theater_chase(uint32_t, uint32_t, bool), - running_base(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, bool), - running(uint32_t, uint32_t), - tricolor_chase(uint32_t, uint32_t), - twinklefox_base(bool), - spots_base(uint16_t), - phased_base(uint8_t), - ball_track(bool collide); - - 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); - - uint32_t _lastPaletteChange = 0; - uint32_t _lastShow = 0; - - #ifdef WLED_USE_ANALOG_LEDS - uint32_t _analogLastShow = 0; - RgbwColor _analogLastColor = 0; - uint8_t _analogLastBri = 0; - #endif - - uint8_t _segment_index = 0; - uint8_t _segment_index_palette_last = 99; - segment _segments[MAX_NUM_SEGMENTS] = { // SRAM footprint: 24 bytes per element - // start, stop, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[] - { 0, 7, DEFAULT_SPEED, 128, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}} + // 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; - uint16_t realPixelIndex(uint16_t i); + uint8_t _modeCount; + std::vector _mode; // SRAM footprint: 4 bytes per element + std::vector _modeData; // mode (effect) name and its slider control data array + + 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","Running 2","Red & Blue","Stream", -"Scanner","Lighthouse","Fireworks","Rain","Merry Christmas","Fire Flicker","Gradient","Loading","Police","Police All", -"Two Dots","Two Areas","Circus","Halloween","Tri Chase","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","Ball Track","Ball Track Collide" -])====="; - - -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" -])====="; +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 00000000..71000e90 --- /dev/null +++ b/wled00/FX_2Dfcn.cpp @@ -0,0 +1,622 @@ +/* + 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 (uint16_t i=0; i= _length) return; + busses.setPixelColor(index, col); +} + +// returns RGBW values of pixel +uint32_t WS2812FX::getPixelColorXY(uint16_t x, uint16_t y) { +#ifndef WLED_DISABLE_2D + uint16_t index = (y * Segment::maxWidth + x); +#else + uint16_t index = x; +#endif + if (index < customMappingSize) index = customMappingTable[index]; + if (index >= _length) return 0; + return busses.getPixelColor(index); +} + +/////////////////////////////////////////////////////////// +// Segment:: routines +/////////////////////////////////////////////////////////// + +#ifndef WLED_DISABLE_2D + +// XY(x,y) - gets pixel index within current segment (often used to reference leds[] array element) +uint16_t /*IRAM_ATTR*/ Segment::XY(uint16_t x, uint16_t y) { + uint16_t width = virtualWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + uint16_t height = virtualHeight(); // segment height in logical pixels (is always >= 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(on ? opacity : 0); + 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 + + 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 + + strip.setPixelColorXY(start + xX, startY + yY, col); + + if (mirror) { //set the corresponding horizontally mirrored pixel + if (transpose) strip.setPixelColorXY(start + xX, startY + height() - yY - 1, col); + else strip.setPixelColorXY(start + width() - xX - 1, startY + yY, col); + } + if (mirror_y) { //set the corresponding vertically mirrored pixel + if (transpose) strip.setPixelColorXY(start + width() - xX - 1, startY + yY, col); + else strip.setPixelColorXY(start + xX, startY + height() - yY - 1, col); + } + if (mirror_y && mirror) { //set the corresponding vertically AND horizontally mirrored pixel + strip.setPixelColorXY(width() - xX - 1, height() - yY - 1, col); + } + } + } +} + +// 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 + uint32_t col = getPixelColorXY(x,y); + uint8_t r = R(col); + uint8_t g = G(col); + uint8_t b = B(col); + uint8_t w = W(col); + if (fast) { + r = qadd8(r, R(color)); + g = qadd8(g, G(color)); + b = qadd8(b, B(color)); + w = qadd8(w, W(color)); + col = RGBW32(r,g,b,w); + } else { + col = color_add(col, color); + } + setPixelColorXY(x, y, col); +} + +void Segment::fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { + if (!isActive()) return; // not active + CRGB pix = CRGB(getPixelColorXY(x,y)).nscale8_video(fade); + setPixelColorXY(x, y, pix); +} + +// blurRow: perform a blur on a row of a rectangular matrix +void Segment::blurRow(uint16_t row, fract8 blur_amount) { + if (!isActive()) 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 (uint_fast16_t 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()) 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 (uint_fast16_t 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()) 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 (uint16_t 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 (uint16_t 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 (uint16_t 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()) 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()) 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) { + 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= rows) break; // drawing off-screen + uint8_t bits = 0; + switch (font) { + case 24: bits = pgm_read_byte_near(&console_font_4x6[(chr * h) + i]); break; // 5x8 font + case 40: bits = pgm_read_byte_near(&console_font_5x8[(chr * h) + i]); break; // 5x8 font + case 48: bits = pgm_read_byte_near(&console_font_6x8[(chr * h) + i]); break; // 6x8 font + case 63: bits = pgm_read_byte_near(&console_font_7x9[(chr * h) + i]); break; // 7x9 font + case 60: bits = pgm_read_byte_near(&console_font_5x12[(chr * h) + i]); break; // 5x12 font + default: return; + } + col = ColorFromPalette(grad, (i+1)*255/h, 255, NOBLEND); + for (int j = 0; j= 0 || x0 < cols) && ((bits>>(j+(8-w))) & 0x01)) { // bit set & drawing on-screen + 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 399df185..74d61cb4 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -23,752 +23,228 @@ Modified heavily for WLED */ - +#include "wled.h" #include "FX.h" #include "palettes.h" -//enable custom per-LED mapping. This can allow for better effects on matrices or special displays -//#define WLED_CUSTOM_LED_MAPPING +/* + Custom per-LED mapping has moved! -#ifdef WLED_CUSTOM_LED_MAPPING -//this is just an example (30 LEDs). It will first set all even, then all uneven LEDs. -const uint16_t customMappingTable[] = { + Create a file "ledmap.json" using the edit page. + + this is just an example (30 LEDs). It will first set all even, then all uneven LEDs. + {"map":[ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, - 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29}; + 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]} -//another example. Switches direction every 5 LEDs. -/*const uint16_t customMappingTable[] = { + another example. Switches direction every 5 LEDs. + {"map":[ 0, 1, 2, 3, 4, 9, 8, 7, 6, 5, 10, 11, 12, 13, 14, - 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25};*/ + 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25]} +*/ -const uint16_t customMappingSize = sizeof(customMappingTable)/sizeof(uint16_t); //30 in example +//factory defaults LED setup +//#define PIXEL_COUNTS 30, 30, 30, 30 +//#define DATA_PINS 16, 1, 3, 4 +//#define DEFAULT_LED_TYPE TYPE_WS2812_RGB + +#ifndef PIXEL_COUNTS + #define PIXEL_COUNTS DEFAULT_LED_COUNT #endif -void WS2812FX::init(bool supportWhite, uint16_t countPixels, bool skipFirst) -{ - if (supportWhite == _useRgbw && countPixels == _length && _skipFirstMode == skipFirst) return; - RESET_RUNTIME; - _useRgbw = supportWhite; - _length = countPixels; - _skipFirstMode = skipFirst; +#ifndef DATA_PINS + #define DATA_PINS LEDPIN +#endif - uint8_t ty = 1; - if (supportWhite) ty = 2; - _lengthRaw = _length; - if (_skipFirstMode) { - _lengthRaw += LED_SKIP_AMOUNT; +#ifndef DEFAULT_LED_TYPE + #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; + +// copy constructor +Segment::Segment(const Segment &orig) { + //DEBUG_PRINTLN(F("-- Copy segment constructor --")); + memcpy((void*)this, (void*)&orig, sizeof(Segment)); + transitional = false; // copied segment cannot be in transition + name = nullptr; + data = nullptr; + _dataLen = 0; + _t = nullptr; + 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); } + //if (orig._t) { _t = new Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } +} + +// move constructor +Segment::Segment(Segment &&orig) noexcept { + //DEBUG_PRINTLN(F("-- Move segment constructor --")); + memcpy((void*)this, (void*)&orig, sizeof(Segment)); + orig.transitional = false; // old segment cannot be in transition any more + orig.name = nullptr; + orig.data = nullptr; + orig._dataLen = 0; + orig._t = nullptr; +} + +// copy assignment +Segment& Segment::operator= (const Segment &orig) { + //DEBUG_PRINTLN(F("-- Copying segment --")); + if (this != &orig) { + // clean destination + transitional = false; // copied segment cannot be in transition + if (name) delete[] name; + if (_t) delete _t; + deallocateData(); + // copy source + memcpy((void*)this, (void*)&orig, sizeof(Segment)); + transitional = false; + // erase pointers to allocated data + name = nullptr; + data = nullptr; + _dataLen = 0; + _t = nullptr; + // 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); } + //if (orig._t) { _t = new Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } } + return *this; +} - bus->Begin((NeoPixelType)ty, _lengthRaw); +// move assignment +Segment& Segment::operator= (Segment &&orig) noexcept { + //DEBUG_PRINTLN(F("-- Moving segment --")); + if (this != &orig) { + transitional = false; // just temporary + if (name) { delete[] name; name = nullptr; } // free old name + deallocateData(); // free old runtime data + if (_t) { delete _t; _t = nullptr; } + memcpy((void*)this, (void*)&orig, sizeof(Segment)); + orig.transitional = false; // old segment cannot be in transition + orig.name = nullptr; + orig.data = nullptr; + orig._dataLen = 0; + orig._t = nullptr; + } + return *this; +} + +bool Segment::allocateData(size_t len) { + if (data && _dataLen == len) return true; //already allocated + deallocateData(); + if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) return false; //not enough memory + // do not use SPI RAM on ESP32 since it is slow + data = (byte*) malloc(len); + if (!data) return false; //allocation failed + Segment::addUsedSegmentData(len); + _dataLen = len; + memset(data, 0, len); + return true; +} + +void Segment::deallocateData() { + if (!data) return; + free(data); + data = nullptr; + Segment::addUsedSegmentData(-_dataLen); + _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; - _segments[0].start = 0; - _segments[0].stop = _length; - - setBrightness(_brightness); + deallocateData(); + next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; + reset = false; } -void WS2812FX::service() { - uint32_t 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; - if (SEGMENT.isActive()) - { - if(nowUp > SEGENV.next_time || _triggered || (doShow && SEGMENT.mode == 0)) //last is temporary - { - if (SEGMENT.grouping == 0) SEGMENT.grouping = 1; //sanity check - _virtualSegmentLength = SEGMENT.virtualLength(); - doShow = true; - handle_palette(); - uint16_t delay = (this->*_mode[SEGMENT.mode])(); - SEGENV.next_time = nowUp + delay; - if (SEGMENT.mode != FX_MODE_HALLOWEEN_EYES) SEGENV.call++; - } - } +CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { + static unsigned long _lastPaletteChange = 0; // perhaps it should be per segment + static CRGBPalette16 randomPalette = CRGBPalette16(DEFAULT_COLOR); + static CRGBPalette16 prevRandomPalette = CRGBPalette16(CRGB(BLACK)); + byte tcp[72]; + 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; + //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 } - _virtualSegmentLength = 0; - if(doShow) { - yield(); - show(); - } - _triggered = false; -} - -void WS2812FX::setPixelColor(uint16_t n, uint32_t c) { - uint8_t w = (c >> 24); - uint8_t r = (c >> 16); - uint8_t g = (c >> 8); - uint8_t b = c ; - setPixelColor(n, r, g, b, w); -} - -uint16_t WS2812FX::realPixelIndex(uint16_t i) { - int16_t iGroup = i * SEGMENT.groupLength(); - - /* reverse just an individual segment */ - int16_t realIndex = iGroup; - if (IS_REVERSE) realIndex = SEGMENT.length() -iGroup -1; - - realIndex += SEGMENT.start; - /* Reverse the whole string */ - if (reverseMode) realIndex = _length - 1 - realIndex; - - return realIndex; -} - -void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) -{ - //auto calculate white channel value if enabled - if (_useRgbw) { - if (rgbwMode == RGBW_MODE_AUTO_BRIGHTER || (w == 0 && (rgbwMode == RGBW_MODE_DUAL || rgbwMode == RGBW_MODE_LEGACY))) - { - //white value is set to lowest RGB channel - //thank you to @Def3nder! - w = r < g ? (r < b ? r : b) : (g < b ? g : b); - } else if (rgbwMode == RGBW_MODE_AUTO_ACCURATE && w == 0) - { - w = r < g ? (r < b ? r : b) : (g < b ? g : b); - r -= w; g -= w; b -= w; - } - } - - //reorder channels to selected order - RgbwColor col; - switch (colorOrder) - { - case 0: 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 - } - col.W = w; - - uint16_t skip = _skipFirstMode ? LED_SKIP_AMOUNT : 0; - if (SEGLEN) {//from segment - - //color_blend(getpixel, col, SEGMENT.opacity); (pseudocode for future blending of segments) - if (IS_SEGMENT_ON) - { - if (SEGMENT.opacity < 255) { - col.R = scale8(col.R, SEGMENT.opacity); - col.G = scale8(col.G, SEGMENT.opacity); - col.B = scale8(col.B, SEGMENT.opacity); - col.W = scale8(col.W, SEGMENT.opacity); - } - } else { - col = BLACK; - } - - /* Set all the pixels in the group, ensuring _skipFirstMode is honored */ - bool reversed = reverseMode ^ IS_REVERSE; - uint16_t realIndex = realPixelIndex(i); - - for (uint16_t j = 0; j < SEGMENT.grouping; j++) { - int16_t indexSet = realIndex + (reversed ? -j : j); - int16_t indexSetRev = indexSet; - if (reverseMode) indexSetRev = _length - 1 - indexSet; - #ifdef WLED_CUSTOM_LED_MAPPING - if (indexSet < customMappingSize) indexSet = customMappingTable[indexSet]; - #endif - if (indexSetRev >= SEGMENT.start && indexSetRev < SEGMENT.stop) bus->SetPixelColor(indexSet + skip, col); - } - } else { //live data, etc. - if (reverseMode) i = _length - 1 - i; - #ifdef WLED_CUSTOM_LED_MAPPING - if (i < customMappingSize) i = customMappingTable[i]; - #endif - bus->SetPixelColor(i + skip, col); - } - if (skip && i == 0) { - for (uint16_t j = 0; j < skip; j++) { - bus->SetPixelColor(j, RgbwColor(0, 0, 0, 0)); - } - } -} - - -//DISCLAIMER -//The following function attemps to calculate the current LED power usage, -//and will limit the brightness to stay below a set amperage threshold. -//It is NOT a measurement and NOT guaranteed to stay within the ablMilliampsMax margin. -//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 -#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::show(void) { - if (_callback) _callback(); - - //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 - //so A=2,R=255,G=0,B=0 would use 510 PU per LED (1mA is about 3700 PU) - bool useWackyWS2815PowerModel = false; - byte actualMilliampsPerLed = milliampsPerLed; - - if(milliampsPerLed == 255) { - useWackyWS2815PowerModel = true; - actualMilliampsPerLed = 12; // from testing an actual strip - } - - if (ablMilliampsMax > 149 && actualMilliampsPerLed > 0) //0 mA per LED and too low numbers turn off calculation - { - uint32_t puPerMilliamp = 195075 / actualMilliampsPerLed; - uint32_t powerBudget = (ablMilliampsMax - MA_FOR_ESP) * puPerMilliamp; //100mA for ESP power - if (powerBudget > puPerMilliamp * _length) //each LED uses about 1mA in standby, exclude that from power budget - { - powerBudget -= puPerMilliamp * _length; - } else - { - powerBudget = 0; - } - - uint32_t powerSum = 0; - - for (uint16_t i = 0; i < _length; i++) //sum up the usage of each LED - { - RgbwColor c = bus->GetPixelColorRgbw(i); - - if(useWackyWS2815PowerModel) - { - // ignore white component on WS2815 power calculation - powerSum += (MAX(MAX(c.R,c.G),c.B)) * 3; - } - else - { - powerSum += (c.R + c.G + c.B + c.W); - } - } - - - if (_useRgbw) //RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less - { - powerSum *= 3; - powerSum = powerSum >> 2; //same as /= 4 - } - - uint32_t powerSum0 = powerSum; - powerSum *= _brightness; - - if (powerSum > powerBudget) //scale brightness down to stay in current limit - { - float scale = (float)powerBudget / (float)powerSum; - uint16_t scaleI = scale * 255; - uint8_t scaleB = (scaleI > 255) ? 255 : scaleI; - uint8_t newBri = scale8(_brightness, scaleB); - bus->SetBrightness(newBri); - currentMilliamps = (powerSum0 * newBri) / puPerMilliamp; - } else - { - currentMilliamps = powerSum / puPerMilliamp; - bus->SetBrightness(_brightness); - } - currentMilliamps += MA_FOR_ESP; //add power of ESP back to estimate - currentMilliamps += _length; //add standby power back to estimate - } else { - currentMilliamps = 0; - bus->SetBrightness(_brightness); - } - - bus->Show(); - _lastShow = millis(); -} - -void WS2812FX::trigger() { - _triggered = true; -} - -void WS2812FX::setMode(uint8_t segid, uint8_t m) { - if (segid >= MAX_NUM_SEGMENTS) return; - - if (m >= MODE_COUNT) m = MODE_COUNT - 1; - - if (_segments[segid].mode != m) - { - _segment_runtimes[segid].reset(); - _segments[segid].mode = m; - } -} - -uint8_t WS2812FX::getModeCount() -{ - return MODE_COUNT; -} - -uint8_t WS2812FX::getPaletteCount() -{ - return 13 + GRADIENT_PALETTE_COUNT; -} - -//TODO transitions - - -bool WS2812FX::setEffectConfig(uint8_t m, uint8_t s, uint8_t in, uint8_t p) { - uint8_t mainSeg = getMainSegmentId(); - Segment& seg = _segments[getMainSegmentId()]; - uint8_t modePrev = seg.mode, speedPrev = seg.speed, intensityPrev = seg.intensity, palettePrev = seg.palette; - - bool applied = false; - - if (applyToAllSelected) { - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].isSelected()) - { - _segments[i].speed = s; - _segments[i].intensity = in; - _segments[i].palette = p; - setMode(i, m); - applied = true; - } - } - } - - if (!applyToAllSelected || !applied) { - seg.speed = s; - seg.intensity = in; - seg.palette = p; - setMode(mainSegment, m); - } - - if (seg.mode != modePrev || seg.speed != speedPrev || seg.intensity != intensityPrev || seg.palette != palettePrev) return true; - return false; -} - -void WS2812FX::setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w) { - setColor(slot, ((uint32_t)w << 24) |((uint32_t)r << 16) | ((uint32_t)g << 8) | b); -} - -void WS2812FX::setColor(uint8_t slot, uint32_t c) { - if (slot >= NUM_COLORS) return; - - bool applied = false; - - if (applyToAllSelected) { - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].isSelected()) _segments[i].colors[slot] = c; - } - } - - if (!applyToAllSelected || !applied) { - _segments[getMainSegmentId()].colors[slot] = c; - } -} - -void WS2812FX::setBrightness(uint8_t b) { - if (_brightness == b) return; - _brightness = (gammaCorrectBri) ? gamma8(b) : b; - _segment_index = 0; - if (SEGENV.next_time > millis() + 22 && millis() - _lastShow > MIN_SHOW_DELAY) show();//apply brightness change immediately if no refresh soon -} - -uint8_t WS2812FX::getMode(void) { - return _segments[getMainSegmentId()].mode; -} - -uint8_t WS2812FX::getSpeed(void) { - return _segments[getMainSegmentId()].speed; -} - -uint8_t WS2812FX::getBrightness(void) { - return _brightness; -} - -uint8_t WS2812FX::getMaxSegments(void) { - return MAX_NUM_SEGMENTS; -} - -/*uint8_t WS2812FX::getFirstSelectedSegment(void) -{ - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].isActive() && _segments[i].isSelected()) return i; - } - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) //if none selected, get first active - { - if (_segments[i].isActive()) return i; - } - return 0; -}*/ - -uint8_t WS2812FX::getMainSegmentId(void) { - if (mainSegment >= MAX_NUM_SEGMENTS) return 0; - if (_segments[mainSegment].isActive()) return mainSegment; - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) //get first active - { - if (_segments[i].isActive()) return i; - } - return 0; -} - -uint32_t WS2812FX::getColor(void) { - return _segments[getMainSegmentId()].colors[0]; -} - -uint32_t WS2812FX::getPixelColor(uint16_t i) -{ - i = realPixelIndex(i); - - #ifdef WLED_CUSTOM_LED_MAPPING - if (i < customMappingSize) i = customMappingTable[i]; - #endif - - if (_skipFirstMode) i += LED_SKIP_AMOUNT; - - if (i >= _lengthRaw) return 0; - - RgbwColor col = bus->GetPixelColorRgbw(i); - switch (colorOrder) - { - // W G R B - case 0: 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 - case 4: return ((col.W << 24) | (col.R << 8) | (col.B << 16) | (col.G)); //4 = BGR - case 5: return ((col.W << 24) | (col.G << 8) | (col.B << 16) | (col.R)); //5 = GBR - } - return 0; -} - -WS2812FX::Segment& WS2812FX::getSegment(uint8_t id) { - if (id >= MAX_NUM_SEGMENTS) return _segments[0]; - return _segments[id]; -} - -WS2812FX::Segment_runtime WS2812FX::getSegmentRuntime(void) { - return SEGENV; -} - -WS2812FX::Segment* WS2812FX::getSegments(void) { - return _segments; -} - -uint32_t WS2812FX::getLastShow(void) { - return _lastShow; -} - -void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing) { - if (n >= MAX_NUM_SEGMENTS) return; - Segment& seg = _segments[n]; - - //return if neither bounds nor grouping have changed - if (seg.start == i1 && seg.stop == i2 && (!grouping || (seg.grouping == grouping && seg.spacing == spacing))) 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 (n == mainSegment) //if main segment is deleted, set first active as main segment - { - for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) - { - if (_segments[i].isActive()) { - mainSegment = i; - return; - } - } - mainSegment = 0; //should not happen (always at least one active segment) - } - return; - } - if (i1 < _length) seg.start = i1; - seg.stop = i2; - if (i2 > _length) seg.stop = _length; - if (grouping) { - seg.grouping = grouping; - seg.spacing = spacing; - } - _segment_runtimes[n].reset(); -} - -void WS2812FX::resetSegments() { - 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].stop = _length; - _segments[0].grouping = 1; - _segments[0].setOption(SEG_OPTION_SELECTED, 1); - _segments[0].setOption(SEG_OPTION_ON, 1); - _segments[0].opacity = 255; - - 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; - _segment_runtimes[i].reset(); - } - _segment_runtimes[0].reset(); -} - -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::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++) - { - _segment_index = i; - SEGMENT.setOption(SEG_OPTION_TRANSITIONAL, t); - - if (t && SEGMENT.mode == FX_MODE_STATIC && SEGENV.next_time > waitMax) SEGENV.next_time = waitMax; - } -} - -/* - * color blend function - */ -uint32_t WS2812FX::color_blend(uint32_t color1, uint32_t color2, uint8_t blend) { - if(blend == 0) return color1; - if(blend == 255) return color2; - - uint32_t w1 = (color1 >> 24) & 0xff; - uint32_t r1 = (color1 >> 16) & 0xff; - uint32_t g1 = (color1 >> 8) & 0xff; - uint32_t b1 = color1 & 0xff; - - uint32_t w2 = (color2 >> 24) & 0xff; - uint32_t r2 = (color2 >> 16) & 0xff; - uint32_t g2 = (color2 >> 8) & 0xff; - uint32_t b2 = color2 & 0xff; - - uint32_t w3 = ((w2 * blend) + (w1 * (255 - blend))) >> 8; - uint32_t r3 = ((r2 * blend) + (r1 * (255 - blend))) >> 8; - uint32_t g3 = ((g2 * blend) + (g1 * (255 - blend))) >> 8; - uint32_t b3 = ((b2 * blend) + (b1 * (255 - blend))) >> 8; - - return ((w3 << 24) | (r3 << 16) | (g3 << 8) | (b3)); -} - -/* - * Fills segment with color - */ -void WS2812FX::fill(uint32_t c) { - for(uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, c); - } -} - -/* - * 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 = (color >> 24) & 0xff; - int r2 = (color >> 16) & 0xff; - int g2 = (color >> 8) & 0xff; - int b2 = color & 0xff; - - for(uint16_t i = 0; i < SEGLEN; i++) { - color = getPixelColor(i); - int w1 = (color >> 24) & 0xff; - int r1 = (color >> 16) & 0xff; - int g1 = (color >> 8) & 0xff; - int b1 = color & 0xff; - - 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 = (c >> 16 & 0xFF); - uint8_t g = (c >> 8 & 0xFF); - uint8_t b = (c & 0xFF); - 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 WS2812FX::triwave16(uint16_t in) -{ - if (in < 0x8000) return in *2; - return 0xFFFF - (in - 0x8000)*2; -} - -/* - * 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; -} - - -uint32_t WS2812FX::crgb_to_col(CRGB fastled) -{ - return (((uint32_t)fastled.red << 16) | ((uint32_t)fastled.green << 8) | fastled.blue); -} - - -CRGB WS2812FX::col_to_crgb(uint32_t color) -{ - CRGB fastled_col; - fastled_col.red = (color >> 16 & 0xFF); - fastled_col.green = (color >> 8 & 0xFF); - fastled_col.blue = (color & 0xFF); - return fastled_col; -} - - -void WS2812FX::load_gradient_palette(uint8_t index) -{ - byte i = constrain(index, 0, GRADIENT_PALETTE_COUNT -1); - 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) - { + switch (pal) { 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))); + case 1: {//periodically replace palette with a random one. Transition palette change in 500ms + uint32_t timeSinceLastChange = millis() - _lastPaletteChange; + if (timeSinceLastChange > randomPaletteChangeTime * 1000U) { + prevRandomPalette = randomPalette; + randomPalette = 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(); - } break;} + timeSinceLastChange = 0; + } + if (timeSinceLastChange <= 250) { + targetPalette = prevRandomPalette; + // there needs to be 255 palette blends (48) for full blend but that is too resource intensive + // so 128 is a compromise (we need to perform full blend of the two palettes as each segment can have random + // palette selected but only 2 static palettes are used) + size_t noOfBlends = ((128U * timeSinceLastChange) / 250U); + for (size_t i=0; i245) { + targetPalette = strip.customPalettes[255-pal]; // we checked bounds above + } else { + memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72); + targetPalette.loadDynamicGradientPalette(tcp); + } + break; } - - if (singleSegmentMode && paletteFade) //only blend if just one segment uses FastLED mode - { - nblendPaletteTowardPalette(currentPalette, targetPalette, 48); - } else - { - currentPalette = targetPalette; + return targetPalette; +} + +void Segment::startTransition(uint16_t dur) { + if (!dur) { + transitional = false; + if (_t) { + delete _t; + _t = nullptr; + } + return; + } + if (transitional && _t) 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 + + CRGBPalette16 _palT = CRGBPalette16(DEFAULT_COLOR); loadPalette(_palT, palette); + _t->_briT = on ? opacity : 0; + _t->_cctT = cct; + _t->_palT = _palT; + _t->_modeP = mode; + for (size_t i=0; i_colorT[i] = colors[i]; + transitional = true; // setOption(SEG_OPTION_TRANSITIONAL, true); +} + +// transition progression between 0-65535 +uint16_t Segment::progress() { + if (!transitional || !_t) return 0xFFFFU; + unsigned long timeNow = millis(); + if (timeNow - _t->_start > _t->_dur || _t->_dur == 0) return 0xFFFFU; + return (timeNow - _t->_start) * 0xFFFFU / _t->_dur; +} + +uint8_t Segment::currentBri(uint8_t briNew, bool useCct) { + uint32_t prog = progress(); + if (transitional && _t && prog < 0xFFFFU) { + if (useCct) return ((briNew * prog) + _t->_cctT * (0xFFFFU - prog)) >> 16; + else return ((briNew * prog) + _t->_briT * (0xFFFFU - prog)) >> 16; + } else { + return briNew; } } -uint32_t WS2812FX::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) -{ - if (SEGMENT.palette == 0 && mcol < 3) return SEGCOLOR(mcol); //WS2812FX default - uint8_t paletteIndex = i; - if (mapping) 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 fastled_col.r*65536 + fastled_col.g*256 + fastled_col.b; +uint8_t Segment::currentMode(uint8_t newMode) { + return (progress()>32767U) ? newMode : _t->_modeP; // change effect in the middle of transition } -bool WS2812FX::segmentsAreIdentical(Segment* a, Segment* b) -{ - //if (a->start != b->start) return false; - //if (a->stop != b->stop) return false; - for (uint8_t i = 0; i < NUM_COLORS; i++) - { - if (a->colors[i] != b->colors[i]) return false; +uint32_t Segment::currentColor(uint8_t slot, uint32_t colorNew) { + return transitional && _t ? color_blend(_t->_colorT[slot], colorNew, progress(), true) : colorNew; +} + +CRGBPalette16 &Segment::currentPalette(CRGBPalette16 &targetPalette, uint8_t pal) { + loadPalette(targetPalette, pal); + if (transitional && _t && progress() < 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 long timeMS = millis() - _t->_start; + uint16_t noOfBlends = (255U * timeMS / _t->_dur) - _t->_prevPaletteBlends; + for (int i=0; i_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, targetPalette, 48); + targetPalette = _t->_palT; // copy transitioning/temporary palette } - if (a->mode != b->mode) return false; - if (a->speed != b->speed) return false; - if (a->intensity != b->intensity) return false; - if (a->palette != b->palette) return false; - //if (a->getOption(SEG_OPTION_REVERSED) != b->getOption(SEG_OPTION_REVERSED)) return false; + return targetPalette; +} + +void Segment::handleTransition() { + if (!transitional) return; + uint16_t _progress = progress(); + if (_progress == 0xFFFFU) transitional = false; // finish transitioning segment + if (_t) { // thanks to @nXm AKA https://github.com/NMeirer + if (_progress >= 32767U && _t->_modeP != mode) markForReset(); + if (_progress == 0xFFFFU) { + delete _t; + _t = nullptr; + } + } +} + +// 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; + + 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; + + 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; } -#ifdef WLED_USE_ANALOG_LEDS -void WS2812FX::setRgbwPwm(void) { - uint32_t nowUp = millis(); // Be aware, millis() rolls over every 49 days - if (nowUp - _analogLastShow < MIN_SHOW_DELAY) return; - - _analogLastShow = nowUp; - - RgbwColor color = bus->GetPixelColorRgbw(0); - byte b = getBrightness(); - if (color == _analogLastColor && b == _analogLastBri) return; - - // check color values for Warm / Cold white mix (for RGBW) // EsplanexaDevice.cpp - #ifdef WLED_USE_5CH_LEDS - if (color.R == 255 && color.G == 255 && color.B == 255 && color.W == 255) { - bus->SetRgbwPwm(0, 0, 0, 0, color.W * b / 255); - } else if (color.R == 127 && color.G == 127 && color.B == 127 && color.W == 255) { - bus->SetRgbwPwm(0, 0, 0, color.W * b / 512, color.W * b / 255); - } else if (color.R == 0 && color.G == 0 && color.B == 0 && color.W == 255) { - bus->SetRgbwPwm(0, 0, 0, color.W * b / 255, 0); - } else if (color.R == 130 && color.G == 90 && color.B == 0 && color.W == 255) { - bus->SetRgbwPwm(0, 0, 0, color.W * b / 255, color.W * b / 512); - } else if (color.R == 255 && color.G == 153 && color.B == 0 && color.W == 255) { - bus->SetRgbwPwm(0, 0, 0, color.W * b / 255, 0); - } else { // not only white colors - bus->SetRgbwPwm(color.R * b / 255, color.G * b / 255, color.B * b / 255, color.W * b / 255); - } - #else - bus->SetRgbwPwm(color.R * b / 255, color.G * b / 255, color.B * b / 255, color.W * b / 255); - #endif - _analogLastColor = color; - _analogLastBri = b; +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 } -#else -void WS2812FX::setRgbwPwm() {} + +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 || n == SEG_OPTION_TRANSITIONAL)) 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) { + if (fadeTransition) startTransition(strip.getTransition()); // set effect transitions + 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); + 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); + } + 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 -//gamma 2.4 lookup table used for color correction -const byte gammaT[] = { - 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 }; + uint16_t len = length(); + uint8_t _bri_t = currentBri(on ? opacity : 0); + 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); + } -uint8_t WS2812FX::gamma8(uint8_t b) -{ - return gammaT[b]; + // 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 + + // 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 + strip.setPixelColor(indexMir, col); + } + indexSet += offset; // offset/phase + if (indexSet >= stop) indexSet -= len; // wrap + strip.setPixelColor(indexSet, col); + } + } } -uint32_t WS2812FX::gamma32(uint32_t color) +// anti-aliased normalized version of setPixelColor() +void Segment::setPixelColor(float i, uint32_t col, bool aa) { - if (!gammaCorrectCol) return color; - uint8_t w = (color >> 24); - uint8_t r = (color >> 16); - uint8_t g = (color >> 8); - uint8_t b = color; - w = gammaT[w]; - r = gammaT[r]; - g = gammaT[g]; - b = gammaT[b]; - return ((w << 24) | (r << 16) | (g << 8) | (b)); + 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); + } } -uint16_t WS2812FX::_usedSegmentData = 0; +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:1, mapping:3, transposed, mirrorY, reverseY, [transitional, reset,] paused, mirrored, on, reverse, [selected] + if ((options & 0b1111111110011110U) != (b.options & 0b1111111110011110U)) d |= SEG_DIFFERS_OPT; + if ((options & 0x0001U) != (b.options & 0x0001U)) 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; +} + +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 (uint8_t 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(uint16_t y = 0; y < rows; y++) for (uint16_t 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 + uint32_t col = getPixelColor(n); + uint8_t r = R(col); + uint8_t g = G(col); + uint8_t b = B(col); + uint8_t w = W(col); + if (fast) { + r = qadd8(r, R(color)); + g = qadd8(g, G(color)); + b = qadd8(b, B(color)); + w = qadd8(w, W(color)); + col = RGBW32(r,g,b,w); + } else { + col = color_add(col, color); + } + setPixelColor(n, col); +} + +void Segment::fadePixelColor(uint16_t n, uint8_t fade) { + if (!isActive()) return; // not active + CRGB pix = CRGB(getPixelColor(n)).nscale8_video(fade); + setPixelColor(n, pix); +} + +/* + * 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.1; + + 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 (uint16_t y = 0; y < rows; y++) for (uint16_t 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 (uint16_t y = 0; y < rows; y++) for (uint16_t x = 0; x < cols; x++) { + if (is2D()) setPixelColorXY(x, y, CRGB(getPixelColorXY(x,y)).nscale8(255-fadeBy)); + else setPixelColor(x, CRGB(getPixelColor(x)).nscale8(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 uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); + for (uint_fast16_t i = 0; i < rows; i++) blurRow(i, blur_amount); // blur all rows + for (uint_fast16_t 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; + CRGB carryover = CRGB::Black; + uint_fast16_t vlength = virtualLength(); + for(uint_fast16_t i = 0; i < vlength; i++) + { + CRGB cur = CRGB(getPixelColor(i)); + CRGB part = cur; + CRGB before = cur; // remember color before blur + 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((uint16_t)(i-1), qadd8(r, part.red), qadd8(g, part.green), qadd8(b, part.blue)); + } + if (before != cur) // optimization: only set pixel if color has changed + setPixelColor((uint16_t)i,cur.red, cur.green, cur.blue); + 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); + } +} + +/* + * Returns a new, random wheel index with a minimum distance of 42 from pos. + */ +uint8_t Segment::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; +} + +/* + * 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, colors[mcol]); + color = gamma32(color); + 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)); + } + + uint8_t paletteIndex = i; + if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1); + if (!wrap) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" + CRGB fastled_col; + CRGBPalette16 curPal; + if (transitional && _t) curPal = _t->_palT; + else loadPalette(curPal, palette); + fastled_col = ColorFromPalette(curPal, 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 (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+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, 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->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(); + if (busEnd > _length) _length = busEnd; + #ifdef ESP8266 + if ((!IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType()))) continue; + uint8_t pins[5]; + if (!bus->getPins(pins)) continue; + BusDigital* bd = static_cast(bus); + if (pins[0] == 3) bd->reinit(); + #endif + } + + if (isMatrix) setUpMatrix(); + else { + Segment::maxWidth = _length; + Segment::maxHeight = 1; + } + + //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() { + 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; + + _isServicing = true; + _segment_index = 0; + for (segment &seg : _segments) { + // process transition (mode changes in the middle of transition) + seg.handleTransition(); + // reset the segment runtime data if needed + seg.resetIfRequired(); + + // last condition ensures all solid segments are updated at the same time + if (seg.isActive() && (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC))) + { + doShow = true; + uint16_t delay = FRAMETIME; + + if (!seg.freeze) { //only run effect function if not frozen + _virtualSegmentLength = seg.virtualLength(); + _colors_t[0] = seg.currentColor(0, seg.colors[0]); + _colors_t[1] = seg.currentColor(1, seg.colors[1]); + _colors_t[2] = seg.currentColor(2, seg.colors[2]); + seg.currentPalette(_currentPalette, seg.palette); + + if (!cctFromRgb || correctWB) busses.setSegmentCCT(seg.currentBri(seg.cct, true), correctWB); + for (uint8_t c = 0; c < NUM_COLORS; c++) _colors_t[c] = gamma32(_colors_t[c]); + + // effect blending (execute previous effect) + // actual code may be a bit more involved as effects have runtime data including allocated memory + //if (seg.transitional && seg._modeP) (*_mode[seg._modeP])(progress()); + delay = (*_mode[seg.currentMode(seg.mode)])(); + if (seg.mode != FX_MODE_HALLOWEEN_EYES) seg.call++; + if (seg.transitional && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition + } + + seg.next_time = nowUp + delay; + } + if (_segment_index == _queuedChangesSegId) setUpSegmentFromQueuedChanges(); + _segment_index++; + } + _virtualSegmentLength = 0; + busses.setSegmentCCT(-1); + _isServicing = false; + _triggered = false; + + #ifdef WLED_DEBUG + if (millis() - nowUp > _frametime) DEBUG_PRINTLN(F("Slow effects.")); + #endif + if (doShow) { + yield(); + show(); + } + #ifdef WLED_DEBUG + if (millis() - nowUp > _frametime) DEBUG_PRINTLN(F("Slow strip.")); + #endif +} + +void IRAM_ATTR WS2812FX::setPixelColor(int i, uint32_t col) +{ + if (i < customMappingSize) i = customMappingTable[i]; + if (i >= _length) return; + busses.setPixelColor(i, col); +} + +uint32_t WS2812FX::getPixelColor(uint16_t i) +{ + if (i < customMappingSize) i = customMappingTable[i]; + if (i >= _length) return 0; + return busses.getPixelColor(i); +} + + +//DISCLAIMER +//The following function attemps to calculate the current LED power usage, +//and will limit the brightness to stay below a set amperage threshold. +//It is NOT a measurement and NOT guaranteed to stay within the ablMilliampsMax margin. +//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 +#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 + +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 + //so A=2,R=255,G=0,B=0 would use 510 PU per LED (1mA is about 3700 PU) + bool useWackyWS2815PowerModel = false; + byte actualMilliampsPerLed = milliampsPerLed; + + if (ablMilliampsMax < 150 || actualMilliampsPerLed == 0) { //0 mA per LED and too low numbers turn off calculation + currentMilliamps = 0; + return _brightness; + } + + if (milliampsPerLed == 255) { + useWackyWS2815PowerModel = true; + actualMilliampsPerLed = 12; // from testing an actual strip + } + + size_t powerBudget = (ablMilliampsMax - MA_FOR_ESP); //100mA for ESP power + + 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 (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 + busPowerSum += (MAX(MAX(r,g),b)) * 3; + } else { + busPowerSum += (r + g + b + w); + } + } + + if (bus->hasWhite()) { //RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less + busPowerSum *= 3; + busPowerSum >>= 2; //same as /= 4 + } + powerSum += busPowerSum; + } + + 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; + 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 (1mA/LED) back to estimate + return newBri; +} + +void WS2812FX::show(void) { + // avoid race condition, caputre _callback value + show_callback callback = _callback; + if (callback) callback(); + + #ifdef WLED_DEBUG + static unsigned long sumMicros = 0, sumCurrent = 0; + static size_t calls = 0; + unsigned long microsStart = micros(); + #endif + + uint8_t newBri = estimateCurrentAndLimitBri(); + busses.setBrightness(newBri); // "repaints" all pixels if brightness changed + + #ifdef WLED_DEBUG + sumCurrent += micros() - microsStart; + #endif + + // 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(); + + // 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); + + #ifdef WLED_DEBUG + sumMicros += micros() - microsStart; + if (++calls == 100) { + DEBUG_PRINTF("%d show calls: %lu[us] avg: %lu[us] (current: %lu[us] avg: %lu[us])\n", calls, sumMicros, sumMicros/calls, sumCurrent, sumCurrent/calls); + sumMicros = sumCurrent = 0; + calls = 0; + } + #endif + + unsigned long now = millis(); + size_t diff = now - _lastShow; + size_t fpsCurr = 200; + if (diff > 0) fpsCurr = 1000 / diff; + _cumulativeFps = (3 * _cumulativeFps + fpsCurr +2) >> 2; // "+2" for proper rounding (2/4 = 0.5) + _lastShow = now; +} + +/** + * Returns a true value if any of the strips are still being updated. + * On some hardware (ESP32), strip updates are done asynchronously. + */ +bool WS2812FX::isUpdating() { + return !busses.canAllShow(); +} + +/** + * 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 + */ +uint16_t WS2812FX::getFps() { + if (millis() - _lastShow > 2000) return 0; + return _cumulativeFps +1; +} + +void WS2812FX::setTargetFps(uint8_t fps) { + if (fps > 0 && fps <= 120) _targetFps = fps; + _frametime = 1000 / _targetFps; +} + +void WS2812FX::setMode(uint8_t segid, uint8_t m) { + if (segid >= _segments.size()) return; + + if (m >= getModeCount()) m = getModeCount() - 1; + + if (_segments[segid].mode != m) { + _segments[segid].startTransition(_transitionDur); // set effect transitions + //_segments[segid].markForReset(); + _segments[segid].mode = m; + } +} + +//applies to all active and selected segments +void WS2812FX::setColor(uint8_t slot, uint32_t c) { + if (slot >= NUM_COLORS) return; + + for (segment &seg : _segments) { + if (seg.isActive() && seg.isSelected()) { + seg.setColor(slot, c); + } + } +} + +void WS2812FX::setCCT(uint16_t k) { + 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 (segment &seg : _segments) { + seg.freeze = false; + } + } + // 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::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) +{ + 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) { + _mainSegment = 0; + if (n < _segments.size()) { + _mainSegment = n; + } + return; +} + +uint8_t WS2812FX::getLastActiveSegmentId(void) { + for (size_t i = _segments.size() -1; i > 0; i--) { + if (_segments[i].isActive()) return i; + } + return 0; +} + +uint8_t WS2812FX::getActiveSegmentsNum(void) { + uint8_t c = 0; + for (size_t i = 0; i < _segments.size(); i++) { + if (_segments[i].isActive()) c++; + } + return c; +} + +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; +} + +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->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses + len += bus->getLength(); + } + 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 (size_t b = 0; b < busses.getNumBusses(); b++) { + Bus *bus = busses.getBus(b); + if (bus == nullptr || bus->getLength()==0) break; + if (bus->hasRGB() && bus->hasWhite()) return true; + } + return false; +} + +bool WS2812FX::hasCCTBus(void) { + 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()) { + case TYPE_ANALOG_5CH: + case TYPE_ANALOG_2CH: + return true; + } + } + return false; +} + +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 (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 (_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; + return; // queued changes are applied immediately after effect function returns + } + + _segments[segId].setUp(i1, i2, grouping, spacing, offset, startY, stopY); +} + +void WS2812FX::setUpSegmentFromQueuedChanges() { + if (_queuedChangesSegId >= getSegmentsNum()) return; + getSegment(_queuedChangesSegId).setUp(_qStart, _qStop, _qGrouping, _qSpacing, _qOffset, _qStartY, _qStopY); + _queuedChangesSegId = 255; +} + +void WS2812FX::restartRuntime() { + for (segment &seg : _segments) seg.markForReset(); +} + +void WS2812FX::resetSegments() { + _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; +} + +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}; + 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(); + + #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 (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]); + segStops [j] = max(segStops [s],segStops [j]); segStops[s] = 0; + s--; + } + } + s++; + } + + _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 { + + 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 (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() { + bool aligned = false; + for (segment &seg : _segments) { + for (uint8_t b = 0; bgetStart() && seg.stop == bus->getStart() + bus->getLength()) aligned = true; + } + if (seg.start == 0 && seg.stop == _length) aligned = true; + if (!aligned) return false; + } + return true; +} + +//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 prevSegId = _segment_index; + if (n < _segments.size()) { + _segment_index = n; + _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::setTransitionMode(bool t) { + for (segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); +} + +#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::loadCustomPalettes() { + byte tcp[72]; //support gradient palettes with up to 18 entries + 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()>4) { // 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] = 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] = (uint8_t) pal[i+1].as(); // R + tcp[i+2] = (uint8_t) pal[i+2].as(); // G + tcp[i+3] = (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 { + break; + } + } +} + +//load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) +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"); + bool isFile = WLED_FS.exists(fileName); + + if (!isFile) { + // erase custom mapping if selecting nonexistent ledmap.json (n==0) + if (!isMatrix && !n && customMappingTable != nullptr) { + customMappingSize = 0; + delete[] customMappingTable; + customMappingTable = nullptr; + } + return false; + } + + if (!requestJSONBufferLock(7)) return false; + + if (!readObjectFromFile(fileName, nullptr, &doc)) { + releaseJSONBufferLock(); + 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; + delete[] customMappingTable; + customMappingTable = nullptr; + } + + JsonArray map = doc[F("map")]; + if (!map.isNull() && map.size()) { // not an empty map + customMappingSize = map.size(); + customMappingTable = new uint16_t[customMappingSize]; + for (uint16_t i=0; i +#include + +#define NODE_TYPE_ID_UNDEFINED 0 +#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 +\*********************************************************************************************/ +struct NodeStruct +{ + String nodeName; + IPAddress ip; + uint8_t age; + 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) + { + for (uint8_t i = 0; i < 4; ++i) { ip[i] = 0; } + } +}; +typedef std::map NodesMap; + +#endif // WLED_NODESTRUCT_H diff --git a/wled00/NpbWrapper.h b/wled00/NpbWrapper.h deleted file mode 100644 index b4bfe9b5..00000000 --- a/wled00/NpbWrapper.h +++ /dev/null @@ -1,344 +0,0 @@ -//this code is a modified version of https://github.com/Makuna/NeoPixelBus/issues/103 -#ifndef NpbWrapper_h -#define NpbWrapper_h - -//PIN CONFIGURATION -#ifndef LEDPIN -#define LEDPIN 2 //strip pin. Any for ESP32, gpio2 or 3 is recommended for ESP8266 (gpio2/3 are labeled D4/RX on NodeMCU and Wemos) -#endif -//#define USE_APA102 // Uncomment for using APA102 LEDs. -//#define USE_WS2801 // Uncomment for using WS2801 LEDs (make sure you have NeoPixelBus v2.5.6 or newer) -//#define USE_LPD8806 // Uncomment for using LPD8806 -//#define USE_TM1814 // Uncomment for using TM1814 LEDs (make sure you have NeoPixelBus v2.5.7 or newer) -//#define USE_P9813 // Uncomment for using P9813 LEDs (make sure you have NeoPixelBus v2.5.8 or newer) -//#define WLED_USE_ANALOG_LEDS //Uncomment for using "dumb" PWM controlled LEDs (see pins below, default R: gpio5, G: 12, B: 15, W: 13) -//#define WLED_USE_H801 //H801 controller. Please uncomment #define WLED_USE_ANALOG_LEDS as well -//#define WLED_USE_5CH_LEDS //5 Channel H801 for cold and warm white -//#define WLED_USE_BWLT11 -//#define WLED_USE_SHOJO_PCB - -#ifndef BTNPIN -#define BTNPIN 0 //button pin. Needs to have pullup (gpio0 recommended) -#endif - -#ifndef IR_PIN -#define IR_PIN 4 //infrared pin (-1 to disable) MagicHome: 4, H801 Wifi: 0 -#endif - -#ifndef RLYPIN -#define RLYPIN 12 //pin for relay, will be set HIGH if LEDs are on (-1 to disable). Also usable for standby leds, triggers,... -#endif - -#ifndef AUXPIN -#define AUXPIN -1 //debug auxiliary output pin (-1 to disable) -#endif - -#ifndef RLYMDE -#define RLYMDE 1 //mode for relay, 0: LOW if LEDs are on 1: HIGH if LEDs are on -#endif - -//END CONFIGURATION - -#if defined(USE_APA102) || defined(USE_WS2801) || defined(USE_LPD8806) || defined(USE_P9813) - #define CLKPIN 0 - #define DATAPIN 2 - #if BTNPIN == CLKPIN || BTNPIN == DATAPIN - #undef BTNPIN // Deactivate button pin if it conflicts with one of the APA102 pins. - #endif -#endif - -#ifdef WLED_USE_ANALOG_LEDS - //PWM pins - PINs 15,13,12,14 (W2 = 04)are used with H801 Wifi LED Controller - #ifdef WLED_USE_H801 - #define RPIN 15 //R pin for analog LED strip - #define GPIN 13 //G pin for analog LED strip - #define BPIN 12 //B pin for analog LED strip - #define WPIN 14 //W pin for analog LED strip - #define W2PIN 04 //W2 pin for analog LED strip - #undef BTNPIN - #undef IR_PIN - #define IR_PIN 0 //infrared pin (-1 to disable) MagicHome: 4, H801 Wifi: 0 - #elif defined(WLED_USE_BWLT11) - //PWM pins - to use with BW-LT11 - #define RPIN 12 //R pin for analog LED strip - #define GPIN 4 //G pin for analog LED strip - #define BPIN 14 //B pin for analog LED strip - #define WPIN 5 //W pin for analog LED strip - #elif defined(WLED_USE_SHOJO_PCB) - //PWM pins - to use with Shojo PCB (https://www.bastelbunker.de/esp-rgbww-wifi-led-controller-vbs-edition/) - #define RPIN 14 //R pin for analog LED strip - #define GPIN 4 //G pin for analog LED strip - #define BPIN 5 //B pin for analog LED strip - #define WPIN 15 //W pin for analog LED strip - #define W2PIN 12 //W2 pin for analog LED strip - #elif defined(WLED_USE_PLJAKOBS_PCB) - // PWM pins - to use with esp_rgbww_controller from patrickjahns/pljakobs (https://github.com/pljakobs/esp_rgbww_controller) - #define RPIN 12 //R pin for analog LED strip - #define GPIN 13 //G pin for analog LED strip - #define BPIN 14 //B pin for analog LED strip - #define WPIN 4 //W pin for analog LED strip - #define W2PIN 5 //W2 pin for analog LED strip - #undef IR_PIN - #else - //PWM pins - PINs 5,12,13,15 are used with Magic Home LED Controller - #define RPIN 5 //R pin for analog LED strip - #define GPIN 12 //G pin for analog LED strip - #define BPIN 15 //B pin for analog LED strip - #define WPIN 13 //W pin for analog LED strip - #endif - #undef RLYPIN - #define RLYPIN -1 //disable as pin 12 is used by analog LEDs -#endif - -//automatically uses the right driver method for each platform -#ifdef ARDUINO_ARCH_ESP32 - #ifdef USE_APA102 - #define PIXELMETHOD DotStarMethod - #elif defined(USE_WS2801) - #define PIXELMETHOD NeoWs2801Method - #elif defined(USE_LPD8806) - #define PIXELMETHOD Lpd8806Method - #elif defined(USE_TM1814) - #define PIXELMETHOD NeoTm1814Method - #elif defined(USE_P9813) - #define PIXELMETHOD P9813Method - #else - #define PIXELMETHOD NeoEsp32Rmt0Ws2812xMethod - #endif -#else //esp8266 - //autoselect the right method depending on strip pin - #ifdef USE_APA102 - #define PIXELMETHOD DotStarMethod - #elif defined(USE_WS2801) - #define PIXELMETHOD NeoWs2801Method - #elif defined(USE_LPD8806) - #define PIXELMETHOD Lpd8806Method - #elif defined(USE_TM1814) - #define PIXELMETHOD NeoTm1814Method - #elif defined(USE_P9813) - #define PIXELMETHOD P9813Method - #elif LEDPIN == 2 - #define PIXELMETHOD NeoEsp8266Uart1Ws2813Method //if you get an error here, try to change to NeoEsp8266UartWs2813Method or update Neopixelbus - #elif LEDPIN == 3 - #define PIXELMETHOD NeoEsp8266Dma800KbpsMethod - #else - #define PIXELMETHOD NeoEsp8266BitBang800KbpsMethod - #pragma message "Software BitBang will be used because of your selected LED pin. This may cause flicker. Use GPIO 2 or 3 for best results." - #endif -#endif - - -//you can now change the color order in the web settings -#ifdef USE_APA102 - #define PIXELFEATURE3 DotStarBgrFeature - #define PIXELFEATURE4 DotStarLbgrFeature -#elif defined(USE_LPD8806) - #define PIXELFEATURE3 Lpd8806GrbFeature -#elif defined(USE_WS2801) - #define PIXELFEATURE3 NeoRbgFeature - #define PIXELFEATURE4 NeoRbgFeature -#elif defined(USE_TM1814) - #define PIXELFEATURE3 NeoWrgbTm1814Feature - #define PIXELFEATURE4 NeoWrgbTm1814Feature -#elif defined(USE_P9813) - #define PIXELFEATURE3 P9813BgrFeature - #define PIXELFEATURE4 NeoGrbwFeature -#else - #define PIXELFEATURE3 NeoGrbFeature - #define PIXELFEATURE4 NeoGrbwFeature -#endif - - -#include - -enum NeoPixelType -{ - NeoPixelType_None = 0, - NeoPixelType_Grb = 1, - NeoPixelType_Grbw = 2, - NeoPixelType_End = 3 -}; - -class NeoPixelWrapper -{ -public: - NeoPixelWrapper() : - // initialize each member to null - _pGrb(NULL), - _pGrbw(NULL), - _type(NeoPixelType_None) - { - - } - - ~NeoPixelWrapper() - { - cleanup(); - } - - void Begin(NeoPixelType type, uint16_t countPixels) - { - cleanup(); - _type = type; - - switch (_type) - { - case NeoPixelType_Grb: - #if defined(USE_APA102) || defined(USE_WS2801) || defined(USE_LPD8806) || defined(USE_P9813) - _pGrb = new NeoPixelBrightnessBus(countPixels, CLKPIN, DATAPIN); - #else - _pGrb = new NeoPixelBrightnessBus(countPixels, LEDPIN); - #endif - _pGrb->Begin(); - break; - - case NeoPixelType_Grbw: - #if defined(USE_APA102) || defined(USE_WS2801) || defined(USE_LPD8806) || defined(USE_P9813) - _pGrbw = new NeoPixelBrightnessBus(countPixels, CLKPIN, DATAPIN); - #else - _pGrbw = new NeoPixelBrightnessBus(countPixels, LEDPIN); - #endif - _pGrbw->Begin(); - break; - } - - #ifdef WLED_USE_ANALOG_LEDS - #ifdef ARDUINO_ARCH_ESP32 - ledcSetup(0, 5000, 8); - ledcAttachPin(RPIN, 0); - ledcSetup(1, 5000, 8); - ledcAttachPin(GPIN, 1); - ledcSetup(2, 5000, 8); - ledcAttachPin(BPIN, 2); - if(_type == NeoPixelType_Grbw) - { - ledcSetup(3, 5000, 8); - ledcAttachPin(WPIN, 3); - #ifdef WLED_USE_5CH_LEDS - ledcSetup(4, 5000, 8); - ledcAttachPin(W2PIN, 4); - #endif - } - #else // ESP8266 - //init PWM pins - pinMode(RPIN, OUTPUT); - pinMode(GPIN, OUTPUT); - pinMode(BPIN, OUTPUT); - if(_type == NeoPixelType_Grbw) - { - pinMode(WPIN, OUTPUT); - #ifdef WLED_USE_5CH_LEDS - pinMode(W2PIN, OUTPUT); - #endif - } - analogWriteRange(255); //same range as one RGB channel - analogWriteFreq(880); //PWM frequency proven as good for LEDs - #endif - #endif - } - -#ifdef WLED_USE_ANALOG_LEDS - void SetRgbwPwm(uint8_t r, uint8_t g, uint8_t b, uint8_t w, uint8_t w2=0) - { - #ifdef ARDUINO_ARCH_ESP32 - ledcWrite(0, r); - ledcWrite(1, g); - ledcWrite(2, b); - switch (_type) { - case NeoPixelType_Grb: break; - #ifdef WLED_USE_5CH_LEDS - case NeoPixelType_Grbw: ledcWrite(3, w); ledcWrite(4, w2); break; - #else - case NeoPixelType_Grbw: ledcWrite(3, w); break; - #endif - } - #else // ESP8266 - analogWrite(RPIN, r); - analogWrite(GPIN, g); - analogWrite(BPIN, b); - switch (_type) { - case NeoPixelType_Grb: break; - #ifdef WLED_USE_5CH_LEDS - case NeoPixelType_Grbw: analogWrite(WPIN, w); analogWrite(W2PIN, w2); break; - #else - case NeoPixelType_Grbw: analogWrite(WPIN, w); break; - #endif - } - #endif - } -#endif - - void Show() - { - byte b; - switch (_type) - { - case NeoPixelType_Grb: _pGrb->Show(); break; - case NeoPixelType_Grbw: _pGrbw->Show(); break; - } - } - - void SetPixelColor(uint16_t indexPixel, RgbwColor color) - { - switch (_type) { - case NeoPixelType_Grb: { - _pGrb->SetPixelColor(indexPixel, RgbColor(color.R,color.G,color.B)); - } - break; - case NeoPixelType_Grbw: { - #if defined(USE_LPD8806) || defined(USE_WS2801) - _pGrbw->SetPixelColor(indexPixel, RgbColor(color.R,color.G,color.B)); - #else - _pGrbw->SetPixelColor(indexPixel, color); - #endif - } - break; - } - - } - - void SetBrightness(byte b) - { - switch (_type) { - case NeoPixelType_Grb: _pGrb->SetBrightness(b); break; - case NeoPixelType_Grbw:_pGrbw->SetBrightness(b); break; - } - } - - // NOTE: Due to feature differences, some support RGBW but the method name - // here needs to be unique, thus GetPixeColorRgbw - RgbwColor GetPixelColorRgbw(uint16_t indexPixel) const - { - switch (_type) { - case NeoPixelType_Grb: return _pGrb->GetPixelColor(indexPixel); break; - case NeoPixelType_Grbw: return _pGrbw->GetPixelColor(indexPixel); break; - } - return 0; - } - - uint8_t* GetPixels(void) - { - switch (_type) { - case NeoPixelType_Grb: return _pGrb->Pixels(); break; - case NeoPixelType_Grbw: return _pGrbw->Pixels(); break; - } - return 0; - } - - -private: - NeoPixelType _type; - - // have a member for every possible type - NeoPixelBrightnessBus* _pGrb; - NeoPixelBrightnessBus* _pGrbw; - - void cleanup() - { - switch (_type) { - case NeoPixelType_Grb: delete _pGrb ; _pGrb = NULL; break; - case NeoPixelType_Grbw: delete _pGrbw; _pGrbw = NULL; break; - } - } -}; -#endif diff --git a/wled00/__vm/.wled00.vsarduino.h b/wled00/__vm/.wled00.vsarduino.h deleted file mode 100644 index 2a9ef40a..00000000 --- a/wled00/__vm/.wled00.vsarduino.h +++ /dev/null @@ -1,124 +0,0 @@ -/* - Editor: https://www.visualmicro.com/ - This file is for intellisense purpose only. - Visual micro (and the arduino ide) ignore this code during compilation. This code is automatically maintained by visualmicro, manual changes to this file will be overwritten - The contents of the _vm sub folder can be deleted prior to publishing a project - All non-arduino files created by visual micro and all visual studio project or solution files can be freely deleted and are not required to compile a sketch (do not delete your own code!). - Note: debugger breakpoints are stored in '.sln' or '.asln' files, knowledge of last uploaded breakpoints is stored in the upload.vmps.xml file. Both files are required to continue a previous debug session without needing to compile and upload again - - Hardware: ESP32 Dev Module, Platform=esp32, Package=esp32 -*/ - -#if defined(_VMICRO_INTELLISENSE) - -#ifndef _VSARDUINO_H_ -#define _VSARDUINO_H_ -#define __ESP32_esp32__ -#define __ESP32_ESP32__ -#define ESP_PLATFORM -#define HAVE_CONFIG_H -#define GCC_NOT_5_2_0 0 -#define WITH_POSIX -#define F_CPU 240000000L -#define ARDUINO 108011 -#define ARDUINO_ESP32_DEV -#define ARDUINO_ARCH_ESP32 -#define ESP32 -#define CORE_DEBUG_LEVEL 0 -#define __cplusplus 201103L - -#define _Pragma(x) -#undef __cplusplus -#define __cplusplus 201103L - -#define __STDC__ -#define __ARM__ -#define __arm__ -#define __inline__ -#define __asm__(...) -#define __extension__ -#define __ATTR_PURE__ -#define __ATTR_CONST__ -#define __volatile__ - -#define __ASM -#define __INLINE -#define __attribute__(noinline) - -//#define _STD_BEGIN -//#define EMIT -#define WARNING -#define _Lockit -#define __CLR_OR_THIS_CALL -#define C4005 -#define _NEW - -typedef bool _Bool; -typedef int _read; -typedef int _seek; -typedef int _write; -typedef int _close; -typedef int __cleanup; - -//#define inline - -#define __builtin_clz -#define __builtin_clzl -#define __builtin_clzll -#define __builtin_labs -#define __builtin_va_list -typedef int __gnuc_va_list; - -#define __ATOMIC_ACQ_REL - -#define __CHAR_BIT__ -#define _EXFUN() - -typedef unsigned char byte; -extern "C" void __cxa_pure_virtual() {;} - -typedef long __INTPTR_TYPE__ ; -typedef long __UINTPTR_TYPE__ ; -typedef long __SIZE_TYPE__ ; -typedef long __PTRDIFF_TYPE__; - -typedef long pthread_t; -typedef long pthread_key_t; -typedef long pthread_once_t; -typedef long pthread_mutex_t; -typedef long pthread_mutex_t; -typedef long pthread_cond_t; - - - -#include "arduino.h" -#include - -#define interrupts() sei() -#define noInterrupts() cli() - -#define ESP_LOGI(tag, ...) - -#include "wled00.ino" -#include "wled01_eeprom.ino" -#include "wled02_xml.ino" -#include "wled03_set.ino" -#include "wled04_file.ino" -#include "wled05_init.ino" -#include "wled06_usermod.ino" -#include "wled07_notify.ino" -#include "wled08_led.ino" -#include "wled09_button.ino" -#include "wled10_ntp.ino" -#include "wled11_ol.ino" -#include "wled12_alexa.ino" -#include "wled13_cronixie.ino" -#include "wled14_colors.ino" -#include "wled15_hue.ino" -#include "wled16_blynk.ino" -#include "wled17_mqtt.ino" -#include "wled18_server.ino" -#include "wled19_json.ino" -#include "wled20_ir.ino" -#endif -#endif diff --git a/wled00/__vm/Compile.vmps.xml b/wled00/__vm/Compile.vmps.xml deleted file mode 100644 index 6390398e..00000000 --- a/wled00/__vm/Compile.vmps.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/wled00/__vm/Configuration.Release.vmps.xml b/wled00/__vm/Configuration.Release.vmps.xml deleted file mode 100644 index bcbca636..00000000 --- a/wled00/__vm/Configuration.Release.vmps.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index 05e46093..179a522c 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,18 +43,35 @@ 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; - colorUpdated(NOTIFIER_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 applyMacro(macroAlexaOn); + } 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) { if (!macroAlexaOff) @@ -55,39 +80,58 @@ void onAlexaChange(EspalexaDevice* dev) { briLast = bri; bri = 0; - colorUpdated(NOTIFIER_CALL_MODE_ALEXA); + stateUpdated(CALL_MODE_ALEXA); } - } else applyMacro(macroAlexaOff); + } else + { + applyPreset(macroAlexaOff, CALL_MODE_ALEXA); + // 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(); - colorUpdated(NOTIFIER_CALL_MODE_ALEXA); + 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 { - uint16_t ct = espalexaDevice->getCt(); - if (useRGBW) - { + byte rgbw[4]; + 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: col[0]=255; col[1]=255; col[2]=255; col[3]=255; break; - case 234: col[0]=127; col[1]=127; col[2]=127; col[3]=255; break; - case 284: col[0]= 0; col[1]= 0; col[2]= 0; col[3]=255; break; - case 350: col[0]=130; col[1]= 90; col[2]= 0; col[3]=255; break; - case 383: col[0]=255; col[1]=153; col[2]= 0; col[3]=255; break; + 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); } } else { - colorCTtoRGB(ct, col); + colorKtoRGB(k, rgbw); } + strip.setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); } else { - uint32_t color = espalexaDevice->getRGB(); - - col[0] = ((color >> 16) & 0xFF); - col[1] = ((color >> 8) & 0xFF); - col[2] = ( color & 0xFF); - col[3] = 0; + uint32_t color = dev->getRGB(); + strip.setColor(0, color); } - colorUpdated(NOTIFIER_CALL_MODE_ALEXA); + stateUpdated(CALL_MODE_ALEXA); } } diff --git a/wled00/blynk.cpp b/wled00/blynk.cpp deleted file mode 100644 index 39b43ba8..00000000 --- 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) -{ - #ifndef WLED_DISABLE_BLYNK - if (!WLED_CONNECTED) return; - blynkEnabled = (auth[0] != 0); - if (blynkEnabled) Blynk.config(auth); - #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 - colorUpdated(NOTIFIER_CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V1) -{ - blHue = param.asInt();//hue - colorHStoRGB(blHue*10,blSat,(false)? colSec:col); - colorUpdated(NOTIFIER_CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V2) -{ - blSat = param.asInt();//sat - colorHStoRGB(blHue*10,blSat,(false)? colSec:col); - colorUpdated(NOTIFIER_CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V3) -{ - bool on = (param.asInt()>0); - if (!on != !bri) {toggleOnOff(); colorUpdated(NOTIFIER_CALL_MODE_BLYNK);} -} - -BLYNK_WRITE(V4) -{ - effectCurrent = param.asInt()-1;//fx - colorUpdated(NOTIFIER_CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V5) -{ - effectSpeed = param.asInt();//sx - colorUpdated(NOTIFIER_CALL_MODE_BLYNK); -} - -BLYNK_WRITE(V6) -{ - effectIntensity = param.asInt();//ix - colorUpdated(NOTIFIER_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 00000000..7ea44b15 --- /dev/null +++ b/wled00/bus_manager.cpp @@ -0,0 +1,637 @@ +/* + * 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 containd 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 < 255) 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; + else pix += _skip; + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co); + } + } + 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; + else 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; + else 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 new file mode 100644 index 00000000..4249c880 --- /dev/null +++ b/wled00/bus_manager.h @@ -0,0 +1,344 @@ +#ifndef BusManager_h +#define BusManager_h + +/* + * Class for addressing various light types + */ + +#include "const.h" + +#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)))) + +#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; + 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}; + 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) + 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 (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; + } + + //validates start and length and extends total if needed + bool adjustBounds(uint16_t& total) { + if (!count) count = 1; + if (count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS; + if (start >= MAX_LEDS) return false; + //limit length of strip if it would exceed total permissible LEDs + if (start + count > MAX_LEDS) count = MAX_LEDS - start; + //extend total count accordingly + if (start + count > total) total = start + count; + return true; + } +}; + + +// Defines an LED Strip and its color ordering. +struct ColorOrderMapEntry { + uint16_t start; + uint16_t len; + uint8_t colorOrder; +}; + +struct ColorOrderMap { + 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)); + } + + const ColorOrderMapEntry* get(uint8_t n) const { + if (n > _count) { + return nullptr; + } + return &(_mappings[n]); + } + + uint8_t getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const; + + private: + 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, 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() = 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) { _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 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) 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 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; + 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); + 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); + ~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(); + + 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 slighly improves recovery / stops degradation on re-scale + } + } + return c; + } +}; + + +class BusPwm : public Bus { + public: + BusPwm(BusConfig &bc); + ~BusPwm() { cleanup(); } + + 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(); } + + private: + uint8_t _pins[5]; + uint8_t _pwmdata[5]; + #ifdef ARDUINO_ARCH_ESP32 + uint8_t _ledcStart; + #endif + uint16_t _frequency; + + void deallocatePins(); +}; + + +class BusOnOff : public Bus { + public: + BusOnOff(BusConfig &bc); + ~BusOnOff() { cleanup(); } + + 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); } + + private: + uint8_t _pin; + uint8_t _onoffdata; +}; + + +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 _UDPtype; + uint8_t _UDPchannels; + bool _rgbw; + bool _broadcastLock; +}; + + +class BusManager { + public: + BusManager() : numBusses(0) {}; + + //utility to get the approx. memory usage of a given BusConfig + static uint32_t memUsage(BusConfig &bc); + + int add(BusConfig &bc); + + //do not call this method from system context (network callback) + void removeAll(); + + 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); + + Bus* getBus(uint8_t busNr); + + //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) + uint16_t getTotalLength(); + inline uint8_t getNumBusses() const { return numBusses; } + + inline void updateColorOrderMap(const ColorOrderMap &com) { memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); } + inline const ColorOrderMap& getColorOrderMap() const { return colorOrderMap; } + + private: + 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 new file mode 100644 index 00000000..72b4435e --- /dev/null +++ b/wled00/bus_wrapper.h @@ -0,0 +1,1220 @@ +#ifndef BusWrapper_h +#define BusWrapper_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 +#define P_8266_HS_CLK 14 +#define P_32_HS_MOSI 13 +#define P_32_HS_CLK 14 +#define P_32_VS_MOSI 23 +#define P_32_VS_CLK 18 + +//The dirty list of possible bus types. Quite a lot... +#define I_NONE 0 +//ESP8266 RGB +#define I_8266_U0_NEO_3 1 +#define I_8266_U1_NEO_3 2 +#define I_8266_DM_NEO_3 3 +#define I_8266_BB_NEO_3 4 +//RGBW +#define I_8266_U0_NEO_4 5 +#define I_8266_U1_NEO_4 6 +#define I_8266_DM_NEO_4 7 +#define I_8266_BB_NEO_4 8 +//400Kbps +#define I_8266_U0_400_3 9 +#define I_8266_U1_400_3 10 +#define I_8266_DM_400_3 11 +#define I_8266_BB_400_3 12 +//TM1814 (RGBW) +#define I_8266_U0_TM1_4 13 +#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 21 +#define I_32_I0_NEO_3 22 +#define I_32_I1_NEO_3 23 +#define I_32_BB_NEO_3 24 // bitbangging on ESP32 not recommended +//RGBW +#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 // bitbangging on ESP32 not recommended +//400Kbps +#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 // bitbangging on ESP32 not recommended +//TM1814 (RGBW) +#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 39 //hardware SPI +#define I_SS_DOT_3 40 //soft SPI + +//LPD8806 +#define I_HS_LPD_3 41 +#define I_SS_LPD_3 42 + +//WS2801 +#define I_HS_WS1_3 43 +#define I_SS_WS1_3 44 + +//P9813 +#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 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 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 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 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 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_NEO_3 NeoPixelBusLg +#endif +#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 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_NEO_4 NeoPixelBusLg +#endif +#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 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_400_3 NeoPixelBusLg +#endif +#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 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_TM1_4 NeoPixelBusLg +#endif +#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 +#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 NeoPixelBusLg +#define B_SS_LPD_3 NeoPixelBusLg + +//LPD6803 +#define B_HS_LPO_3 NeoPixelBusLg +#define B_SS_LPO_3 NeoPixelBusLg + +//WS2801 +#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 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) { + T tm1814_strip = static_cast(busPtr); + tm1814_strip->Begin(); + // 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, uint16_t clock_kHz = 0U) { + switch (busType) { + case I_NONE: break; + #ifdef ESP8266 + case I_8266_U0_NEO_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_NEO_3: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_NEO_3: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_NEO_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_NEO_4: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_NEO_4: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_NEO_4: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_NEO_4: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_400_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_400_3: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_400_3: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_400_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_TM1_4: beginTM1814(busPtr); break; + 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_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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->Begin(); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; + #endif + #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; + 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 + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1_4: beginTM1814(busPtr); 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 inadvertantly 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, uint16_t clock_kHz = 0U) { + void* busPtr = nullptr; + switch (busType) { + case I_NONE: break; + #ifdef ESP8266 + case I_8266_U0_NEO_3: busPtr = new B_8266_U0_NEO_3(len, pins[0]); break; + case I_8266_U1_NEO_3: busPtr = new B_8266_U1_NEO_3(len, pins[0]); break; + case I_8266_DM_NEO_3: busPtr = new B_8266_DM_NEO_3(len, pins[0]); break; + case I_8266_BB_NEO_3: busPtr = new B_8266_BB_NEO_3(len, pins[0]); break; + case I_8266_U0_NEO_4: busPtr = new B_8266_U0_NEO_4(len, pins[0]); break; + case I_8266_U1_NEO_4: busPtr = new B_8266_U1_NEO_4(len, pins[0]); break; + case I_8266_DM_NEO_4: busPtr = new B_8266_DM_NEO_4(len, pins[0]); break; + case I_8266_BB_NEO_4: busPtr = new B_8266_BB_NEO_4(len, pins[0]); break; + case I_8266_U0_400_3: busPtr = new B_8266_U0_400_3(len, pins[0]); break; + case I_8266_U1_400_3: busPtr = new B_8266_U1_400_3(len, pins[0]); break; + case I_8266_DM_400_3: busPtr = new B_8266_DM_400_3(len, pins[0]); break; + case I_8266_BB_400_3: busPtr = new B_8266_BB_400_3(len, pins[0]); break; + case I_8266_U0_TM1_4: busPtr = new B_8266_U0_TM1_4(len, pins[0]); break; + 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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; + #endif + #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; + 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 + #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, clock_kHz); + return busPtr; + } + + 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(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(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 +// 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 + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_4: (static_cast(busPtr))->Show(consistent); break; + #endif +// 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 + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_400_3: (static_cast(busPtr))->Show(consistent); break; + #endif +// 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 + #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 + 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(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; + #ifdef ESP8266 + case I_8266_U0_NEO_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_NEO_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_NEO_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_NEO_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_NEO_4: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_NEO_4: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_NEO_4: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_NEO_4: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_400_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_400_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_400_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_400_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_TM1_4: return (static_cast(busPtr))->CanShow(); break; + 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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: return (static_cast(busPtr))->CanShow(); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; + #endif + #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; + 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 + #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; + uint8_t b = c >> 0; + uint8_t w = c >> 24; + RgbwColor col; + + // 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 + 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 + } + + switch (busType) { + case I_NONE: break; + #ifdef ESP8266 + 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)); 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)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; + #endif +// 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 + #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; + 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 + #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)); 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))->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))->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 + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_400_3: (static_cast(busPtr))->SetLuminance(b); break; + #endif +// 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 + #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_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 + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + #endif +// 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 + #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))->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); + switch (busType) { + case I_NONE: break; + #ifdef ESP8266 + case I_8266_U0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U1_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_DM_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_BB_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U1_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_DM_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_BB_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U1_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_DM_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_BB_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + 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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + #endif + #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; + 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 + #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; + } + + // 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 + 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 + case 4: return ((col.W << 24) | (col.R << 8) | (col.B << 16) | (col.G)); //4 = BGR + case 5: return ((col.W << 24) | (col.G << 8) | (col.B << 16) | (col.R)); //5 = GBR + } + return 0; + } + + static void cleanup(void* busPtr, uint8_t busType) { + if (busPtr == nullptr) return; + switch (busType) { + case I_NONE: break; + #ifdef ESP8266 + case I_8266_U0_NEO_3: delete (static_cast(busPtr)); break; + case I_8266_U1_NEO_3: delete (static_cast(busPtr)); break; + case I_8266_DM_NEO_3: delete (static_cast(busPtr)); break; + case I_8266_BB_NEO_3: delete (static_cast(busPtr)); break; + case I_8266_U0_NEO_4: delete (static_cast(busPtr)); break; + case I_8266_U1_NEO_4: delete (static_cast(busPtr)); break; + case I_8266_DM_NEO_4: delete (static_cast(busPtr)); break; + case I_8266_BB_NEO_4: delete (static_cast(busPtr)); break; + case I_8266_U0_400_3: delete (static_cast(busPtr)); break; + case I_8266_U1_400_3: delete (static_cast(busPtr)); break; + case I_8266_DM_400_3: delete (static_cast(busPtr)); break; + case I_8266_BB_400_3: delete (static_cast(busPtr)); break; + case I_8266_U0_TM1_4: delete (static_cast(busPtr)); break; + 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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: delete (static_cast(busPtr)); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; + #endif + #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 WLED_NO_I2S0_PIXELBUS + case I_32_I0_400_3: delete (static_cast(busPtr)); break; + #endif + #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; + 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 + #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; + case I_SS_P98_3: delete (static_cast(busPtr)); break; + } + } + + //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 + bool isHSPI = false; + #ifdef ESP8266 + if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true; + #else + // temporary hack to limit use of hardware SPI to a single SPI peripheral (HSPI): only allow ESP32 hardware serial on segment 0 + // SPI global variable is normally linked to VSPI on ESP32 (or FSPI C3, S3) + if (!num) isHSPI = true; + #endif + uint8_t t = I_NONE; + switch (busType) { + case TYPE_APA102: t = I_SS_DOT_3; break; + case TYPE_LPD8806: t = I_SS_LPD_3; break; + case TYPE_LPD6803: t = I_SS_LPO_3; break; + case TYPE_WS2801: t = I_SS_WS1_3; break; + case TYPE_P9813: t = I_SS_P98_3; break; + default: t=I_NONE; + } + if (t > I_NONE && isHSPI) t--; //hardware SPI has one smaller ID than software + return t; + } else { + #ifdef ESP8266 + uint8_t offset = pins[0] -1; //for driver: 0 = uart0, 1 = uart1, 2 = dma, 3 = bitbang + if (offset > 3) offset = 3; + switch (busType) { + case TYPE_WS2812_1CH_X3: + case TYPE_WS2812_2CH_X3: + case TYPE_WS2812_RGB: + case TYPE_WS2812_WWA: + return I_8266_U0_NEO_3 + offset; + case TYPE_SK6812_RGBW: + return I_8266_U0_NEO_4 + offset; + case TYPE_WS2811_400KHZ: + 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 + #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; + #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; + case TYPE_SK6812_RGBW: + return I_32_RN_NEO_4 + offset; + case TYPE_WS2811_400KHZ: + 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 + } + return I_NONE; + } +}; + +#endif \ No newline at end of file diff --git a/wled00/button.cpp b/wled00/button.cpp index e71cce96..d45274a6 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -4,118 +4,388 @@ * Physical IO */ -void shortPressAction() +#define WLED_DEBOUNCE_THRESHOLD 50 // only consider button input of at least 50ms as valid (debouncing) +#define WLED_LONG_PRESS 600 // long press if button is released after held for at least 600ms +#define WLED_DOUBLE_PRESS 350 // double press if another press within 350ms after a short press +#define WLED_LONG_REPEATED_ACTION 300 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0 +#define WLED_LONG_AP 5000 // how long button 0 needs to be held to activate WLED-AP +#define WLED_LONG_FACTORY_RESET 10000 // how long button 0 needs to be held to trigger a factory reset + +static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage + +void shortPressAction(uint8_t b) { - if (!macroButton) - { - toggleOnOff(); - colorUpdated(NOTIFIER_CALL_MODE_BUTTON); + if (!macroButton[b]) { + switch (b) { + case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break; + case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break; + } } else { - applyMacro(macroButton); - } -} - - -void handleButton() -{ -#ifdef BTNPIN - if (!buttonEnabled) return; - - if (digitalRead(BTNPIN) == LOW) //pressed - { - if (!buttonPressedBefore) buttonPressedTime = millis(); - buttonPressedBefore = true; - - if (millis() - buttonPressedTime > 600) //long press - { - if (!buttonLongPressed) - { - if (macroLongPress) {applyMacro(macroLongPress);} - else _setRandomColor(false,true); - - buttonLongPressed = true; - } - } - } - else if (digitalRead(BTNPIN) == HIGH && buttonPressedBefore) //released - { - long dur = millis() - buttonPressedTime; - if (dur < 50) {buttonPressedBefore = false; return;} //too short "press", debounce - bool doublePress = buttonWaitTime; - buttonWaitTime = 0; - - if (dur > 6000) //long press - { - WLED::instance().initAP(true); - } - else if (!buttonLongPressed) { //short press - if (macroDoublePress) - { - if (doublePress) applyMacro(macroDoublePress); - else buttonWaitTime = millis(); - } else shortPressAction(); - } - buttonPressedBefore = false; - buttonLongPressed = false; + applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); } - if (buttonWaitTime && millis() - buttonWaitTime > 450 && !buttonPressedBefore) - { - buttonWaitTime = 0; - shortPressAction(); +#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) +{ + if (!macroLongPress[b]) { + switch (b) { + case 0: setRandomColor(col); colorUpdated(CALL_MODE_BUTTON); break; + case 1: bri += 8; stateUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action + } + } else { + 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) +{ + if (!macroDoublePress[b]) { + switch (b) { + //case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set + case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; + } + } else { + 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) +{ + if (btnPin[i]<0) return false; + uint8_t pin = btnPin[i]; + + switch (buttonType[i]) { + case BTN_TYPE_NONE: + case BTN_TYPE_RESERVED: + break; + case BTN_TYPE_PUSH: + case BTN_TYPE_SWITCH: + if (digitalRead(pin) == LOW) return true; + break; + case BTN_TYPE_PUSH_ACT_HIGH: + case BTN_TYPE_PIR_SENSOR: + if (digitalRead(pin) == HIGH) return true; + break; + case BTN_TYPE_TOUCH: + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if (touchRead(pin) <= touchThreshold) return true; + #endif + break; + } + return false; +} + +void handleSwitch(uint8_t b) +{ + // isButtonPressed() handles inverted/noninverted logic + if (buttonPressedBefore[b] != isButtonPressed(b)) { + buttonPressedTime[b] = millis(); + buttonPressedBefore[b] = !buttonPressedBefore[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]; + if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b); + else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); + mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on"); + } +#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] = {0}; + static float filteredReading[WLED_MAX_BUTTONS] = {0.0f}; + uint16_t rawReading; // raw value from analogRead, scaled to 12bit + + #ifdef ESP8266 + rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit + #else + 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 + if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading + + // Unomment 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 (strip.isUpdating()) return; // give up + + oldRead[b] = aRead; + + // if no macro for "short press" and "long press" is defined use brightness control + if (!macroButton[b] && !macroLongPress[b]) { + // if "double press" macro defines which option to change + if (macroDoublePress[b] >= 250) { + // global brightness + if (aRead == 0) { + briLast = bri; + bri = 0; + } else{ + bri = aRead; + } + } else if (macroDoublePress[b] == 249) { + // effect speed + effectSpeed = aRead; + } else if (macroDoublePress[b] == 248) { + // effect intensity + effectIntensity = aRead; + } 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 + Segment& seg = strip.getSegment(macroDoublePress[b]); + if (aRead == 0) { + seg.setOption(SEG_OPTION_ON, false); // off (use transition) + } else { + 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); + } + } else { + //TODO: + // we can either trigger a preset depending on the level (between short and long entries) + // or use it for RGBW direct control + } + colorUpdated(CALL_MODE_BUTTON); +} + +void handleButton() +{ + static unsigned long lastRead = 0UL; + static unsigned long lastRun = 0UL; + unsigned long now = millis(); + + if (strip.isUpdating() && (now - lastRun < 400)) 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 ANALOG_BTN_READ_CYCLE) { + handleAnalog(b); + lastRead = now; + } + continue; + } + + // 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; + } + + // 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) + return; + } + + if (!buttonPressedBefore[b]) buttonPressedTime[b] = now; + buttonPressedBefore[b] = true; + + 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] = now - WLED_LONG_REPEATED_ACTION; //333ms + } + buttonLongPressed[b] = true; + } + + } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + long dur = now - buttonPressedTime[b]; + + // 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 + return; + } + + 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] = now; + } + } + } + buttonPressedBefore[b] = false; + buttonLongPressed[b] = false; + } + + //if 350ms elapsed since last short press release it is a short press + if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) { + buttonWaitTime[b] = 0; + shortPressAction(b); + } + } +} + +// 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) - { - #if RLYPIN >= 0 - digitalWrite(RLYPIN, RLYMDE); + { + #ifdef ESP32_DATA_IDLE_HIGH + esp32RMTInvertIdle(); #endif + if (rlyPin>=0) { + pinMode(rlyPin, OUTPUT); + digitalWrite(rlyPin, rlyMde); + } offMode = false; } } else if (millis() - lastOnTime > 600) { - #if RLYPIN >= 0 - if (!offMode) digitalWrite(RLYPIN, !RLYMDE); - #endif + 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 + 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); + } + } offMode = true; } - - #if AUXPIN >= 0 - //output - if (auxActive || auxActiveBefore) - { - if (!auxActiveBefore) - { - auxActiveBefore = true; - switch (auxTriggeredState) - { - case 0: pinMode(AUXPIN, INPUT); break; - case 1: pinMode(AUXPIN, OUTPUT); digitalWrite(AUXPIN, HIGH); break; - case 2: pinMode(AUXPIN, OUTPUT); digitalWrite(AUXPIN, LOW); break; - } - auxStartTime = millis(); - } - if ((millis() - auxStartTime > auxTime*1000 && auxTime != 255) || !auxActive) - { - auxActive = false; - auxActiveBefore = false; - switch (auxDefaultState) - { - case 0: pinMode(AUXPIN, INPUT); break; - case 1: pinMode(AUXPIN, OUTPUT); digitalWrite(AUXPIN, HIGH); break; - case 2: pinMode(AUXPIN, OUTPUT); digitalWrite(AUXPIN, LOW); break; - } - } - } - #endif -} +} \ No newline at end of file diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp new file mode 100644 index 00000000..cafba6a0 --- /dev/null +++ b/wled00/cfg.cpp @@ -0,0 +1,1097 @@ +#include "wled.h" +#include "wled_ethernet.h" + +/* + * Serializes and parses the cfg.json and wsec.json settings files, stored in internal FS. + * The structure of the JSON is not to be considered an official API and may change without notice. + */ + +//simple macro for ArduinoJSON's or syntax +#define CJSON(a,b) a = b | a + +void getStringFromJson(char* dest, const char* src, size_t len) { + if (src != nullptr) strlcpy(dest, src, len); +} + +bool deserializeConfig(JsonObject doc, bool fromFS) { + bool needsSave = false; + //int rev_major = doc["rev"][0]; // 1 + //int rev_minor = doc["rev"][1]; // 0 + + //long vid = doc[F("vid")]; // 2010020 + + #ifdef WLED_USE_ETHERNET + JsonObject ethernet = doc[F("eth")]; + CJSON(ethernetType, ethernet["type"]); + // NOTE: Ethernet configuration takes priority over other use of pins + WLED::instance().initEthernet(); + #endif + + JsonObject id = doc["id"]; + 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); + //int nw_ins_0_pskl = nw_ins_0[F("pskl")]; + //The WiFi PSK is normally not contained in the regular file for security reasons. + //If it is present however, we will use it + getStringFromJson(clientPass, nw_ins_0["psk"], 65); + + JsonArray nw_ins_0_ip = nw_ins_0["ip"]; + JsonArray nw_ins_0_gw = nw_ins_0["gw"]; + JsonArray nw_ins_0_sn = nw_ins_0["sn"]; + + for (byte i = 0; i < 4; i++) { + CJSON(staticIP[i], nw_ins_0_ip[i]); + CJSON(staticGateway[i], nw_ins_0_gw[i]); + CJSON(staticSubnet[i], nw_ins_0_sn[i]); + } + + JsonObject ap = doc["ap"]; + getStringFromJson(apSSID, ap[F("ssid")], 33); + getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security + //int ap_pskl = ap[F("pskl")]; + + CJSON(apChannel, ap[F("chan")]); + if (apChannel > 13 || apChannel < 1) apChannel = 1; + + CJSON(apHide, ap[F("hide")]); + if (apHide > 1) apHide = 1; + + CJSON(apBehavior, ap[F("behav")]); + + /* + JsonArray ap_ip = ap["ip"]; + for (byte i = 0; i < 4; i++) { + apIP[i] = ap_ip; + } + */ + + noWifiSleep = doc[F("wifi")][F("sleep")] | !noWifiSleep; // inverted + noWifiSleep = !noWifiSleep; + //int wifi_phy = doc[F("wifi")][F("phy")]; //force phy mode n? + + JsonObject hw = doc[F("hw")]; + + // initialize LED pins and lengths prior to other HW (except for ethernet) + JsonObject hw_led = hw["led"]; + + uint8_t autoWhiteMode = RGBW_MODE_MANUAL_ONLY; + CJSON(strip.ablMilliampsMax, hw_led[F("maxpwr")]); + CJSON(strip.milliampsPerLed, hw_led[F("ledma")]); + Bus::setGlobalAWMode(hw_led[F("rgbwm")] | 255); + 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, globalBufMem = 0; + uint16_t maxlen = 0; + bool busesChanged = false; + for (JsonObject elm : ins) { + 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; + pins[0] = pinArr[0]; + uint8_t i = 0; + for (int p : pinArr) { + pins[i++] = p; + if (i>4) break; + } + + uint16_t length = elm["len"] | 1; + uint8_t colorOrder = (int)elm[F("order")]; + 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 + uint8_t AWmode = elm[F("rgbwm")] | autoWhiteMode; + if (fromFS) { + BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer); + mem += BusManager::memUsage(bc); + 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, AWmode, freqkHz, useGlobalLedBuffer); + busesChanged = true; + } + s++; + } + doInitBusses = busesChanged; + // finalization done in beginStrip() + } + 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")]; + if (!hw_com.isNull()) { + ColorOrderMap com = {}; + uint8_t s = 0; + for (JsonObject entry : hw_com) { + if (s > WLED_MAX_COLOR_ORDER_MAPPINGS) break; + uint16_t start = entry["start"] | 0; + uint16_t len = entry["len"] | 0; + uint8_t colorOrder = (int)entry[F("order")]; + com.add(start, len, colorOrder); + s++; + } + busses.updateColorOrderMap(com); + } + + // 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; + #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_PRINTF("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n", btnPin[s], s); + btnPin[s] = -1; + pinManager.deallocatePin(pin,PinOwner::Button); + } + 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; + } + JsonArray hw_btn_ins_0_macros = btn["macros"]; + CJSON(macroButton[s], hw_btn_ins_0_macros[0]); + CJSON(macroLongPress[s],hw_btn_ins_0_macros[1]); + CJSON(macroDoublePress[s], hw_btn_ins_0_macros[2]); + if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached + } + // clear remaining buttons + for (; s -2) { + pinManager.deallocatePin(irPin, PinOwner::IR); + if (pinManager.allocatePin(hw_ir_pin, false, PinOwner::IR)) { + irPin = hw_ir_pin; + } else { + irPin = -1; + } + } + CJSON(irEnabled, hw["ir"]["type"]); + CJSON(irApplyToAllSelected, hw["ir"]["sel"]); + + 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); + } else { + rlyPin = -1; + } + } + if (relay.containsKey("rev")) { + rlyMde = !relay["rev"]; + } + + CJSON(serialBaud, hw[F("baud")]); + if (serialBaud < 96 || serialBaud > 15000) 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 initilised (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")]; + CJSON(briMultiplier, light[F("scale-bri")]); + 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"]; + 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"]); + int tdd = light_tr["dur"] | -1; + if (tdd >= 0) transitionDelay = transitionDelayDefault = tdd * 100; + CJSON(strip.paletteFade, light_tr["pal"]); + CJSON(randomPaletteChangeTime, light_tr[F("rpc")]); + + JsonObject light_nl = light["nl"]; + CJSON(nightlightMode, light_nl["mode"]); + byte prev = nightlightDelayMinsDefault; + CJSON(nightlightDelayMinsDefault, light_nl["dur"]); + if (nightlightDelayMinsDefault != prev) nightlightDelayMins = nightlightDelayMinsDefault; + + CJSON(nightlightTargetBri, light_nl[F("tbri")]); + CJSON(macroNl, light_nl["macro"]); + + JsonObject def = doc["def"]; + CJSON(bootPreset, def["ps"]); + CJSON(turnOnAtBoot, def["on"]); // true + CJSON(briS, def["bri"]); // 128 + + JsonObject interfaces = doc["if"]; + + JsonObject if_sync = interfaces["sync"]; + CJSON(udpPort, if_sync[F("port0")]); // 21324 + CJSON(udpPort2, if_sync[F("port1")]); // 65506 + + JsonObject if_sync_recv = if_sync["recv"]; + CJSON(receiveNotificationBrightness, if_sync_recv["bri"]); + CJSON(receiveNotificationColor, if_sync_recv["col"]); + CJSON(receiveNotificationEffects, if_sync_recv["fx"]); + CJSON(receiveGroups, if_sync_recv["grp"]); + CJSON(receiveSegmentOptions, if_sync_recv["seg"]); + CJSON(receiveSegmentBounds, if_sync_recv["sb"]); + //! following line might be a problem if called after boot + receiveNotifications = (receiveNotificationBrightness || receiveNotificationColor || receiveNotificationEffects || receiveSegmentOptions); + + JsonObject if_sync_send = if_sync["send"]; + prev = notifyDirectDefault; + CJSON(notifyDirectDefault, if_sync_send[F("dir")]); + if (notifyDirectDefault != prev) notifyDirect = notifyDirectDefault; + CJSON(notifyButton, if_sync_send["btn"]); + CJSON(notifyAlexa, if_sync_send["va"]); + CJSON(notifyHue, if_sync_send["hue"]); + CJSON(notifyMacro, if_sync_send["macro"]); + 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")]); + CJSON(nodeBroadcastEnabled, if_nodes[F("bcast")]); + + 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")]); + + JsonObject if_live_dmx = if_live[F("dmx")]; + 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; + if (tdd >= 0) realtimeTimeoutMs = tdd * 100; + CJSON(arlsForceMaxBri, if_live[F("maxbri")]); + CJSON(arlsDisableGammaCorrection, if_live[F("no-gc")]); // false + CJSON(arlsOffset, if_live[F("offset")]); // 0 + + CJSON(alexaEnabled, interfaces["va"][F("alexa")]); // false + + CJSON(macroAlexaOn, interfaces["va"]["macros"][0]); + CJSON(macroAlexaOff, interfaces["va"]["macros"][1]); + + 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); + 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); // "" + 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"]); + CJSON(huePollLightId, if_hue["id"]); + tdd = if_hue[F("iv")] | -1; + if (tdd >= 2) huePollIntervalMs = tdd * 100; + + JsonObject if_hue_recv = if_hue["recv"]; + CJSON(hueApplyOnOff, if_hue_recv["on"]); + CJSON(hueApplyBri, if_hue_recv["bri"]); + CJSON(hueApplyColor, if_hue_recv["col"]); + + JsonArray if_hue_ip = if_hue["ip"]; + + for (byte i = 0; i < 4; i++) + CJSON(hueIP[i], if_hue_ip[i]); +#endif + + JsonObject if_ntp = interfaces[F("ntp")]; + CJSON(ntpEnabled, if_ntp["en"]); + getStringFromJson(ntpServerName, if_ntp[F("host")], 33); // "1.wled.pool.ntp.org" + CJSON(currentTimezone, if_ntp[F("tz")]); + CJSON(utcOffsetSecs, if_ntp[F("offset")]); + CJSON(useAMPM, if_ntp[F("ampm")]); + CJSON(longitude, if_ntp[F("ln")]); + CJSON(latitude, if_ntp[F("lt")]); + + JsonObject ol = doc[F("ol")]; + CJSON(overlayCurrent ,ol[F("clock")]); // 0 + CJSON(countdownMode, ol[F("cntdwn")]); + + CJSON(overlayMin, ol["min"]); + CJSON(overlayMax, ol[F("max")]); + CJSON(analogClock12pixel, ol[F("o12pix")]); + CJSON(analogClock5MinuteMarks, ol[F("o5m")]); + CJSON(analogClockSecondsTrail, ol[F("osec")]); + + //timed macro rules + JsonObject tm = doc[F("timers")]; + JsonObject cntdwn = tm[F("cntdwn")]; + JsonArray cntdwn_goal = cntdwn[F("goal")]; + CJSON(countdownYear, cntdwn_goal[0]); + CJSON(countdownMonth, cntdwn_goal[1]); + CJSON(countdownDay, cntdwn_goal[2]); + CJSON(countdownHour, cntdwn_goal[3]); + CJSON(countdownMin, cntdwn_goal[4]); + CJSON(countdownSec, cntdwn_goal[5]); + CJSON(macroCountdown, cntdwn["macro"]); + setCountdown(); + + JsonArray timers = tm["ins"]; + uint8_t it = 0; + for (JsonObject timer : timers) { + if (it > 9) break; + if (it<8 && timer[F("hour")]==255) it=8; // hour==255 -> sunrise/sunset + CJSON(timerHours[it], timer[F("hour")]); + CJSON(timerMinutes[it], timer["min"]); + CJSON(timerMacro[it], timer["macro"]); + + byte dowPrev = timerWeekday[it]; + //note: act is currently only 0 or 1. + //the reason we are not using bool is that the on-disk type in 0.11.0 was already int + int actPrev = timerWeekday[it] & 0x01; + CJSON(timerWeekday[it], timer[F("dow")]); + if (timerWeekday[it] != dowPrev) { //present in JSON + timerWeekday[it] <<= 1; //add active bit + int act = timer["en"] | actPrev; + if (act) timerWeekday[it]++; + } + if (it<8) { + JsonObject start = timer["start"]; + byte startm = start["mon"]; + if (startm) timerMonth[it] = (startm << 4); + CJSON(timerDay[it], start["day"]); + JsonObject end = timer["end"]; + CJSON(timerDayEnd[it], end["day"]); + byte endm = end["mon"]; + if (startm) timerMonth[it] += endm & 0x0F; + if (!(timerMonth[it] & 0x0F)) timerMonth[it] += 12; //default end month to 12 + } + it++; + } + + JsonObject ota = doc["ota"]; + const char* pwd = ota["psk"]; //normally not present due to security + + bool pwdCorrect = !otaLock; //always allow access if ota not locked + if (pwd != nullptr && strncmp(otaPass, pwd, 33) == 0) pwdCorrect = true; + + if (pwdCorrect) { //only accept these values from cfg.json if ota is unlocked (else from wsec.json) + CJSON(otaLock, ota[F("lock")]); + CJSON(wifiLock, ota[F("lock-wifi")]); + CJSON(aOtaEnabled, ota[F("aota")]); + getStringFromJson(otaPass, pwd, 33); //normally not present due to security + } + + #ifdef WLED_ENABLE_DMX + JsonObject dmx = doc["dmx"]; + CJSON(DMXChannels, dmx[F("chan")]); + CJSON(DMXGap,dmx[F("gap")]); + CJSON(DMXStart, dmx["start"]); + CJSON(DMXStartLED,dmx[F("start-led")]); + + JsonArray dmx_fixmap = dmx[F("fixmap")]; + for (int i = 0; i < dmx_fixmap.size(); i++) { + if (i > 14) break; + CJSON(DMXFixtureMap[i],dmx_fixmap[i]); + } + + CJSON(e131ProxyUniverse, dmx[F("e131proxy")]); + #endif + + DEBUG_PRINTLN(F("Starting usermod config.")); + JsonObject usermods_settings = doc["um"]; + if (!usermods_settings.isNull()) { + needsSave = !usermods.readFromConfig(usermods_settings); + } + + 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 + } + + if (!requestJSONBufferLock(1)) return; + + DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); + + success = readObjectFromFile("/cfg.json", nullptr, &doc); + 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; + } + + // NOTE: This routine deserializes *and* applies the configuration + // Therefore, must also initialize ethernet from this function + bool needsSave = deserializeConfig(doc.as(), true); + releaseJSONBufferLock(); + + if (needsSave) serializeConfig(); // usermods required new parameters +} + +void serializeConfig() { + serializeConfigSec(); + + DEBUG_PRINTLN(F("Writing settings to /cfg.json...")); + + if (!requestJSONBufferLock(2)) return; + + JsonArray rev = doc.createNestedArray("rev"); + rev.add(1); //major settings revision + rev.add(0); //minor settings revision + + doc[F("vid")] = VERSION; + + JsonObject id = doc.createNestedObject("id"); + 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"); + + JsonArray nw_ins = nw.createNestedArray("ins"); + + JsonObject nw_ins_0 = nw_ins.createNestedObject(); + nw_ins_0[F("ssid")] = clientSSID; + nw_ins_0[F("pskl")] = strlen(clientPass); + + JsonArray nw_ins_0_ip = nw_ins_0.createNestedArray("ip"); + JsonArray nw_ins_0_gw = nw_ins_0.createNestedArray("gw"); + JsonArray nw_ins_0_sn = nw_ins_0.createNestedArray("sn"); + + for (byte i = 0; i < 4; i++) { + nw_ins_0_ip.add(staticIP[i]); + nw_ins_0_gw.add(staticGateway[i]); + nw_ins_0_sn.add(staticSubnet[i]); + } + + JsonObject ap = doc.createNestedObject("ap"); + ap[F("ssid")] = apSSID; + ap[F("pskl")] = strlen(apPass); + ap[F("chan")] = apChannel; + ap[F("hide")] = apHide; + ap[F("behav")] = apBehavior; + + JsonArray ap_ip = ap.createNestedArray("ip"); + ap_ip.add(4); + ap_ip.add(3); + ap_ip.add(2); + ap_ip.add(1); + + JsonObject wifi = doc.createNestedObject("wifi"); + wifi[F("sleep")] = !noWifiSleep; + //wifi[F("phy")] = 1; + + #ifdef WLED_USE_ETHERNET + JsonObject ethernet = doc.createNestedObject("eth"); + ethernet["type"] = ethernetType; + if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { + JsonArray pins = ethernet.createNestedArray("pin"); + for (uint8_t p=0; p=0) pins.add(ethernetBoards[ethernetType].eth_power); + if (ethernetBoards[ethernetType].eth_mdc>=0) pins.add(ethernetBoards[ethernetType].eth_mdc); + if (ethernetBoards[ethernetType].eth_mdio>=0) pins.add(ethernetBoards[ethernetType].eth_mdio); + switch (ethernetBoards[ethernetType].eth_clk_mode) { + case ETH_CLOCK_GPIO0_IN: + case ETH_CLOCK_GPIO0_OUT: + pins.add(0); + break; + case ETH_CLOCK_GPIO16_OUT: + pins.add(16); + break; + case ETH_CLOCK_GPIO17_OUT: + pins.add(17); + break; + } + } + #endif + + JsonObject hw = doc.createNestedObject("hw"); + + JsonObject hw_led = hw.createNestedObject("led"); + hw_led[F("total")] = strip.getLengthTotal(); //no longer read, but provided for compatibility on downgrade + hw_led[F("maxpwr")] = strip.ablMilliampsMax; + hw_led[F("ledma")] = strip.milliampsPerLed; + hw_led["cct"] = correctWB; + hw_led[F("cr")] = cctFromRgb; + hw_led[F("cb")] = strip.cctBlending; + hw_led["fps"] = strip.getTargetFps(); + 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; igetLength()==0) break; + JsonObject ins = hw_led_ins.createNestedObject(); + ins["start"] = bus->getStart(); + ins["len"] = bus->getLength(); + JsonArray ins_pin = ins.createNestedArray("pin"); + uint8_t pins[5]; + uint8_t nPins = bus->getPins(pins); + for (uint8_t i = 0; i < nPins; i++) ins_pin.add(pins[i]); + ins[F("order")] = bus->getColorOrder(); + ins["rev"] = bus->isReversed(); + ins[F("skip")] = bus->skippedLeds(); + ins["type"] = bus->getType() & 0x7F; + ins["ref"] = bus->isOffRefreshRequired(); + ins[F("rgbwm")] = bus->getAutoWhiteMode(); + ins[F("freq")] = bus->getFrequency(); + } + + JsonArray hw_com = hw.createNestedArray(F("com")); + const ColorOrderMap& com = busses.getColorOrderMap(); + for (uint8_t s = 0; s < com.count(); s++) { + const ColorOrderMapEntry *entry = com.get(s); + if (!entry) break; + + JsonObject co = hw_com.createNestedObject(); + co["start"] = entry->start; + co["len"] = entry->len; + co[F("order")] = entry->colorOrder; + } + + // 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 + for (uint8_t i=0; i> 1; + if (i<8) { + JsonObject start = timers_ins0.createNestedObject("start"); + start["mon"] = (timerMonth[i] >> 4) & 0xF; + start["day"] = timerDay[i]; + JsonObject end = timers_ins0.createNestedObject("end"); + end["mon"] = timerMonth[i] & 0xF; + end["day"] = timerDayEnd[i]; + } + } + + JsonObject ota = doc.createNestedObject("ota"); + ota[F("lock")] = otaLock; + ota[F("lock-wifi")] = wifiLock; + ota[F("pskl")] = strlen(otaPass); + ota[F("aota")] = aOtaEnabled; + + #ifdef WLED_ENABLE_DMX + JsonObject dmx = doc.createNestedObject("dmx"); + dmx[F("chan")] = DMXChannels; + dmx[F("gap")] = DMXGap; + dmx["start"] = DMXStart; + dmx[F("start-led")] = DMXStartLED; + + JsonArray dmx_fixmap = dmx.createNestedArray(F("fixmap")); + for (byte i = 0; i < 15; i++) { + dmx_fixmap.add(DMXFixtureMap[i]); + } + + dmx[F("e131proxy")] = e131ProxyUniverse; + #endif + + JsonObject usermods_settings = doc.createNestedObject("um"); + usermods.addToConfig(usermods_settings); + + File f = WLED_FS.open("/cfg.json", "w"); + 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...")); + + if (!requestJSONBufferLock(3)) return false; + + bool success = readObjectFromFile("/wsec.json", nullptr, &doc); + if (!success) { + releaseJSONBufferLock(); + return false; + } + + JsonObject nw_ins_0 = doc["nw"]["ins"][0]; + getStringFromJson(clientPass, nw_ins_0["psk"], 65); + + JsonObject ap = doc["ap"]; + getStringFromJson(apPass, ap["psk"] , 65); + + JsonObject interfaces = doc["if"]; + +#ifdef WLED_ENABLE_MQTT + JsonObject if_mqtt = interfaces["mqtt"]; + getStringFromJson(mqttPass, if_mqtt["psk"], 65); +#endif + +#ifndef WLED_DISABLE_HUESYNC + 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")]); + CJSON(wifiLock, ota[F("lock-wifi")]); + CJSON(aOtaEnabled, ota[F("aota")]); + + releaseJSONBufferLock(); + return true; +} + +void serializeConfigSec() { + DEBUG_PRINTLN(F("Writing settings to /wsec.json...")); + + if (!requestJSONBufferLock(4)) return; + + JsonObject nw = doc.createNestedObject("nw"); + + JsonArray nw_ins = nw.createNestedArray("ins"); + + JsonObject nw_ins_0 = nw_ins.createNestedObject(); + nw_ins_0["psk"] = clientPass; + + JsonObject ap = doc.createNestedObject("ap"); + ap["psk"] = apPass; + + JsonObject interfaces = doc.createNestedObject("if"); +#ifdef WLED_ENABLE_MQTT + JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); + if_mqtt["psk"] = mqttPass; +#endif +#ifndef WLED_DISABLE_HUESYNC + JsonObject if_hue = interfaces.createNestedObject("hue"); + if_hue[F("key")] = hueApiKey; +#endif + + doc["pin"] = settingsPIN; + + JsonObject ota = doc.createNestedObject("ota"); + ota[F("pwd")] = otaPass; + ota[F("lock")] = otaLock; + ota[F("lock-wifi")] = wifiLock; + ota[F("aota")] = aOtaEnabled; + + File f = WLED_FS.open("/wsec.json", "w"); + if (f) serializeJson(doc, f); + f.close(); + releaseJSONBufferLock(); +} diff --git a/wled00/colors.cpp b/wled00/colors.cpp index f76499a0..8c4baabb 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -1,68 +1,108 @@ #include "wled.h" /* - * Color conversion methods + * Color conversion & utility methods */ -void colorFromUint32(uint32_t in, bool secondary) -{ - if (secondary) { - colSec[3] = in >> 24 & 0xFF; - colSec[0] = in >> 16 & 0xFF; - colSec[1] = in >> 8 & 0xFF; - colSec[2] = in & 0xFF; - } else { - col[3] = in >> 24 & 0xFF; - col[0] = in >> 16 & 0xFF; - col[1] = in >> 8 & 0xFF; - col[2] = in & 0xFF; - } +/* + * 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); } -//load a color without affecting the white channel -void colorFromUint24(uint32_t in, bool secondary) +/* + * 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) { - if (secondary) { - colSec[0] = in >> 16 & 0xFF; - colSec[1] = in >> 8 & 0xFF; - colSec[2] = in & 0xFF; - } else { - col[0] = in >> 16 & 0xFF; - col[1] = in >> 8 & 0xFF; - col[2] = in & 0xFF; - } + 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); } -//relatively change white brightness, minumum A=5 -void relativeChangeWhite(int8_t amount, byte lowerBoundary) +void setRandomColor(byte* rgb) { - int16_t new_val = (int16_t) col[3] + amount; - if (new_val > 0xFF) new_val = 0xFF; - else if (new_val < lowerBoundary) new_val = lowerBoundary; - col[3] = new_val; + lastRandomIndex = strip.getMainSegment().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; } - if (useRGBW && strip.rgbwMode == RGBW_MODE_LEGACY) colorRGBtoRGBW(col); } -void colorCTtoRGB(uint16_t mired, byte* rgb) //white spectrum to rgb +//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 +{ + int r = 0, g = 0, b = 0; + float temp = kelvin / 100.0f; + if (temp <= 66.0f) { + r = 255; + g = roundf(99.4708025861f * logf(temp) - 161.1195681661f); + if (temp <= 19.0f) { + b = 0; + } else { + b = roundf(138.5177312231f * logf((temp - 10.0f)) - 305.0447927307f); + } + } else { + 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); + rgb[2] = (uint8_t) constrain(b, 0, 255); + rgb[3] = 0; +} + +void colorCTtoRGB(uint16_t mired, byte* rgb) //white spectrum to rgb, bins { //this is only an approximation using WS2812B with gamma correction enabled if (mired > 475) { @@ -82,7 +122,6 @@ void colorCTtoRGB(uint16_t mired, byte* rgb) //white spectrum to rgb } else { rgb[0]=237;rgb[1]=255;rgb[2]=239;//150 } - if (useRGBW && strip.rgbwMode == RGBW_MODE_LEGACY) colorRGBtoRGBW(col); } #ifndef WLED_DISABLE_HUESYNC @@ -111,9 +150,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 @@ -137,10 +176,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; - if (useRGBW) colorRGBtoRGBW(col); + 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) @@ -153,13 +191,13 @@ void colorRGBtoXY(byte* rgb, float* xy) //rgb to coordinates (https://www.develo } #endif // WLED_DISABLE_HUESYNC - +//RRGGBB / WWRRGGBB order for hex 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); @@ -168,10 +206,31 @@ void colorFromDecOrHexString(byte* rgb, char* in) c = strtoul(in, NULL, 10); } - rgb[3] = (c >> 24) & 0xFF; - rgb[0] = (c >> 16) & 0xFF; - rgb[1] = (c >> 8) & 0xFF; - rgb[2] = c & 0xFF; + rgb[0] = R(c); + rgb[1] = G(c); + rgb[2] = B(c); + rgb[3] = W(c); +} + +//contrary to the colorFromDecOrHexString() function, this uses the more standard RRGGBB / RRGGBBWW order +bool colorFromHexString(byte* rgb, const char* in) { + if (in == nullptr) return false; + size_t inputSize = strnlen(in, 9); + if (inputSize != 6 && inputSize != 8) return false; + + uint32_t c = strtoul(in, NULL, 16); + + if (inputSize == 6) { + rgb[0] = (c >> 16); + rgb[1] = (c >> 8); + rgb[2] = c ; + } else { + rgb[0] = (c >> 24); + rgb[1] = (c >> 16); + rgb[2] = (c >> 8); + rgb[3] = c ; + } + return true; } float minf (float v, float w) @@ -186,11 +245,106 @@ float maxf (float v, float w) return v; } -void colorRGBtoRGBW(byte* rgb) //rgb to rgbw (http://codewelt.com/rgbw). (RGBW_MODE_LEGACY) +// 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) { - 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); + //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]; + rgbw[0] = ((uint16_t) correctionRGB[0] * R(rgb)) /255; // correct R + rgbw[1] = ((uint16_t) correctionRGB[1] * G(rgb)) /255; // correct G + rgbw[2] = ((uint16_t) correctionRGB[2] * B(rgb)) /255; // correct B + rgbw[3] = W(rgb); + return RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3]); +} + +//approximates a Kelvin color temperature from an RGB color. +//this does no check for the "whiteness" of the color, +//so should be used combined with a saturation check (as done by auto-white) +//values from http://www.vendian.org/mncharity/dir3/blackbody/UnstableURLs/bbr_color.html (10deg) +//equation spreadsheet at https://bit.ly/30RkHaN +//accuracy +-50K from 1900K up to 8000K +//minimum returned: 1900K, maximum returned: 10091K (range of 8192) +uint16_t approximateKelvinFromRGB(uint32_t rgb) { + //if not either red or blue is 255, color is dimmed. Scale up + uint8_t r = R(rgb), b = B(rgb); + if (r == b) return 6550; //red == blue at about 6600K (also can't go further if both R and B are 0) + + if (r > b) { + //scale blue up as if red was at 255 + uint16_t scale = 0xFFFF / r; //get scale factor (range 257-65535) + b = ((uint16_t)b * scale) >> 8; + //For all temps K<6600 R is bigger than B (for full bri colors R=255) + //-> Use 9 linear approximations for blackbody radiation blue values from 2000-6600K (blue is always 0 below 2000K) + if (b < 33) return 1900 + b *6; + if (b < 72) return 2100 + (b-33) *10; + if (b < 101) return 2492 + (b-72) *14; + if (b < 132) return 2900 + (b-101) *16; + if (b < 159) return 3398 + (b-132) *19; + if (b < 186) return 3906 + (b-159) *22; + if (b < 210) return 4500 + (b-186) *25; + if (b < 230) return 5100 + (b-210) *30; + return 5700 + (b-230) *34; + } else { + //scale red up as if blue was at 255 + uint16_t scale = 0xFFFF / b; //get scale factor (range 257-65535) + r = ((uint16_t)r * scale) >> 8; + //For all temps K>6600 B is bigger than R (for full bri colors B=255) + //-> Use 2 linear approximations for blackbody radiation red values from 6600-10091K (blue is always 0 below 2000K) + if (r > 225) return 6600 + (254-r) *50; + uint16_t k = 8080 + (225-r) *86; + 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 1ec4caf5..aa256ebe 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -5,48 +5,180 @@ * 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 -#define WLED_MAX_USERMODS 4 +#ifndef WLED_MAX_USERMODS + #ifdef ESP8266 + #define WLED_MAX_USERMODS 4 + #else + #define WLED_MAX_USERMODS 6 + #endif +#endif + +#ifndef WLED_MAX_BUSSES + #ifdef ESP8266 + #define WLED_MAX_BUSSES 3 + #define WLED_MIN_VIRTUAL_BUSSES 2 + #else + #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 + #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 + +#ifndef WLED_MAX_BUTTONS + #ifdef ESP8266 + #define WLED_MAX_BUTTONS 2 + #else + #define WLED_MAX_BUTTONS 4 + #endif +#endif + +#ifdef ESP8266 +#define WLED_MAX_COLOR_ORDER_MAPPINGS 5 +#else +#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 -#define USERMOD_ID_EXAMPLE 2 //Usermod "usermod_v2_example.h" -#define USERMOD_ID_TEMPERATURE 3 //Usermod "usermod_temperature.h" -#define USERMOD_ID_FIXNETSERVICES 4 //Usermod "usermod_Fix_unreachable_netservices.h" -#define USERMOD_ID_PIRSWITCH 5 //Usermod "usermod_PIR_sensor_switch.h" -#define USERMOD_ID_IMU 6 //Usermod "usermod_mpu6050_imu.h" +#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 +#define USERMOD_ID_EXAMPLE 2 //Usermod "usermod_v2_example.h" +#define USERMOD_ID_TEMPERATURE 3 //Usermod "usermod_temperature.h" +#define USERMOD_ID_FIXNETSERVICES 4 //Usermod "usermod_Fix_unreachable_netservices.h" +#define USERMOD_ID_PIRSWITCH 5 //Usermod "usermod_PIR_sensor_switch.h" +#define USERMOD_ID_IMU 6 //Usermod "usermod_mpu6050_imu.h" +#define USERMOD_ID_FOUR_LINE_DISP 7 //Usermod "usermod_v2_four_line_display.h +#define USERMOD_ID_ROTARY_ENC_UI 8 //Usermod "usermod_v2_rotary_encoder_ui.h" +#define USERMOD_ID_AUTO_SAVE 9 //Usermod "usermod_v2_auto_save.h" +#define USERMOD_ID_DHT 10 //Usermod "usermod_dht.h" +#define USERMOD_ID_MODE_SORT 11 //Usermod "usermod_v2_mode_sort.h" +#define USERMOD_ID_VL53L0X 12 //Usermod "usermod_vl53l0x_gestures.h" +#define USERMOD_ID_MULTI_RELAY 13 //Usermod "usermod_multi_relay.h" +#define USERMOD_ID_ANIMATED_STAIRCASE 14 //Usermod "Animated_Staircase.h" +#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 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" +#define USERMOD_RGB_ROTARY_ENCODER 22 //Usermod "rgb-rotary-encoder.h" +#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" //Access point behavior -#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot -#define AP_BEHAVIOR_NO_CONN 1 //Open when no connection (either after boot or if connection is lost) -#define AP_BEHAVIOR_ALWAYS 2 //Always open -#define AP_BEHAVIOR_BUTTON_ONLY 3 //Only when button pressed for 6 sec +#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot +#define AP_BEHAVIOR_NO_CONN 1 //Open when no connection (either after boot or if connection is lost) +#define AP_BEHAVIOR_ALWAYS 2 //Always open +#define AP_BEHAVIOR_BUTTON_ONLY 3 //Only when button pressed for 6 sec -//Notifier callMode -#define NOTIFIER_CALL_MODE_INIT 0 //no updates on init, can be used to disable updates -#define NOTIFIER_CALL_MODE_DIRECT_CHANGE 1 -#define NOTIFIER_CALL_MODE_BUTTON 2 -#define NOTIFIER_CALL_MODE_NOTIFICATION 3 -#define NOTIFIER_CALL_MODE_NIGHTLIGHT 4 -#define NOTIFIER_CALL_MODE_NO_NOTIFY 5 -#define NOTIFIER_CALL_MODE_FX_CHANGED 6 //no longer used -#define NOTIFIER_CALL_MODE_HUE 7 -#define NOTIFIER_CALL_MODE_PRESET_CYCLE 8 -#define NOTIFIER_CALL_MODE_BLYNK 9 -#define NOTIFIER_CALL_MODE_ALEXA 10 +//Notifier callMode +#define CALL_MODE_INIT 0 //no updates on init, can be used to disable updates +#define CALL_MODE_DIRECT_CHANGE 1 +#define CALL_MODE_BUTTON 2 //default button actions applied to selected segments +#define CALL_MODE_NOTIFICATION 3 +#define CALL_MODE_NIGHTLIGHT 4 +#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 //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 @@ -57,6 +189,7 @@ #define REALTIME_MODE_ADALIGHT 5 #define REALTIME_MODE_ARTNET 6 #define REALTIME_MODE_TPM2NET 7 +#define REALTIME_MODE_DDP 8 //realtime override modes #define REALTIME_OVERRIDE_NONE 0 @@ -67,24 +200,101 @@ #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 segement) +#define DMX_MODE_EFFECT_SEGMENT_W 9 //trigger standalone effects of WLED (18 channels per segement) +#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 +//bits 4/5/6: specifies the class of LED driver - 0b000 (dec. 0-15) unconfigured/reserved +// - 0b001 (dec. 16-31) digital (data pin only) +// - 0b010 (dec. 32-47) analog (PWM) +// - 0b011 (dec. 48-63) digital (data + clock / SPI) +// - 0b100 (dec. 64-79) unused/reserved +// - 0b101 (dec. 80-95) virtual network busses +// - 0b110 (dec. 96-111) unused/reserved +// - 0b111 (dec. 112-127) unused/reserved +//bit 7 is reserved and set to 0 -//Light capability byte (unused) #define TYPE_NONE 0 //light is not configured #define TYPE_RESERVED 1 //unused. Might indicate a "virtual" light -#define TYPE_WS2812_RGB 2 -#define TYPE_SK6812_RGBW 3 -#define TYPE_WS2812_WWA 4 //amber + warm + cold white -#define TYPE_WS2801 5 -#define TYPE_ANALOG_1CH 6 //single channel PWM. Uses value of brightest RGBW channel -#define TYPE_ANALOG_2CH 7 //analog WW + CW -#define TYPE_ANALOG_3CH 8 //analog RGB -#define TYPE_ANALOG_4CH 9 //analog RGBW -#define TYPE_ANALOG_5CH 10 //analog RGB + WW + CW -#define TYPE_APA102 11 -#define TYPE_LPD8806 12 +//Digital types (data pin only) (16-31) +#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) +#define TYPE_ONOFF 40 //binary output (relays etc.) +#define TYPE_ANALOG_1CH 41 //single channel PWM. Uses value of brightest RGBW channel +#define TYPE_ANALOG_2CH 42 //analog WW + CW +#define TYPE_ANALOG_3CH 43 //analog RGB +#define TYPE_ANALOG_4CH 44 //analog RGBW +#define TYPE_ANALOG_5CH 45 //analog RGB + WW + CW +//Digital types (data + clock / SPI) (48-63) +#define TYPE_WS2801 50 +#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, 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) +#define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only +#define IS_2PIN(t) ((t) > 47) + +//Color orders +#define COL_ORDER_GRB 0 //GRB(w),defaut +#define COL_ORDER_RGB 1 //common for WS2811 +#define COL_ORDER_BRG 2 +#define COL_ORDER_RBG 3 +#define COL_ORDER_BGR 4 +#define COL_ORDER_GBR 5 +#define COL_ORDER_MAX 5 + + +//Button type +#define BTN_TYPE_NONE 0 +#define BTN_TYPE_RESERVED 1 +#define BTN_TYPE_PUSH 2 +#define BTN_TYPE_PUSH_ACT_HIGH 3 +#define BTN_TYPE_SWITCH 4 +#define BTN_TYPE_PIR_SENSOR 5 +#define BTN_TYPE_TOUCH 6 +#define BTN_TYPE_ANALOG 7 +#define BTN_TYPE_ANALOG_INVERTED 8 + +//Ethernet board types +#define WLED_NUM_ETH_TYPES 11 + +#define WLED_ETH_NONE 0 +#define WLED_ETH_WT32_ETH01 1 +#define WLED_ETH_ESP32_POE 2 +#define WLED_ETH_WESP32 3 +#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 @@ -99,30 +309,231 @@ #define SEG_OPTION_SELECTED 0 #define SEG_OPTION_REVERSED 1 #define SEG_OPTION_ON 2 -#define SEG_OPTION_PAUSED 3 //unused -#define SEG_OPTION_NONUNITY 4 //Indicates that the effect does not use FRAMETIME or needs getPixelColor -#define SEG_OPTION_TRANSITIONAL 7 +#define SEG_OPTION_MIRROR 3 //Indicates that the effect will be mirrored within the segment +#define SEG_OPTION_FREEZE 4 //Segment contents will not be refreshed +#define SEG_OPTION_RESET 5 //Segment runtime requires reset +#define SEG_OPTION_TRANSITIONAL 6 +#define SEG_OPTION_REVERSED_Y 7 +#define SEG_OPTION_MIRROR_Y 8 +#define SEG_OPTION_TRANSPOSED 9 -//Timer mode types +//Segment differs return byte +#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 ounds +#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_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_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 #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. -//EEPROM size -#define EEPSIZE 2560 //Maximum is 4096 +// 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 -// maximum number of LEDs - MAX_LEDS is coming from the JSON response getting too big, MAX_LEDS_DMA will become a timing issue -#define MAX_LEDS 1500 -#define MAX_LEDS_DMA 500 +//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 +#else +#define MAX_LEDS 8192 +#endif +#endif + +#ifndef MAX_LED_MEMORY + #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 2048 // may not be enough for fast LEDs (i.e. APA102) +#endif // string temp buffer (now stored in stack locally) -#define OMAX 2048 +#ifdef ESP8266 +#define SETTINGS_STACK_BUF_SIZE 2048 +#else +#define SETTINGS_STACK_BUF_SIZE 3608 // warning: quite a large value for stack +#endif -#define E131_MAX_UNIVERSE_COUNT 9 +#ifdef WLED_USE_ETHERNET + #define E131_MAX_UNIVERSE_COUNT 20 +#else + #ifdef ESP8266 + #define E131_MAX_UNIVERSE_COUNT 9 + #else + #define E131_MAX_UNIVERSE_COUNT 12 + #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 +#ifdef ESP8266 + #define WLED_PWM_FREQ 880 //PWM frequency proven as good for LEDs +#else + #define WLED_PWM_FREQ 19531 +#endif +#endif + +#define TOUCH_THRESHOLD 32 // limit to recognize a touch, higher value means more sensitive + +// Size of buffer for API JSON object (increase for more segments) +#ifdef ESP8266 + #define JSON_BUFFER_SIZE 10240 +#else + #define JSON_BUFFER_SIZE 24576 +#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 + #define WLED_MAX_NODES 24 +#else + #define WLED_MAX_NODES 150 +#endif + +//this is merely a default now and can be changed at runtime +#ifndef LEDPIN +#if defined(ESP8266) || (defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_PSRAM)) || defined(CONFIG_IDF_TARGET_ESP32C3) + #define LEDPIN 2 // GPIO2 (D4) on Wemod D1 mini compatible boards +#else + #define LEDPIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards +#endif +#endif + +#ifdef WLED_ENABLE_DMX +#if (LEDPIN == 2) + #undef LEDPIN + #define LEDPIN 1 + #warning "Pin conflict compiling with DMX and LEDs on pin 2. The default LED pin has been changed to pin 1." +#endif +#endif + +#ifndef DEFAULT_LED_COUNT + #define DEFAULT_LED_COUNT 30 +#endif + +#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 hardwarea 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 hardwarea 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 new file mode 100644 index 00000000..ff41fa6e --- /dev/null +++ b/wled00/data/404.htm @@ -0,0 +1,47 @@ + + + + + + + Not found + + + + +

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 00000000..5a8c801e --- /dev/null +++ b/wled00/data/cpal/cpal.htm @@ -0,0 +1,689 @@ + + + + + + + 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/dmxmap.htm b/wled00/data/dmxmap.htm index 23e056cb..25953b0e 100644 --- a/wled00/data/dmxmap.htm +++ b/wled00/data/dmxmap.htm @@ -16,11 +16,7 @@ } DMXMap = ""; for (i=0;i<512;i++) { - isstart = ""; - if ((i+1) % 10 == 0) { - isstart="S" - } - DMXMap += "
" + String(i+1) + "
" + dmxlabels[dmxchans[i]] + "
"; + DMXMap += "
" + String(i+1) + "
" + dmxlabels[dmxchans[i]] + "
"; } document.getElementById("map").innerHTML = DMXMap; } diff --git a/wled00/data/icons-ui/Read Me.txt b/wled00/data/icons-ui/Read Me.txt new file mode 100644 index 00000000..8491652f --- /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 00000000..39b8991d --- /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 00000000..6f45f1c4 --- /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 00000000..0416231f --- /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 00000000..cc389b3a 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 00000000..d13b37ac 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 00000000..5af8516b --- /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 00000000..59982557 --- /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 new file mode 100644 index 00000000..049e6f03 --- /dev/null +++ b/wled00/data/index.css @@ -0,0 +1,1580 @@ +@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: #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)); + --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 14px 10px 14px; + --bbp: 9px 0 7px 0; + --bhd: none; + --sgp: "block"; + --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; +} + +.labels { + margin: 0; + padding: 8px 0 2px 0; +} + +#namelabel { + position: fixed; + bottom: calc(var(--bh) + 6px); + 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 { + padding: 4px; +} + +.wrapper { + position: fixed; + top: 0; + left: 0; + right: 0; + background: var(--c-tb); + z-index: 1; +} + +.icons { + font-family: 'WIcons'; + font-style: normal; + 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; +} + +.huge { + font-size: 42px; +} + +.segt, .plentry TABLE { + table-layout: fixed; + width: 100%; +} + +.segt TD { + padding: 2px 0 !important; + text-align: center; + /*text-transform: uppercase;*/ +} +.segt TD, .plentry TD { + font-size: 13px; + padding: 0; + vertical-align: middle; +} + +.keytd { + text-align: left; +} + +.valtd { + text-align: right; +} + +.valtd i { + font-size: small; +} + +.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); +} + +.g-icon { + font-style: normal; + position: absolute; + 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; + top: 8px; + left: 12px; + width: 24px; + height: 24px; +} + +.clear-icon { + position: absolute; + top: 8px; + right: 9px; + cursor: pointer; +} + +.flr { + 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); +} + +.il { + display: inline-block; + vertical-align: middle; +} + +#liveview { + height: 4px; + 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); +} + +.bot { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + background-color: var(--c-tb); +} + +.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; +} + +.bot button { + padding: var(--bbp); + width:25%; + 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 { + --n: 1; + width: 100%; + width: calc(var(--n)*100%); + height: calc(100% - var(--tp) - var(--bh)); + margin-top: var(--tp); + transform: translate(calc(var(--i, 0)/var(--n)*-100%)); + overscroll-behavior: none; +} + +.tabcontent { + float: left; + position: relative; + width: 100%; + width: calc(100%/var(--n)); + box-sizing: border-box; + border: 0px; + overflow: auto; + height: 100%; + overscroll-behavior: none; + padding: 0 4px; + -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 } + +.tab-label { + margin: 0 0 -5px 0; + padding-bottom: 4px; + display: var(--bhd); +} + +.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; +} + +.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; + 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; + overflow: hidden; +} + +/* 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; +} + +.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 { + margin-top: 10px; +} + +#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: 22px; + 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; +} + +.close { + position: -webkit-sticky; + position: sticky; + top: 0; + float: right; +} + +#info, #nodes { + z-index: 4; +} + +#rover { + z-index: 3; +} + +#ndlt { + margin: 12px 0; +} + +#roverstar { + position: fixed; + top: calc(var(--th) + 5px); + left: 1px; + cursor: pointer; +} + +#connind { + position: fixed; + bottom: calc(var(--bh) + 5px); + left: 4px; + padding: 5px; + border-radius: 5px; + background-color: #a90; + z-index: -2; +} + +#info .slider { + max-width: 200px; + min-width: 145px; + float: right; + margin: 0; +} +#info .sliderwrap { + width: 200px; +} + +#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; + margin: 0 auto; +} + +#info #imgw { + margin: 8px auto; +} + +#lv { + max-width: 600px; + display: inline-block; +} + +#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;} +} + +.sliderdisplay { + content:''; + position: absolute; + top: 12px; left: 8px; right: 8px; + height: 5px; + background: var(--c-4); + border-radius: 16px; + pointer-events: none; + z-index: -1; + --bg: var(--c-f); +} + +#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: 24px; + position: absolute; + 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; +} + +output.sliderbubbleshow { + visibility: visible; + opacity: 1; +} + +input[type=range] { + -webkit-appearance: none; + width: 100%; + padding: 0; + margin: 0; + background-color: transparent; + cursor: pointer; +} + +input[type=range]:focus { + outline: none; +} +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 30px; + cursor: pointer; + background: transparent; +} +input[type=range]::-webkit-slider-thumb { + height: 16px; + width: 16px; + border-radius: 50%; + background: var(--c-f); + cursor: pointer; + -webkit-appearance: none; + margin-top: 7px; +} +input[type=range]::-moz-range-track { + width: 100%; + height: 30px; + background-color: rgba(0, 0, 0, 0); +} +input[type=range]::-moz-range-thumb { + border: 0px solid rgba(0, 0, 0, 0); + height: 16px; + width: 16px; + border-radius: 50%; + background: var(--c-f); + transform: translateY(5px); +} +#Colors input[type=range]::-webkit-slider-thumb { + height: 18px; + width: 18px; + border: 2px solid var(--c-1); + margin-top: 5px; +} +#Colors input[type=range]::-moz-range-thumb { + border: 2px solid var(--c-1); +} + +#Colors .sliderwrap { + margin: 4px 0 0; +} + +/* Dynamically hide brightness slider label */ +.hd { + display: var(--bhd); +} + +#briwrap { + min-width: 267px; + float: right; + margin-top: var(--bmt); +} + +#picker { + margin-top: 8px !important; + max-width: 260px; +} + +/* buttons */ +.btn { + padding: 8px; + margin: 10px 4px; + width: 230px; + font-size: 19px; + color: var(--c-d); + cursor: pointer; + border-radius: 25px; + 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); +} +#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*/; +} +.btn-s { + width: 100%; + margin: 0; +} +.btn-icon { + margin: -4px 4px -1px 0; + vertical-align: middle; + display: inline-block; +} +.btn-n { + width: 230px; + margin: 0 8px 0 0; +} +.btn-p { + 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; +} + +#segutil .btn-s, #segutil2 .btn-s, #putil .btn-s { + background-color: var(--c-3); + border: 1px solid var(--c-3); +} + +.btn-pl-del, .btn-pl-add { + margin: 0; + white-space: nowrap; +} + +/* Quick color select wrapper div */ +#qcs-w { + margin-top: 10px; +} + +/* Quick color select buttons */ +.qcs { + margin: 2px; + border-radius: 14px; + display: inline-block; + width: 28px; + height: 28px; + line-height: 28px; +} + +/* Quick color select Black button (has white border) */ +.qcsb { + width: 26px; + height: 26px; + line-height: 26px; + border: 1px solid #fff; +} + +/* Hex color input wrapper div */ +#hexw { + margin-top: 5px; +} + +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; +} +.cl { + background-color: #000; +} +select.sel-p, select.sel-pl, select.sel-ple { + margin: 5px 0; + width: 100%; + height: 40px; +} +div.sel-p { + position: relative; +} +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.sel-sg { + margin: 5px 0; + height: 40px; +} +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-2); + border-radius: 10px; + padding: 8px; + /*margin: 6px 6px 6px 0;*/ + font-size: 19px; + transition: background-color 0.2s; + outline: none; + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; +} + +input[type=number] { + text-align: right; + width: 50px; +} + +input[type=text] { + text-align: center; +} + +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; +} + +#hexw input[type=text] { + width: 6em; +} + +input[type=text].ptxt { + width: calc(100% - 24px); +} + +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; +} + +.apitxt { + height: 7em; +} + +::selection { + background: var(--c-b); +} + +.ptxt { + margin: -1px 4px 8px !important; +} + +.stxt { + width: 50px !important; +} + +.segname, .pname, .bname { + white-space: nowrap; + text-align: center; + overflow: hidden; + 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%; +} + +.pname { + top: 1px; +} +.plname { + top:0; +} + +/* preset id number */ +.pid { + position: absolute; + top: 8px; + left: 12px; + font-size: 16px; + text-align: center; + color: var(--c-b); +} + +.newseg { + cursor: default; +} + +.ic { + padding: 6px 0 0 0; +} + +.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); +} + +.psts { + color: var(--c-f); + margin: 4px; +} + +.pwr { + color: var(--c-6); + cursor: pointer; +} + +.act { + color: var(--c-f); +} + +.del { + position: absolute; + bottom: 8px; + right: 8px; +} + +.frz { + left: 10px; + position: absolute; + top: 8px; + cursor: pointer; + z-index: 1; +} + +/* radiobuttons and checkmarks */ +.check, .radio { + display: block; + position: relative; + cursor: pointer; +} + +.revchkl { + padding: 4px 0px 0px 35px; + margin-bottom: 0px; + margin-top: 8px; +} + +TD .revchkl { + padding: 0 0 0 32px; + margin-top: 0; +} + +.check input, .radio input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +.checkmark, .radiomark { + position: absolute; + height: 24px; + width: 24px; + 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; +} + +.checkmark { + border-radius: 10px; +} + +.radio:hover input ~ .radiomark, +.check:hover input ~ .checkmark { + background-color: var(--c-5); +} + +.checkmark:after, .radiomark:after { + content: ""; + position: absolute; + display: none; +} + +.check .checkmark:after { + 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: 14px; + height: 14px; + top: 50%; + left: 50%; + margin: -7px; + border-radius: 50%; + background: var(--c-f); +} + +TD .checkmark, TD .radiomark { + top: -6px; +} + +.h { + font-size: 13px; + text-align: center; + color: var(--c-b); +} + +.bp { + margin-bottom: 8px; +} + +/* segment & preset wrapper */ +.seg, .pres { + background-color: var(--c-2); + /*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; + border-radius: 21px; +} + +.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%); +} + +.lbl-l { + font-size: 13px; + text-align: center; + padding: 4px 0; +} + +.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; + line-height: 24px; +} + +/* list item */ +.lstI { + 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); +} + +#segutil .lstI { + margin-top: 0; +} + +/* selected item/element */ +.selected { /* has to be after .lstI since !important is not ok */ + background: var(--c-4); +} + +#segcont .seg:hover:not([class*="expanded"]), +.lstI:hover:not([class*="expanded"]) { + background: var(--c-5); +} + +.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); +} + +#pcont .selected:not([class*="expanded"]) { + bottom: 52px; + top: 42px; +} + +#fxlist .lstI.selected { + top: 84px; +} + +#fxlist .lstI.sticky { + top: 42px; +} + +#pallist .lstI.selected { + top: 84px; +} + +#pallist .lstI.sticky { + top: 42px; +} + +/* list item content */ +.lstIcontent { + padding: 9px 0 7px; + position: relative; +} + +/* list item name (for sorting) */ +.lstIname { + white-space: nowrap; + text-overflow: ellipsis; + -webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ + filter: grayscale(100%); +} + +/* list item palette preview */ +.lstIprev { + width: 100%; + height: 6px; + position: absolute; + bottom: 0; + left: 0; + z-index: -1; +} + +/* find/search element */ +.fnd { + position: relative; +} + +.fnd input[type="text"] { + display: block; + width: 100%; + box-sizing: border-box; + padding: 8px 40px 8px 44px; + margin: 5px auto 0; + text-align: left; + 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%); +} + +.fnd input[type="text"]:focus { + background-color: var(--c-4); +} + +.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; +} + +.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*/; +} + +/* 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: 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 { + text-align: center; +} + +.po2 { + display: none; + margin-top: 8px; +} + +.pwarn { + color: red; +} + +/* horizontal divider (playlist entries) */ +.hrz { + width: auto; + height: 2px; + background-color: var(--c-b); + margin: 3px 0; +} + +::-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: 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) { + #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: 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; + } +} + +@media all and (min-width: 420px) and (max-width: 639px) { + .top button { + width: 14.28%; + padding: 8px 0 4px 0; + } +} + +@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 6c899e6b..0cf48d6e 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -1,5 +1,5 @@ - + @@ -7,887 +7,52 @@ WLED - - + + - +
Loading WLED UI...
@@ -900,18 +65,20 @@ input[type=number]::-webkit-outer-spin-button { - - - + + + +

Brightness

-
- +
+
+
@@ -921,131 +88,259 @@ input[type=number]::-webkit-outer-spin-button {
-
+
- -
-

-
- -
-

-
- -
-

+ +
+
+ Hue
-
-

White channel

+
- + +
+
+ Saturation +
+
+
+
+ Value/Brightness +
+
+
+ +
+
+ Kelvin/Temperature +
+
+ +
+
+ +
+
+ Red channel +
+
+
+ +
+
+ Green channel +
+
+
+ +
+
+ Blue channel +
+
+
+ +
+ +
+
+ White channel +
+
+ +
+ +
+
+ White balance
-
-
-
-
-
-

-
-
-
-
-
R
+
+
+
+
+
+

+
+
+
+
+
R
- - - + + +
+

- - + + +
-

Color palette

-
- - +

Color palette

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

Effect speed

-
- -
- -
+
+

Effect mode

+
+ + + +
+
+
+ +
-

Effect intensity

-
- -
- -
+
+
+ + + + + + +
+
+ +
+ +
+
+ + Effect speed +
+
+ +
+ +
+
+ + Effect intensity +
+
+ +
+ +
+
+ + Custom 1 +
+
+ +
+ +
+
+ + Custom 2 +
+
+ +
+ +
+
+ + Custom 3 +
+
+ + +
-

Effect mode

-
- -
-
- Loading... -
-
- +
Loading...
-
- +
- +
+

Transition:  s

+

-
-

Load from slot

- - - -
- - - -
- - - -
- - - -
-
- Slot 16 can save all segments.

-
- First preset:
- Last preset:
- Time per preset: s
- Transition: s +
+
+
+

Presets

+
+ + + +
+
+ Loading... +
+
+
@@ -1053,22 +348,39 @@ input[type=number]::-webkit-outer-spin-button { - +
-
-
+
+
+
Loading...

+
+ + + + +
+
+ Made with ❤︎ by Aircoookie and the WLED community +
+ + + +
- + - \ No newline at end of file + diff --git a/wled00/data/index.js b/wled00/data/index.js new file mode 100644 index 00000000..145ed2f2 --- /dev/null +++ b/wled00/data/index.js @@ -0,0 +1,2927 @@ +//page js +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 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, wW; +var tr = 7; +var d = document; +var palettesData; +var fxdata = []; +var pJson = {}, eJson = {}, lJson = {}; +var plJson = {}; // array of playlists +var pN = "", pI = 0, pNum = 0; +var pmt = 1, pmtLS = 0, pmtLast = 0; +var lastinfo = {}; +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, segpwr:false, segexp:false, + css:true, hdays:false, fxdef:true} +}; +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 + [2025,3,20,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"], + [0,6,4,1,"https://initiate.alphacoders.com/download/wallpaper/516792/images/jpg/510921363292536"], // 4th of July + [0,0,1,1,"https://initiate.alphacoders.com/download/wallpaper/1198800/images/jpg/2522807481585600"] // new year +]; + +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);} + +// 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 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) {return 0.2126*parseInt(a.r) + 0.7152*parseInt(a.g) + 0.0722*parseInt(a.b);} + +// sets background of color slot selectors +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 (hasRGB && !hasWhite) w = 0; + cs.style.background = `rgb(${w},${w},${w})`; + if (!hasShadow) cs.style.color = w > 127 ? "#000":"#fff"; + } +} + +function applyCfg() +{ + cTheme(cfg.theme.base === "light"); + var bg = cfg.theme.color.bg; + if (bg) sCol('--c-1', bg); + 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"); // 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() +{ + cfg.comp.colors.hex = !cfg.comp.colors.hex; + applyCfg(); +} + +function tglTheme() +{ + cfg.theme.base = (cfg.theme.base === "light") ? "dark":"light"; + applyCfg(); +} + +function tglLabels() +{ + cfg.comp.labels = !cfg.comp.labels; + 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','#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 == "" || iUrl==="https://picsum.photos/1920/1080") { + var today = new Date(); + for (var h of (hol||[])) { + var yr = h[0]==0 ? today.getFullYear() : h[0]; + var hs = new Date(yr,h[1],h[2]); + var he = new Date(hs); + he.setDate(he.getDate() + h[3]); + if (today>=hs && today<=he) img.src = h[4]; + } + } + 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 (!gId(cId)) // check if element exists + { + var h = d.getElementsByTagName('head')[0]; + var l = d.createElement('link'); + l.id = cId; + l.rel = 'stylesheet'; + l.type = 'text/css'; + l.href = getURL('/skin.css'); + l.media = 'all'; + h.appendChild(l); + } +} + +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(getURL("/holidays.json"), { // may be loaded from external source + method: 'get' + }) + .then((res)=>{ + //if (!res.ok) showErrorToast(); + return res.json(); + }) + .then((json)=>{ + if (Array.isArray(json)) hol = json; + //TODO: do some parsing first + }) + .catch((e)=>{ + console.log("No array of holidays in holidays.json. Defaults loaded."); + }) + .finally(()=>{ + loadBg(cfg.theme.bg.url); + }); + } else + loadBg(cfg.theme.bg.url); + if (cfg.comp.css) loadSkinCSS('skinCss'); + + selectSlot(0); + updateTablinks(0); + pmtLS = localStorage.getItem('wledPmt'); + + // 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(); + 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); + } +} + +function updateTablinks(tabI) +{ + var tablinks = gEBCN("tablinks"); + for (var i of tablinks) i.classList.remove('active'); + if (pcMode) return; + tablinks[tabI].classList.add('active'); +} + +function openTab(tabI, force = false) +{ + if (pcMode && !force) return; + iSlide = tabI; + _C.classList.toggle('smooth', false); + _C.style.setProperty('--i', iSlide); + updateTablinks(tabI); +} + +var timeout; +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.classList.add(error ? 'error':'show'); + clearTimeout(timeout); + x.style.animation = 'none'; + timeout = setTimeout(()=>{ x.classList.remove('show'); }, 2900); + if (error) console.log(text); +} + +function showErrorToast() +{ + showToast('Connection to light failed!', true); +} + +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) +{ + 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 getLowestUnusedP() +{ + var l = 1; + for (var key in pJson) if (key == l) l++; + if (l > 250) l = 250; + return l; +} + +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) +{ + 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 presetError(empty) +{ + var hasBackup = false; var bckstr = ""; + try { + bckstr = localStorage.getItem("wledP"); + if (bckstr.length > 10) hasBackup = true; + } catch (e) {} + + var cn = `
`; + if (empty) + cn += `You have no presets yet!`; + else + cn += `Sorry, there was an issue loading your presets!`; + + if (hasBackup) { + cn += `

`; + if (empty) + cn += `However, there is backup preset data of a previous installation available.
+ (Saving a preset will hide this and overwrite the backup)`; + else + cn += `Here is a backup of the last known good state:`; + cn += `
+ `; + } + cn += `
`; + gId('pcont').innerHTML = cn; + if (hasBackup) gId('bck').value = bckstr; +} + +function loadPresets(callback = null) +{ + // 1st boot (because there is a callback) + if (callback && pmt == pmtLS && pmt > 0) { + // we have a copy of the presets in local storage and don't need to fetch another one + populatePresets(true); + pmtLast = pmt; + callback(); + return; + } + + // afterwards + if (!callback && pmt == pmtLast) return; + + fetch(getURL('/presets.json'), { + method: 'get' + }) + .then(res => { + if (res.status=="404") return {"0":{}}; + //if (!res.ok) showErrorToast(); + return res.json(); + }) + .then(json => { + pJson = json; + pmtLast = pmt; + populatePresets(); + }) + .catch((e)=>{ + //showToast(e, true); + presetError(false); + }) + .finally(()=>{ + if (callback) setTimeout(callback,99); + }); +} + +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) { + pQL.sort((a,b) => (a[0]>b[0])); + cn += `

Quick load

`; + for (var key of (pQL||[])) { + 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); + 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; + if (pNum > 0) { + if (pmtLS != pmt && pmt != 0) { + localStorage.setItem("wledPmt", pmt); + pJson["0"] = {}; + localStorage.setItem("wledP", JSON.stringify(pJson)); + } + pmtLS = pmt; + } 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/1024; + 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} +${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("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 inst of (s.seg||[])) { + segCount++; + + let i = parseInt(inst.id); + if (i == lowestUnused) lowestUnused = i+1; + if (i > lSeg) lSeg = i; + + 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(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"); + 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'); + } +} + +function populateEffects() +{ + var effects = eJson; + var html = ""; + + effects.shift(); // temporary 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" + }); + + 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); + } + } + + gId('fxlist').innerHTML=html; +} + +function populatePalettes() +{ + lJson.shift(); // temporary remove default + lJson.sort((a,b) => (a[1]).localeCompare(b[1])); + lJson.unshift([0,"Default"]); + + var html = ""; + for (let pa of lJson) { + html += generateListItemHtml( + 'palette', + pa[0], + pa[1], + 'setPalette', + `
` + ); + } + 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 (var pal of (palettes||[])) { + let lP = pal.querySelector('.lstIprev'); + if (lP) { + lP.style = genPalPrevCss(pal.dataset.id); + } + } +} + +function genPalPrevCss(id) +{ + if (!palettesData) return; + + var paletteData = palettesData[id]; + + 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 e = paletteData[j]; + let r, g, b; + let index = false; + 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 { + 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 = 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 = '', effectPar = '') +{ + return `
`+ + ``+ + 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) +{ + 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 o of n.nodes) { + if (o.name) { + 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 += ` + ${inforow("Current instance:",i.name)} + ${urows} +
`; + 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(100); + populateNodes(lastinfo, json); + }) + .catch((e)=>{ + showToast(e, true); + }); +} + +// update the 'sliderdisplay' background div of a slider for a visual indication of slider position +function updateTrail(e) +{ + if (e==null) return; + 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 +function toggleBubble(e) +{ + var b = e.target.parentNode.parentNode.getElementsByTagName('output')[0]; + b.classList.toggle('sliderbubbleshow'); +} + +// updates segment length upon input of segment values +function updateLen(s) +{ + 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`; + } else if (len == 1) { + out = "1 LED"; + } + + if (gId(`seg${s}grp`) != null) + { + 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]"; + + gId(`seg${s}len`).innerHTML = out; +} + +// updates background color of currently selected preset +function updatePA() +{ + 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 = 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() +{ + 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')); + + var ccfg = cfg.comp.colors; + 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 == 5) 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) +{ + 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; + gId('lv').innerHTML = `WLED is receiving live ${i.lm} data${sour}`; + gId('roverstar').style.display = (i.live && s.lor) ? "block":"none"; +} + +function cmpP(a, b) +{ + if (!a[1].n) return (a[0] > 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); + if ((c>47 && c<58) || (c>64 && c<91) || (c>96 && c<123) || c>255) x = '='; else x = '>'; + if ((d>47 && d<58) || (d>64 && d<91) || (d>96 && d<123) || d>255) y = '='; else y = '>'; + const n = (a[1].playlist ? '<' : x) + a[1].n; + return n.localeCompare((b[1].playlist ? '<' : y) + b[1].n, undefined, {numeric: true}); +} + +function makeWS() { + 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 = (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(); + 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)=>{ + 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) +{ + if (!s) return false; + if (s.success) return true; // no data to process + + 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; + 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) { + selc = i; + sellvl = 1; + } + if (s.seg[i].sel) { + 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); + 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); + } + if (!i) { + showToast('No Segments!', true); + updateUI(); + return true; + } + + if (s.seg.length>2) d.querySelectorAll(".pop").forEach((e)=>{e.classList.remove("hide");}); + + 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]); + } + selectSlot(csel); + if (i.cct != null && i.cct>=0) gId("sliderA").value = i.cct; + + 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 (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); + } + + 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 ; seperates slider controls (left) from color controls (middle) and palette control (right) +// - if left, middle or right is empty no controls are shown +// - a , seperates 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+i{ + let top = parseInt(getComputedStyle(gId("sliders")).height); + top += 5; + let sel = d.querySelector('#fxlist .selected'); + if (sel) sel.style.bottom = top + "px"; // we will need to remove this when unselected (in setFX()) + },750); + // set html color items on/off + var cslLabel = ''; + var sep = ''; + var cslCnt = 0, oCsel = csel; + for (let i=0; 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 = ', '; + } + } + 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 + } + } + 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=null) +{ + gId('connind').style.backgroundColor = "var(--c-y)"; + if (command && !reqsLegal) return; // stop post requests from chrome onchange event on page restore + if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000); + var req = null; + var useWs = (ws && ws.readyState === WebSocket.OPEN); + var type = command ? 'post':'get'; + if (command) { + command.v = true; // force complete /json/si API response + command.time = Math.floor(Date.now() / 1000); + var t = gId('tt'); + if (t.validity.valid && command.transition==null) { + var tn = parseInt(t.value*10); + if (tn != tr) command.transition = tn; + } + req = JSON.stringify(command); + if (req.length > 1340) useWs = false; // do not send very long requests over websocket + if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; // esp8266 can only handle 500 bytes + }; + + if (useWs) { + ws.send(req?req:'{"v":true}'); + return; + } + + fetch(getURL('/json/si'), { + method: type, + headers: { + "Content-type": "application/json; charset=UTF-8" + }, + body: req + }) + .then(res => { + clearTimeout(jsonTimeout); + jsonTimeout = null; + if (!res.ok) showErrorToast(); + return res.json(); + }) + .then(json => { + lastUpdate = new Date(); + clearErrorToast(3000); + gId('connind').style.backgroundColor = "var(--c-g)"; + if (!json) { showToast('Empty response', true); return; } + if (json.success) return; + if (json.info) { + let i = json.info; + parseInfo(i); + populatePalettes(i); + if (isInfo) populateInfo(i); + } + 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((e)=>{ + showToast(e, true); + }); +} + +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() +{ + nlA = !nlA; + if (nlA) + { + showToast(`Timer active. Your light will turn ${nlTar > 0 ? "on":"off"} ${nlMode ? "over":"after"} ${nlDur} minutes.`); + } else { + showToast('Timer deactivated.'); + } + var obj = {"nl": {"on": nlA}}; + requestJson(obj); +} + +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.'); + var obj = {"udpn": {"send": syncSend}}; + if (syncTglRecv) obj.udpn.recv = syncSend; + requestJson(obj); +} + +function toggleLiveview() +{ + if (isInfo && isM) toggleInfo(); + if (isNodes && isM) toggleNodes(); + isLv = !isLv; + 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() +{ + if (isNodes) toggleNodes(); + if (isLv && isM) toggleLiveview(); + isInfo = !isInfo; + if (isInfo) requestJson(); + gId('info').style.transform = (isInfo) ? "translateY(0px)":"translateY(100%)"; + gId('buttonI').className = (isInfo) ? "active":""; +} + +function toggleNodes() +{ + if (isInfo) toggleInfo(); + if (isLv && isM) toggleLiveview(); + isNodes = !isNodes; + if (isNodes) loadNodes(); + gId('nodes').style.transform = (isNodes) ? "translateY(0px)":"translateY(100%)"; + gId('buttonNodes').className = (isNodes) ? "active":""; +} + +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
` + + '
' + + `` + + '
' + + '
'; +} + +function makePlSel(el, incPl=false) +{ + var plSelContent = ""; + delete pJson["0"]; // remove filler preset + var arr = Object.entries(pJson); + for (var a of arr) { + var n = a[1].n ? a[1].n : "Preset " + a[0]; + 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 = gId(`ple${p}`); + if (!plEDiv) return; + 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 = 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; + else plJson[p].ps[i.dataset.index] = parseInt(i.value); + } + } +} + +// 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) +{ + if (plJson[p].ps.length < 2) return; + plJson[p].ps.splice(i,1); + plJson[p].dur.splice(i,1); + plJson[p].transition.splice(i,1); + refreshPlE(p); +} + +function plePs(p,i,field) +{ + plJson[p].ps[i] = parseInt(field.value); +} + +function pleDur(p,i,field) +{ + if (field.validity.valid) + plJson[p].dur[i] = Math.floor(field.value*10); +} + +function pleTr(p,i,field) +{ + if (field.validity.valid) + plJson[p].transition[i] = Math.floor(field.value*10); +} + +function plR(p) +{ + var pl = plJson[p]; + pl.r = gId(`pl${p}rtgl`).checked; + if (gId(`pl${p}rptgl`).checked) { // infinite + pl.repeat = 0; + delete pl.end; + gId(`pl${p}o1`).style.display = "none"; + } else { + 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) +{ + 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 = +`
+ +
+
Repeat 0?rep:1}> times
+
End preset:
+
+
+
`; + } else { + content = +` + +`; + if (Array.isArray(lastinfo.maps) && lastinfo.maps.length>1) { + content += `
Ledmap: 
"; + } + } + + return ` +
Quick load label:
+
(leave empty for no Quick load button)
+
+ +
+
API command
+
${content}
+
Save to ID 0)?i:getLowestUnusedP()}>
+
+ + ${(i>0)?' +
+
+${(i>0)? ('
ID ' +i+ '
'):""}`; +} + +function makePUtil() +{ + let p = gId('putil'); + p.classList.remove('staybot'); + p.classList.add('pres'); + p.innerHTML = `
${makeP(0)}
`; + let pTx = gId('p0txt'); + pTx.focus(); + pTx.value = eJson.find((o)=>{return o.id==selectedFx}).name; + pTx.select(); + p.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + gId('psFind').classList.remove('staytop'); +} + +function makePlEntry(p,i) +{ + return `
+
+ + + + + + + + + + + + + + + +
+
+
DurationTransition#${i+1}
ss
+
`; +} + +function makePlUtil() +{ + if (pNum < 2) { + showToast("You need at least 2 presets to make a playlist!"); //return; + } + let p = gId('putil'); + p.classList.remove('staybot'); + p.classList.add('pres'); + p.innerHTML = `
${makeP(0,true)}
`; + refreshPlE(0); + gId('p0txt').focus(); + p.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + gId('psFind').classList.remove('staytop'); +} + +function resetPUtil() +{ + gId('psFind').classList.add('staytop'); + let p = gId('putil'); + p.classList.add('staybot'); + p.classList.remove('pres'); + p.innerHTML = `` + + ``; +} + +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 tglSegn(s) +{ + let t = gId(s<100?`seg${s}t`:`p${s-100}txt`); + if (t) { + t.classList.toggle('show'); + t.focus(); + t.select(); + } + event.preventDefault(); + event.stopPropagation(); +} + +function selSegAll(o) +{ + var obj = {"seg":[]}; + for (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({"id":i,"sel":o.checked}); + requestJson(obj); +} + +function selSegEx(s) +{ + var obj = {"seg":[]}; + for (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({"id":i,"sel":(i==s)}); + obj.mainseg = s; + requestJson(obj); +} + +function selSeg(s) +{ + var sel = gId(`seg${s}sel`).checked; + var obj = {"seg": {"id": s, "sel": sel}}; + requestJson(obj); +} + +function selGrp(g) +{ + event.preventDefault(); + event.stopPropagation(); + var sel = gId(`segcont`).querySelectorAll(`div[data-set="${g}"]`); + var obj = {"seg":[]}; + for (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({"id":i,"sel":false}); + if (sel) for (let s of sel||[]) { + let i = parseInt(s.id.substring(3)); + obj.seg[i] = {"id":i,"sel":true}; + } + if (obj.seg.length) requestJson(obj); +} + +function rptSeg(s) +{ + //TODO: 2D support + var name = gId(`seg${s}t`).value; + var start = parseInt(gId(`seg${s}s`).value); + var stop = parseInt(gId(`seg${s}e`).value); + if (stop == 0) {return;} + var rev = gId(`seg${s}rev`).checked; + var mi = gId(`seg${s}mi`).checked; + var sel = gId(`seg${s}sel`).checked; + var pwr = gId(`seg${s}pwr`).classList.contains('act'); + var obj = {"seg": {"id": s, "n": name, "start": start, "stop": (cfg.comp.seglen?start:0)+stop, "rev": rev, "mi": mi, "on": pwr, "bri": parseInt(gId(`seg${s}bri`).value), "sel": sel}}; + if (gId(`seg${s}grp`)) { + var grp = parseInt(gId(`seg${s}grp`).value); + var spc = parseInt(gId(`seg${s}spc`).value); + var ofs = parseInt(gId(`seg${s}of` ).value); + obj.seg.grp = grp; + obj.seg.spc = spc; + obj.seg.of = ofs; + } + obj.seg.rpt = true; + expand(s); + requestJson(obj); +} + +function setSeg(s) +{ + var name = gId(`seg${s}t`).value; + let sX = gId(`seg${s}s`); + let eX = gId(`seg${s}e`); + var start = parseInt(sX.value); + var stop = parseInt(eX.value) + (cfg.comp.seglen?start:0); + if (startsX.max) {sX.value=sX.min; return;} // prevent out of bounds + if (stopeX.max) {eX.value=eX.max; return;} // prevent out of bounds + if ((cfg.comp.seglen && stop == 0) || (!cfg.comp.seglen && stop <= start)) {delSeg(s); return;} + var obj = {"seg": {"id": s, "n": name, "start": start, "stop": stop}}; + if (isM && startsY.max) {sY.value=sY.min; return;} // prevent out of bounds + if (stopYeY.max) {eY.value=eY.max; return;} // prevent out of bounds + obj.seg.startY = startY; + obj.seg.stopY = stopY; + } + let g = gId(`seg${s}grp`); + if (g) { // advanced options, not present in new segment dialog (makeSeg()) + let grp = parseInt(g.value); + let spc = parseInt(gId(`seg${s}spc`).value); + let ofs = parseInt(gId(`seg${s}of` ).value); + obj.seg.grp = grp; + obj.seg.spc = spc; + obj.seg.of = ofs; + if (isM && gId(`seg${s}tp`)) obj.seg.tp = gId(`seg${s}tp`).checked; + } + resetUtil(); // close add segment dialog just in case + requestJson(obj); +} + +function delSeg(s) +{ + if (segCount < 2) { + showToast("You need to have multiple segments to delete one!"); + return; + } + segCount--; + var obj = {"seg": {"id": s, "stop": 0}}; + requestJson(obj); +} + +function setRev(s) +{ + var rev = gId(`seg${s}rev`).checked; + var obj = {"seg": {"id": s, "rev": rev}}; + requestJson(obj); +} + +function setRevY(s) +{ + var rev = gId(`seg${s}rY`).checked; + var obj = {"seg": {"id": s, "rY": rev}}; + requestJson(obj); +} + +function setMi(s) +{ + var mi = gId(`seg${s}mi`).checked; + var obj = {"seg": {"id": s, "mi": mi}}; + requestJson(obj); +} + +function setMiY(s) +{ + var mi = gId(`seg${s}mY`).checked; + var obj = {"seg": {"id": s, "mY": mi}}; + requestJson(obj); +} + +function setM12(s) +{ + var value = gId(`seg${s}m12`).selectedIndex; + var obj = {"seg": {"id": s, "m12": value}}; + requestJson(obj); +} + +function setSi(s) +{ + var value = gId(`seg${s}si`).selectedIndex; + var obj = {"seg": {"id": s, "si": value}}; + requestJson(obj); +} + +function setTp(s) +{ + var tp = gId(`seg${s}tp`).checked; + var obj = {"seg": {"id": s, "tp": tp}}; + requestJson(obj); +} + +function setGrp(s, g) +{ + event.preventDefault(); + event.stopPropagation(); + var obj = {"seg": {"id": s, "set": g}}; + requestJson(obj); +} + +function setSegPwr(s) +{ + var pwr = gId(`seg${s}pwr`).classList.contains('act'); + var obj = {"seg": {"id": s, "on": !pwr}}; + requestJson(obj); +} + +function setSegBri(s) +{ + var obj = {"seg": {"id": s, "bri": parseInt(gId(`seg${s}bri`).value)}}; + requestJson(obj); +} + +function tglFreeze(s=null) +{ + var obj = {"seg": {"frz": "t"}}; // toggle + if (s!==null) { + obj.seg.id = s; + // if live segment, enter live override (which also unfreezes) + if (lastinfo && s==lastinfo.liveseg && lastinfo.live) obj = {"lor":1}; + } + requestJson(obj); +} + +function setFX(ind = null) +{ + if (ind === null) { + ind = parseInt(d.querySelector('#fxlist input[name="fx"]:checked').value); + } else { + d.querySelector(`#fxlist input[name="fx"][value="${ind}"]`).checked = true; + } + var obj = {"seg": {"fx": parseInt(ind), "fxdef": cfg.comp.fxdef}}; // fxdef sets effect parameters to default values + requestJson(obj); +} + +function setPalette(paletteId = null) +{ + if (paletteId === null) { + paletteId = parseInt(d.querySelector('#pallist input[name="palette"]:checked').value); + } else { + d.querySelector(`#pallist input[name="palette"][value="${paletteId}"]`).checked = true; + } + + 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 setCustom(i=1) +{ + if (i<1 || i>3) return; + var obj = {"seg": {}}; + var val = parseInt(gId(`sliderC${i}`).value); + if (i===3) obj.seg.c3 = val; + else if (i===2) obj.seg.c2 = val; + else obj.seg.c1 = val; + requestJson(obj); +} + +function setOption(i=1, v=false) +{ + if (i<1 || i>3) return; + var obj = {"seg": {}}; + if (i===3) obj.seg.o3 = !(!v); //make sure it is bool + else if (i===2) obj.seg.o2 = !(!v); //make sure it is bool + else obj.seg.o1 = !(!v); //make sure it is bool + requestJson(obj); +} + +function setLor(i) +{ + var obj = {"lor": i}; + requestJson(obj); +} + +function setPreset(i) +{ + var obj = {"ps":i}; + if (!isPlaylist(i) && pJson && pJson[i] && (!pJson[i].win || pJson[i].win.indexOf("Please") <= 0)) { + // we will send the complete preset content as to avoid delay introduced by + // async nature of applyPreset() and having to read the preset from file system. + obj = {"pd":i}; // use "pd" instead of "ps" to indicate that we are sending the preset content directly + Object.assign(obj, pJson[i]); + delete obj.ql; // no need for quick load + delete obj.n; // no need for name + } + if (isPlaylist(i)) obj.on = true; // force on + showToast("Loading preset " + pName(i) +" (" + i + ")"); + requestJson(obj); +} + +function saveP(i,pl) +{ + pI = parseInt(gId(`p${i}id`).value); + if (!pI || pI < 1) pI = (i>0) ? i : getLowestUnusedP(); + if (pI > 250) {alert("Preset ID must be 250 or less."); return;} + pN = gId(`p${i}txt`).value; + if (pN == "") pN = (pl?"Playlist ":"Preset ") + pI; + var obj = {}; + if (!gId(`p${i}cstgl`).checked) { + var raw = gId(`p${i}api`).value; + try { + obj = JSON.parse(raw); + } catch (e) { + obj.win = raw; + if (raw.length < 2) { + gId(`p${i}warn`).innerHTML = "⚠ Please enter your API command first"; + return; + } else if (raw.indexOf('{') > -1) { + gId(`p${i}warn`).innerHTML = "⚠ Syntax error in custom JSON API command"; + return; + } else if (raw.indexOf("Please") == 0) { + gId(`p${i}warn`).innerHTML = "⚠ Please refresh the page before modifying this preset"; + return; + } + } + obj.o = true; + } else { + if (pl) { + obj.playlist = plJson[i]; + obj.on = true; + obj.o = true; + } else { + obj.ib = gId(`p${i}ibtgl`).checked; + obj.sb = gId(`p${i}sbtgl`).checked; + obj.sc = gId(`p${i}sbchk`).checked; + if (gId(`p${i}lmp`) && gId(`p${i}lmp`).value!=="") obj.ledmap = parseInt(gId(`p${i}lmp`).value); + } + } + + obj.psave = pI; obj.n = pN; + var pQN = gId(`p${i}ql`).value; + if (pQN.length > 0) obj.ql = pQN; + + showToast("Saving " + pN +" (" + pI + ")"); + requestJson(obj); + if (obj.o) { + pJson[pI] = obj; + delete pJson[pI].psave; + delete pJson[pI].o; + delete pJson[pI].v; + delete pJson[pI].time; + } else { + pJson[pI] = {"n":pN, "win":"Please refresh the page to see this newly saved command."}; + if (obj.win) pJson[pI].win = obj.win; + if (obj.ql) pJson[pI].ql = obj.ql; + } + populatePresets(); + resetPUtil(); + setTimeout(()=>{pmtLast=0; loadPresets();}, 750); // force reloading of presets +} + +function testPl(i,bt) { + if (bt.dataset.test == 1) { + bt.dataset.test = 0; + bt.innerHTML = "Test"; + stopPl(); + return; + } + bt.dataset.test = 1; + bt.innerHTML = "Stop"; + var obj = {}; + obj.playlist = plJson[i]; + obj.on = true; + requestJson(obj); +} + +function stopPl() { + requestJson({playlist:{}}) +} + +function delP(i) { + var bt = gId(`p${i}del`); + if (bt.dataset.cnf == 1) { + var obj = {"pdel": i}; + requestJson(obj); + delete pJson[i]; + populatePresets(); + gId('putil').classList.add('staybot'); + } else { + bt.style.color = "var(--c-r)"; + bt.innerHTML = "Delete!"; + bt.dataset.cnf = 1; + } +} + +function selectSlot(b) +{ + csel = b; + var cd = gId('csl').children; + for (let i of cd) i.classList.remove('xxs-w'); + cd[b].classList.add('xxs-w'); + setPicker(rgbStr(cd[b].dataset)); + // force slider update on initial load (picker "color:change" not fired if black) + if (cpick.color.value == 0) updatePSliders(); + gId('sliderW').value = parseInt(cd[b].dataset.w); + updateTrail(gId('sliderW')); + redrawPalPrev(); +} + +// set the color from a hex string. Used by quick color selectors +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 = parseInt(gId("csl").children[csel].dataset.w); + if (w > 0) str += w.toString(16); + gId('hexc').value = str; + gId('hexcnf').style.backgroundColor = "var(--c-3)"; + + // update HSV sliders + var c; + let h = cpick.color.hue; + let s = cpick.color.saturation; + let v = cpick.color.value; + + gId("sliderH").value = h; + gId("sliderS").value = s; + gId('sliderV').value = v; + + c = iro.Color.hsvToRgb({"h":h,"s":100,"v":100}); + gId("sliderS").nextElementSibling.style.backgroundImage = 'linear-gradient(90deg, #aaa -15%, rgb('+c.r+','+c.g+','+c.b+'))'; + + c = iro.Color.hsvToRgb({"h":h,"s":s,"v":100}); + gId('sliderV').nextElementSibling.style.backgroundImage = 'linear-gradient(90deg, #000 -15%, rgb('+c.r+','+c.g+','+c.b+'))'; + + // update Kelvin slider + gId('sliderK').value = cpick.color.kelvin; +} + +function hexEnter() +{ + if(event.keyCode == 13) fromHex(); +} + +function segEnter(s) { + if(event.keyCode == 13) setSeg(s); +} + +function fromHex() +{ + var str = gId('hexc').value; + let w = parseInt(str.substring(6), 16); + try { + setPicker("#" + str.substring(0,6)); + } catch (e) { + setPicker("#ffaa00"); + } + gId("csl").children[csel].dataset.w = isNaN(w) ? 0 : w; + setColor(2); +} + +function setPicker(rgb) { + var c = new iro.Color(rgb); + if (c.value > 0) cpick.color.set(c); + else cpick.color.setChannel('hsv', 'v', 0); + updateTrail(gId('sliderR')); + updateTrail(gId('sliderG')); + updateTrail(gId('sliderB')); +} + +function fromH() +{ + cpick.color.setChannel('hsv', 'h', gId('sliderH').value); +} + +function fromS() +{ + cpick.color.setChannel('hsv', 's', gId('sliderS').value); +} + +function fromV() +{ + cpick.color.setChannel('hsv', 'v', gId('sliderV').value); +} + +function fromK() +{ + cpick.color.set({ kelvin: gId('sliderK').value }); +} + +function fromRgb() +{ + var r = gId('sliderR').value; + var g = gId('sliderG').value; + var b = gId('sliderB').value; + setPicker(`rgb(${r},${g},${b})`); + let cd = gId('csl').children; // color slots + cd[csel].dataset.r = r; + cd[csel].dataset.g = g; + cd[csel].dataset.b = b; + setCSL(cd[csel]); +} + +function fromW() +{ + let w = gId('sliderW'); + let cd = gId('csl').children; // color slots + cd[csel].dataset.w = w.value; + setCSL(cd[csel]); + updateTrail(w); +} + +// sr 0: from RGB sliders, 1: from picker, 2: from hex +function setColor(sr) +{ + var cd = gId('csl').children; // color slots + let cdd = cd[csel].dataset; + let w = 0, r,g,b; + if (sr == 1 && isRgbBlack(cdd)) cpick.color.setChannel('hsv', 'v', 100); + if (sr != 2 && hasWhite) w = parseInt(gId('sliderW').value); + var col = cpick.color.rgb; + cdd.r = r = hasRGB ? col.r : w; + cdd.g = g = hasRGB ? col.g : w; + cdd.b = b = hasRGB ? col.b : w; + cdd.w = w; + setCSL(cd[csel]); + var obj = {"seg": {"col": [[],[],[]]}}; + obj.seg.col[csel] = [r, g, b, w]; + requestJson(obj); +} + +function setBalance(b) +{ + var obj = {"seg": {"cct": parseInt(b)}}; + requestJson(obj); +} + +function rmtTgl(ip,i) { + event.preventDefault(); + event.stopPropagation(); + fetch(`http://${ip}/win&T=2`, {method: 'get'}) + .then((r)=>{ + return r.text(); + }) + .then((t)=>{ + let c = (new window.DOMParser()).parseFromString(t, "text/xml"); + // perhaps just i.classList.toggle("off"); would be enough + if (c.getElementsByTagName('ac')[0].textContent === "0") { + i.classList.add("off"); + } else { + i.classList.remove("off"); + } + }); +} + +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 = "var(--c-r)"; + bt.innerHTML = "Confirm Reboot"; + cnfr = true; return; + } + window.location.href = getURL("/reset"); +} + +var cnfrS = false; +function rSegs() +{ + var bt = gId('rsbtn'); + if (!cnfrS) { + bt.style.color = "var(--c-r)"; + bt.innerHTML = "Confirm reset"; + cnfrS = true; return; + } + cnfrS = false; + bt.style.color = "var(--c-f)"; + bt.innerHTML = "Reset segments"; + var obj = {"seg":[{"start":0,"stop":ledCount,"sel":true}]}; + if (isM) { + obj.seg[0].stop = mw; + obj.seg[0].startX = 0; + obj.seg[0].stopY = mh; + } + for (let i=1; i<=lSeg; i++) obj.seg.push({"stop":0}); + requestJson(obj); +} + +function loadPalettesData(callback = null) +{ + if (palettesData) return; + const lsKey = "wledPalx"; + var lsPalData = localStorage.getItem(lsKey); + if (lsPalData) { + try { + var d = JSON.parse(lsPalData); + if (d && d.vid == d.vid) { + palettesData = d.p; + if (callback) callback(); + return; + } + } catch (e) {} + } + + palettesData = {}; + getPalettesData(0, ()=>{ + localStorage.setItem(lsKey, JSON.stringify({ + p: palettesData, + vid: lastinfo.vid + })); + redrawPalPrev(); + if (callback) setTimeout(callback, 99); + }); +} + +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((error)=>{ + showToast(error, true); + }); +} +/* +function hideModes(txt) +{ + for (let e of (gId('fxlist').querySelectorAll('.lstI')||[])) { + let iT = e.querySelector('.lstIname').innerText; + let f = false; + if (txt==="2D") f = iT.indexOf("\u25A6") >= 0 && iT.indexOf("\u22EE") < 0; // 2D && !1D + else f = iT.indexOf(txt) >= 0; + if (f) e.classList.add('hide'); //else e.classList.remove('hide'); + } +} +*/ +function search(f,l=null) +{ + f.nextElementSibling.style.display=(f.value!=='')?'block':'none'; + if (!l) return; + var el = gId(l).querySelectorAll('.lstI'); + // filter list items but leave (Default & Solid) always visible + for (i = (l==='pcont'?0:1); i < el.length; i++) { + var it = el[i]; + var itT = it.querySelector('.lstIname').innerText.toUpperCase(); + it.style.display = (itT.indexOf(f.value.toUpperCase())<0) ? 'none' : ''; + } +} + +function clean(c) +{ + c.style.display='none'; + var i=c.previousElementSibling; + i.value=''; + i.focus(); + i.dispatchEvent(new Event('input')); + if (i.parentElement.id=='fxFind') { + gId("filters").querySelectorAll("input[type=checkbox]").forEach((e)=>{e.checked=false;}); + } +} + +function filterFx(o) +{ + if (!o) return; + let i = gId('fxFind').children[0]; + i.value=!o.checked?'':o.dataset.flt; + i.focus(); + i.dispatchEvent(new Event('input')); + gId("filters").querySelectorAll("input[type=checkbox]").forEach((e)=>{if(e!==o)e.checked=false;}); +} + +// make sure "dur" and "transition" are arrays with at least the length of "ps" +function formatArr(pl) { + var l = pl.ps.length; + if (!Array.isArray(pl.dur)) { + var v = pl.dur; + if (isNaN(v)) v = 100; + pl.dur = [v]; + } + var l2 = pl.dur.length; + if (l2 < l) + { + for (var i = 0; i < l - l2; i++) + pl.dur.push(pl.dur[l2-1]); + } + + if (!Array.isArray(pl.transition)) { + var v = pl.transition; + if (isNaN(v)) v = tr; + pl.transition = [v]; + } + var l2 = pl.transition.length; + if (l2 < l) + { + for (var i = 0; i < l - l2; i++) + pl.transition.push(pl.transition[l2-1]); + } +} + +function expand(i) +{ + var seg = i<100 ? gId('seg' +i) : gId(`p${i-100}o`); + let ps = gId("pcont").children; // preset wrapper + if (i>100) for (let p of ps) { p.classList.remove('selected'); if (p!==seg) p.classList.remove('expanded'); } // collapse all other presets & remove selected + + seg.classList.toggle('expanded'); + + // presets + if (i >= 100) { + var p = i-100; + if (seg.classList.contains('expanded')) { + if (isPlaylist(p)) { + plJson[p] = pJson[p].playlist; + // make sure all keys are present in plJson[p] + formatArr(plJson[p]); + if (isNaN(plJson[p].repeat)) plJson[p].repeat = 0; + if (!plJson[p].r) plJson[p].r = false; + if (isNaN(plJson[p].end)) plJson[p].end = 0; + gId('seg' +i).innerHTML = makeP(p,true); + refreshPlE(p); + } else { + gId('seg' +i).innerHTML = makeP(p); + } + var papi = papiVal(p); + gId(`p${p}api`).value = papi; + if (papi.indexOf("Please") == 0) gId(`p${p}cstgl`).checked = false; + tglCs(p); + gId('putil').classList.remove('staybot'); + } else { + updatePA(); + gId('seg' +i).innerHTML = ""; + gId('putil').classList.add('staybot'); + } + } + + seg.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); +} + +function unfocusSliders() +{ + gId("sliderBri").blur(); + gId("sliderSpeed").blur(); + gId("sliderIntensity").blur(); +} + +// sliding UI +const _C = d.querySelector('.container'), N = 4; + +let iSlide = 0, x0 = null, scrollS = 0, locked = false; + +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) +{ + if (pcMode) return; + 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 || pcMode) return; + var clientX = unify(e).clientX; + var dx = clientX - x0; + var s = Math.sign(dx); + var f = +(s*dx/wW).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() +{ + wW = window.innerWidth; + var h = gId('top').clientHeight; + sCol('--th', h + "px"); + sCol('--bh', gId('bot').clientHeight + "px"); + if (isLv) h -= 4; + sCol('--tp', h + "px"); + togglePcMode(); + lastw = wW; +} + +function togglePcMode(fromB = false) +{ + if (fromB) { + pcModeA = !pcModeA; + localStorage.setItem('pcm', pcModeA); + } + pcMode = (wW >= 1024) && pcModeA; + if (cpick) cpick.resize(pcMode && wW>1023 && wW<1250 ? 230 : 260); // for tablet in landscape + if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size() + openTab(0, true); + updateTablinks(0); + gId('buttonPcm').className = (pcMode) ? "active":""; + gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto"; + sCol('--bh', gId('bot').clientHeight + "px"); + _C.style.width = (pcMode)?'100%':'400%'; +} + +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(); +_C.style.setProperty('--n', N); + +window.addEventListener('resize', size, true); + +_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/iro.js b/wled00/data/iro.js new file mode 100644 index 00000000..3d63d041 --- /dev/null +++ b/wled00/data/iro.js @@ -0,0 +1,7 @@ +/*! + * iro.js v5.5.2 + * 2016-2021 James Daniel + * Licensed under MPL 2.0 + * github.com/jaames/iro.js + */ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self).iro=n()}(this,function(){"use strict";var m,s,n,i,o,x={},j=[],r=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i;function M(t,n){for(var i in n)t[i]=n[i];return t}function y(t){var n=t.parentNode;n&&n.removeChild(t)}function h(t,n,i){var r,e,u,o,l=arguments;if(n=M({},n),3=r/i?u=n:e=n}return n},function(t,n,i){n&&g(t.prototype,n),i&&g(t,i)}(l,[{key:"hsv",get:function(){var t=this.$;return{h:t.h,s:t.s,v:t.v}},set:function(t){var n=this.$;if(t=b({},n,t),this.onChange){var i={h:!1,v:!1,s:!1,a:!1};for(var r in n)i[r]=t[r]!=n[r];this.$=t,(i.h||i.s||i.v||i.a)&&this.onChange(this,i)}else this.$=t}},{key:"hsva",get:function(){return b({},this.$)},set:function(t){this.hsv=t}},{key:"hue",get:function(){return this.$.h},set:function(t){this.hsv={h:t}}},{key:"saturation",get:function(){return this.$.s},set:function(t){this.hsv={s:t}}},{key:"value",get:function(){return this.$.v},set:function(t){this.hsv={v:t}}},{key:"alpha",get:function(){return this.$.a},set:function(t){this.hsv=b({},this.hsv,{a:t})}},{key:"kelvin",get:function(){return l.rgbToKelvin(this.rgb)},set:function(t){this.rgb=l.kelvinToRgb(t)}},{key:"red",get:function(){return this.rgb.r},set:function(t){this.rgb=b({},this.rgb,{r:t})}},{key:"green",get:function(){return this.rgb.g},set:function(t){this.rgb=b({},this.rgb,{g:t})}},{key:"blue",get:function(){return this.rgb.b},set:function(t){this.rgb=b({},this.rgb,{b:t})}},{key:"rgb",get:function(){var t=l.hsvToRgb(this.$),n=t.r,i=t.g,r=t.b;return{r:G(n),g:G(i),b:G(r)}},set:function(t){this.hsv=b({},l.rgbToHsv(t),{a:void 0===t.a?1:t.a})}},{key:"rgba",get:function(){return b({},this.rgb,{a:this.alpha})},set:function(t){this.rgb=t}},{key:"hsl",get:function(){var t=l.hsvToHsl(this.$),n=t.h,i=t.s,r=t.l;return{h:G(n),s:G(i),l:G(r)}},set:function(t){this.hsv=b({},l.hslToHsv(t),{a:void 0===t.a?1:t.a})}},{key:"hsla",get:function(){return b({},this.hsl,{a:this.alpha})},set:function(t){this.hsl=t}},{key:"rgbString",get:function(){var t=this.rgb;return"rgb("+t.r+", "+t.g+", "+t.b+")"},set:function(t){var n,i,r,e,u=1;if((n=_.exec(t))?(i=K(n[1],255),r=K(n[2],255),e=K(n[3],255)):(n=H.exec(t))&&(i=K(n[1],255),r=K(n[2],255),e=K(n[3],255),u=K(n[4],1)),!n)throw new Error("Invalid rgb string");this.rgb={r:i,g:r,b:e,a:u}}},{key:"rgbaString",get:function(){var t=this.rgba;return"rgba("+t.r+", "+t.g+", "+t.b+", "+t.a+")"},set:function(t){this.rgbString=t}},{key:"hexString",get:function(){var t=this.rgb;return"#"+U(t.r)+U(t.g)+U(t.b)},set:function(t){var n,i,r,e,u=255;if((n=D.exec(t))?(i=17*Q(n[1]),r=17*Q(n[2]),e=17*Q(n[3])):(n=F.exec(t))?(i=17*Q(n[1]),r=17*Q(n[2]),e=17*Q(n[3]),u=17*Q(n[4])):(n=L.exec(t))?(i=Q(n[1]),r=Q(n[2]),e=Q(n[3])):(n=B.exec(t))&&(i=Q(n[1]),r=Q(n[2]),e=Q(n[3]),u=Q(n[4])),!n)throw new Error("Invalid hex string");this.rgb={r:i,g:r,b:e,a:u/255}}},{key:"hex8String",get:function(){var t=this.rgba;return"#"+U(t.r)+U(t.g)+U(t.b)+U(Z(255*t.a))},set:function(t){this.hexString=t}},{key:"hslString",get:function(){var t=this.hsl;return"hsl("+t.h+", "+t.s+"%, "+t.l+"%)"},set:function(t){var n,i,r,e,u=1;if((n=P.exec(t))?(i=K(n[1],360),r=K(n[2],100),e=K(n[3],100)):(n=$.exec(t))&&(i=K(n[1],360),r=K(n[2],100),e=K(n[3],100),u=K(n[4],1)),!n)throw new Error("Invalid hsl string");this.hsl={h:i,s:r,l:e,a:u}}},{key:"hslaString",get:function(){var t=this.hsla;return"hsla("+t.h+", "+t.s+"%, "+t.l+"%, "+t.a+")"},set:function(t){this.hslString=t}}]),l}();function X(t){var n,i=t.width,r=t.sliderSize,e=t.borderWidth,u=t.handleRadius,o=t.padding,l=t.sliderShape,s="horizontal"===t.layoutDirection;return r=null!=(n=r)?n:2*o+2*u,"circle"===l?{handleStart:t.padding+t.handleRadius,handleRange:i-2*o-2*u,width:i,height:i,cx:i/2,cy:i/2,radius:i/2-e/2}:{handleStart:r/2,handleRange:i-r,radius:r/2,x:0,y:0,width:s?r:i,height:s?i:r}}function Y(t,n){var i=X(t),r=i.width,e=i.height,u=i.handleRange,o=i.handleStart,l="horizontal"===t.layoutDirection,s=l?r/2:e/2,c=o+function(t,n){var i=n.hsva,r=n.rgb;switch(t.sliderType){case"red":return r.r/2.55;case"green":return r.g/2.55;case"blue":return r.b/2.55;case"alpha":return 100*i.a;case"kelvin":var e=t.minTemperature,u=t.maxTemperature-e,o=(n.kelvin-e)/u*100;return Math.max(0,Math.min(o,100));case"hue":return i.h/=3.6;case"saturation":return i.s;case"value":default:return i.v}}(t,n)/100*u;return l&&(c=-1*c+u+2*o),{x:l?s:c,y:l?c:s}}var tt,nt=2*Math.PI,it=function(t,n){return(t%n+n)%n},rt=function(t,n){return Math.sqrt(t*t+n*n)};function et(t){return t.width/2-t.padding-t.handleRadius-t.borderWidth}function ut(t){var n=t.width/2;return{width:t.width,radius:n-t.borderWidth,cx:n,cy:n}}function ot(t,n,i){var r=t.wheelAngle,e=t.wheelDirection;return i&&"clockwise"===e?n=r+n:"clockwise"===e?n=360-r+n:i&&"anticlockwise"===e?n=r+180-n:"anticlockwise"===e&&(n=r-n),it(n,360)}function lt(t,n,i){var r=ut(t),e=r.cx,u=r.cy,o=et(t);n=e-n,i=u-i;var l=ot(t,Math.atan2(-i,-n)*(360/nt)),s=Math.min(rt(n,i),o);return{h:Math.round(l),s:Math.round(100/o*s)}}function st(t){var n=t.width,i=t.boxHeight;return{width:n,height:null!=i?i:n,radius:t.padding+t.handleRadius}}function ct(t,n,i){var r=st(t),e=r.width,u=r.height,o=r.radius,l=(n-o)/(e-2*o)*100,s=(i-o)/(u-2*o)*100;return{s:Math.max(0,Math.min(l,100)),v:Math.max(0,Math.min(100-s,100))}}function at(t,n,i,r){for(var e=0;e - - -
+ + +
\ No newline at end of file diff --git a/wled00/data/liveviewws2D.htm b/wled00/data/liveviewws2D.htm new file mode 100644 index 00000000..007ac246 --- /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 e25aeda0..2bb7e882 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 00000000..c000c2e6 --- /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 00000000..feb51ca0 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 00000000..a3b5cceb 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 00000000..bde8945e 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 00000000..6f68f2d2 --- /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 rezised Set desitred 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 evry 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 00000000..39ba1f28 --- /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 00000000..c67ac46a --- /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 00000000..7c347f19 --- /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 00000000..82452af2 --- /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 00000000..b4f3c407 --- /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 00000000..587da71e --- /dev/null +++ b/wled00/data/pxmagic/pxmagic.htm @@ -0,0 +1,2026 @@ + + + + + + + + Pixel Magic Tool + + + + + + +
+
+
+
+ + + +
+
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+ + +
+ + Images uploaded to + + WLED + + or upload image + +
+
+ +
+
+ +
+
+
+ +
+ +
+
+
+ + + diff --git a/wled00/data/rangetouch.js b/wled00/data/rangetouch.js new file mode 100644 index 00000000..ceaef537 --- /dev/null +++ b/wled00/data/rangetouch.js @@ -0,0 +1,8 @@ +// ========================================================================== +// rangetouch.js v2.0.1 +// Making work on touch devices +// https://github.com/sampotts/rangetouch +// License: The MIT License (MIT) +// ========================================================================== +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define("RangeTouch",t):(e=e||self).RangeTouch=t()}(this,(function(){"use strict";function e(e,t){for(var n=0;nt){var n=function(e){var t="".concat(e).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}return function(){function t(e,n){(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")})(this,t),m(e)?this.element=e:d(e)&&(this.element=document.querySelector(e)),m(this.element)&&p(this.element.rangeTouch)&&(this.config=r({},i,{},n),this.init())}return n=t,c=[{key:"setup",value:function(e){var n=1(n=100/l.width*(i.clientX-l.left))?n=0:100n?n-=(100-2*n)*a:50 - - + + + + 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 00000000..5c95d317 --- /dev/null +++ b/wled00/data/settings_2D.htm @@ -0,0 +1,362 @@ + + + + + + + 2D Set-up + + + + +
+
+
+
+
+

2D setup

+ Strip or panel: +
+ +
+ +
+
+ + diff --git a/wled00/data/settings_dmx.htm b/wled00/data/settings_dmx.htm index e8610062..2864c002 100644 --- a/wled00/data/settings_dmx.htm +++ b/wled00/data/settings_dmx.htm @@ -1,41 +1,92 @@ -DMX Settings - + + + + + + DMX Settings + +
+

+

Imma firin ma lazer (if it has DMX support)

Proxy Universe from E1.31 to DMX (0=disabled)
@@ -48,9 +99,9 @@ Spacing between start channels: WARNING: Channel gap is lower than channels per fixture.
This will cause overlap.

DMX fixtures start LED: -

channel functions

+

Channel functions


- \ No newline at end of file + diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 75795b87..11a34493 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -1,179 +1,822 @@ - - - - - - LED Settings - - - - -
-
-
-

LED setup

- LED count:
- - 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.
-
-
- LEDs are 4-channel type (RGBW):
- - Auto-calculate white channel from RGB:
- Color order: - -

Defaults

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

- Apply preset at boot (0 uses defaults) -
- or -
- Set current preset cycle setting as boot default:

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

- Brightness factor: % -

Transitions

- Crossfade:
- Transition Time: ms
- Enable Palette transitions: -

Timed light

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

Advanced

- Palette blending: -
- Reverse LED order (rotate 180):
- Skip first LED:
- -
- - + + + + + + + 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:
+ 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) +

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

+ Brightness factor: % +

Transitions

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

Timed light

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

White management

+ 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:
+
+ +
+
+ + diff --git a/wled00/data/settings_pin.htm b/wled00/data/settings_pin.htm new file mode 100644 index 00000000..de8e46b3 --- /dev/null +++ b/wled00/data/settings_pin.htm @@ -0,0 +1,24 @@ + + + + + + + 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 08d9427f..9040a2aa 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -1,36 +1,112 @@ - + + 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!
@@ -39,19 +115,31 @@ Settings on this page are only changable if OTA lock is disabled!
Deny access to WiFi settings if locked:

Factory reset:
- All EEPROM content (settings) will be erased.

- HTTP traffic is unencrypted. An attacker in the same network can intercept form data! + All settings and presets will be erased.

+
⚠ Unencrypted transmission. An attacker on the same network can intercept form data!
+

Software Update


- Enable ArduinoOTA:
+ 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. +

About

WLED version ##VERSION##

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

- (c) 2016-2019 Christian Schwinne
- Licensed under the MIT license

+ (c) 2016-2023 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 42b493fa..e63526d9 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -1,47 +1,154 @@ -Sync Settings - - + + + + + + Sync Settings + + + -
+ +

+

Sync setup

-

Button setup

-On/Off button enabled:
-Infrared remote: -
-IR info

WLED Broadcast

UDP Port:
-Receive Brightness, Color, and Effects
+2nd Port:
+

Sync groups

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
12345678
Send:
Receive:

+Receive: Brightness, Color, and Effects
+ Segment options, bounds
Send notifications on direct change:
-Send notifications on button press:
+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: +Type:
Multicast:
Start universe:
-Reboot required. Check out LedFx!
+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
+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)

-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: +Broker: Port:
The MQTT credentials are sent over an unsecured connection.
Never use the MQTT password for another service!

-Username:
-Password:
-Client ID:
-Device Topic:
-Group Topic:
-Reboot required to apply changes. MQTT info +Username:
+Password:
+Client ID:
+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
Hue Bridge IP:
- . - . - . -
+ . + . + . +
Press the pushlink button on the bridge, after that save this page!
(when first connecting)
-Hue status: Disabled in this build
+Hue status: Disabled in this build +
+

Serial

+Baud rate: +
+Keep at 115200 to use Improv. Some boards may not support high rates. +
diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index c0231277..19316f7d 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -1,92 +1,175 @@ - + + Time Settings - +
+

+

Time setup

Get time from NTP server:
-
+
Use 24h format:
Time zone:
UTC offset: seconds (max. 18 hours)
- Current local time is unknown. + Current local time is unknown.
+ Latitude:
+ Longitude:
+ +
(opens new tab, only works in browser)
+

Clock

- Clock Overlay: -
-
- First LED: Last LED:
-
+ Analog Clock overlay:
+
+ First LED: Last LED:
12h LED:
- Show 5min marks:
+ Show 5min marks:
Seconds (as trail):
-
- Cronixie Display:
- Cronixie Backlight:
-
Countdown Mode:
Countdown Goal:
- Year: 20 Month: Day:
- Hour: Minute: Second:
-

Advanced Macros

- Define API macros here:
- 1:
- 2:
- 3:
- 4:
- 5:
- 6:
- 7:
- 8:
- 9:
- 10:
- 11:
- 12:
- 13:
- 14:
- 15:
- 16:

- Use 0 for the default action instead of a macro
- Boot Macro:
- Alexa On/Off Macros:
- Button short press macro: Macro:
- Long Press: Double press:
- Countdown-Over Macro:
- Timed-Light-Over Macro:
- Time-Controlled Macros:
-
- -

+ Date: 20--
+ 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!
+ Use 0 for the default action instead of a preset
+ Alexa On/Off Preset:
+ Countdown-Over Preset:
+ Timed-Light-Over Presets:
+

Button actions

+ + + + + + + + + + + +
push
switch
short
on->off
long
off->on
double
N/A
+ Analog Button setup +

Time-controlled presets

+
+
+
+
diff --git a/wled00/data/settings_ui.htm b/wled00/data/settings_ui.htm index d3f4de23..00cfe644 100644 --- a/wled00/data/settings_ui.htm +++ b/wled00/data/settings_ui.htm @@ -1,53 +1,298 @@ - - + + + 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.

+ +
Loading settings...
+ +

UI Appearance

+ :
+ :
+ :
+ :
+ :
+ :
+ I hate dark mode:
+ + :
+ :
+ :
+ BG image URL:
+ Random BG image:
+ + :
+
Custom CSS:
+ :
+
Holidays:
+
+
+
\ No newline at end of file diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm new file mode 100644 index 00000000..bfe734b5 --- /dev/null +++ b/wled00/data/settings_um.htm @@ -0,0 +1,345 @@ + + + + + + + Usermod Settings + + + + + +
+
+
+
+ +
+
+

Usermod Setup

+ 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...
+
+
+ + + \ No newline at end of file diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm index a72c43fd..1021dfd8 100644 --- a/wled00/data/settings_wifi.htm +++ b/wled00/data/settings_wifi.htm @@ -1,71 +1,227 @@ - + + 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):
- . - . - . -
+ . + . + . +
Static gateway:
- . - . - . -
+ . + . + . +
Static subnet mask:
- . - . - . -
- 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 password (leave empty for open):

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

Experimental

Disable WiFi sleep:
Can help with connectivity issues.
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 00000000..87eecee7 --- /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 00000000..955bd65d --- /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 00000000..b8b87617 --- /dev/null +++ b/wled00/data/simple.js @@ -0,0 +1,1458 @@ +//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("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 occured."; + 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 41e4c1fe..5daca929 100644 --- a/wled00/data/style.css +++ b/wled00/data/style.css @@ -1,5 +1,9 @@ +html { + touch-action: manipulation; +} body { font-family: Verdana, sans-serif; + font-size: 1rem; text-align: center; background: #222; color: #fff; @@ -9,39 +13,189 @@ body { hr { border-color: #666; } -button { +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; + border-radius: 24px; display: inline-block; font-size: 20px; - margin: 8px; - margin-top: 12px; + 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; } .helpB { 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; } +input:disabled { + color: #888; +} +input[type="text"], +input[type="number"], +select { + font-size: medium; + margin: 2px; + } input[type="number"] { width: 4em; } +input[type="number"].xxl { + width: 100px; +} +input[type="number"].xl { + width: 85px; +} +input[type="number"].l { + width: 64px; +} +input[type="number"].m { + width: 56px; +} +input[type="number"].s { + width: 48px; +} +input[type="number"].xs { + width: 40px; +} +input[type="checkbox"] { + transform: scale(1.5); + margin-right: 10px; +} +td input[type="checkbox"] { + margin-right: revert; +} +input[type=file] { + font-size: 16px +} select { + 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; } .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; + border-radius: 5px; + bottom: 64px; + color: #fff; + font-size: 17px; + padding: 16px; + pointer-events: none; + position: fixed; + text-align: center; + z-index: 5; + transform: translateX(-50%); + max-width: 90%; + left: 50%; +} + +#toast.show { + opacity: 1; + background-color: #264; + animation: fadein 0.5s, fadein 0.5s 2.5s reverse; +} + +#toast.error { + opacity: 1; + 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 be594989..f157f98d 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -1,43 +1,28 @@ - - - WLED Update - - + + WLED Update + + - -

WLED Software Update

Installed version: ##VERSION##
Download the latest binary:
-

+ +

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 0542aff5..671212a3 100644 --- a/wled00/data/welcome.htm +++ b/wled00/data/welcome.htm @@ -1,54 +1,63 @@ - - - - - WLED Setup - - - - - - - -

- -

Welcome to WLED!

-

Thank you for installing my application!

-If you encounter a bug or have a question/feature suggestion, feel free to open a GitHub issue!

-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 7fef5666..bcd7e7f9 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,17 +17,24 @@ 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(); - for (int i = DMXStartLED; i < ledCount; i++) { // uses the amount of LEDs as fixture count + 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 uint32_t in = strip.getPixelColor(i); // get the colors for the individual fixtures as suggested by Aircoookie in issue #462 - byte w = in >> 24 & 0xFF; - byte r = in >> 16 & 0xFF; - byte g = in >> 8 & 0xFF; - byte b = in & 0xFF; + byte w = W(in); + byte r = R(in); + byte g = G(in); + byte b = B(in); int DMXFixtureStart = DMXStart + (DMXGap * (i - DMXStartLED)); for (int j = 0; j < DMXChannels; j++) { @@ -34,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); @@ -59,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 3c68d375..68c7ca5a 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -1,28 +1,93 @@ #include "wled.h" +#define MAX_3_CH_LEDS_PER_UNIVERSE 170 +#define MAX_4_CH_LEDS_PER_UNIVERSE 128 +#define MAX_CHANNELS_PER_UNIVERSE 512 + /* * E1.31 handler */ -void handleE131Packet(e131_packet_t* p, IPAddress clientIP, bool isArtnet){ - //E1.31 protocol support +//DDP protocol support, called by handleE131Packet +//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; + if (sn) { + if (lastPushSeq > 5) { + if (sn > (lastPushSeq -5) && sn < lastPushSeq) return; + } else { + if (sn > (10 + lastPushSeq) || sn < lastPushSeq) return; + } + } + } + + 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 || (realtimeMode && useMainSegmentOnly)) { + for (uint16_t i = start; i < stop; i++) { + setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0); + c += ddpChannelsPerLed; + } + } + + bool push = p->flags & DDP_PUSH_FLAG; + if (push) { + e131NewData = true; + byte sn = p->sequenceNum & 0xF; + if (sn) e131LastSequenceNumber[0] = sn; + } +} + +//E1.31 and Art-Net protocol support +void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ uint16_t uni = 0, dmxChannels = 0; uint8_t* e131_data = nullptr; uint8_t seq = 0, mde = REALTIME_MODE_E131; - if (isArtnet) + 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 { + } 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); + return; } #ifdef WLED_ENABLE_DMX @@ -35,142 +100,430 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, bool isArtnet){ #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; - uint16_t possibleLEDsInCurrentUniverse; 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: + 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; - for (uint16_t i = 0; i < ledCount; i++) - setRealtimePixel(i, e131_data[DMXAddress+0], e131_data[DMXAddress+1], e131_data[DMXAddress+2], wChannel); + + 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[dataOffset+0], e131_data[dataOffset+1], e131_data[dataOffset+2], wChannel); break; - case DMX_MODE_SINGLE_DRGB: + 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]; - strip.setBrightness(bri); + 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 < ledCount; i++) - setRealtimePixel(i, e131_data[DMXAddress+1], e131_data[DMXAddress+2], e131_data[DMXAddress+3], wChannel); + + for (uint16_t i = 0; i < totalLen; i++) + setRealtimePixel(i, e131_data[dataOffset+1], e131_data[dataOffset+2], e131_data[dataOffset+3], wChannel); break; - case DMX_MODE_EFFECT: - if (uni != e131Universe) return; - if (dmxChannels-DMXAddress+1 < 11) return; - 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_PRESET: // 2 channel: [Dimmer,Preset] { - col[3] = e131_data[DMXAddress+11]; //white - colSec[3] = e131_data[DMXAddress+12]; - } - transitionDelayTemp = 0; // act fast - colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); // don't send UDP - return; // don't activate realtime live mode - break; + if (uni != e131Universe || availDMXLen < 2) return; - case DMX_MODE_MULTIPLE_RGB: - realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride) return; - if (previousUniverses == 0) { - // first universe of this fixture - possibleLEDsInCurrentUniverse = (dmxChannels - DMXAddress + 1) / 3; - for (uint16_t i = 0; i < ledCount; i++) { - if (i >= possibleLEDsInCurrentUniverse) break; // more LEDs will follow in next universe(s) - setRealtimePixel(i, e131_data[DMXAddress+i*3+0], e131_data[DMXAddress+i*3+1], e131_data[DMXAddress+i*3+2], 0); + // 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); } - } else if (previousUniverses > 0 && uni < (e131Universe + E131_MAX_UNIVERSE_COUNT)) { - // additional universe(s) of this fixture - uint16_t numberOfLEDsInPreviousUniverses = ((512 - DMXAddress + 1) / 3); // first universe - if (previousUniverses > 1) numberOfLEDsInPreviousUniverses += (512 / 3) * (previousUniverses - 1); // extended universe(s) before current - possibleLEDsInCurrentUniverse = dmxChannels / 3; - for (uint16_t i = numberOfLEDsInPreviousUniverses; i < ledCount; i++) { - uint8_t j = i - numberOfLEDsInPreviousUniverses; - if (j >= possibleLEDsInCurrentUniverse) break; // more LEDs will follow in next universe(s) - setRealtimePixel(i, e131_data[j*3+1], e131_data[j*3+2], e131_data[j*3+3], 0); - } - } - break; + // 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; + } + + 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; + { + 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: - realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride) return; - if (previousUniverses == 0) { - // first universe of this fixture - if (DMXOldDimmer != e131_data[DMXAddress+0]) { - DMXOldDimmer = e131_data[DMXAddress+0]; - bri = e131_data[DMXAddress+0]; - strip.setBrightness(bri); - } - possibleLEDsInCurrentUniverse = (dmxChannels - DMXAddress) / 3; - for (uint16_t i = 0; i < ledCount; i++) { - if (i >= possibleLEDsInCurrentUniverse) break; // more LEDs will follow in next universe(s) - setRealtimePixel(i, e131_data[DMXAddress+i*3+1], e131_data[DMXAddress+i*3+2], e131_data[DMXAddress+i*3+3], 0); - } - } else if (previousUniverses > 0 && uni < (e131Universe + E131_MAX_UNIVERSE_COUNT)) { - // additional universe(s) of this fixture - uint16_t numberOfLEDsInPreviousUniverses = ((512 - DMXAddress + 1) / 3); // first universe - if (previousUniverses > 1) numberOfLEDsInPreviousUniverses += (512 / 3) * (previousUniverses - 1); // extended universe(s) before current - possibleLEDsInCurrentUniverse = dmxChannels / 3; - for (uint16_t i = numberOfLEDsInPreviousUniverses; i < ledCount; i++) { - uint8_t j = i - numberOfLEDsInPreviousUniverses; - if (j >= possibleLEDsInCurrentUniverse) break; // more LEDs will follow in next universe(s) - setRealtimePixel(i, e131_data[j*3+1], e131_data[j*3+2], e131_data[j*3+3], 0); - } - } - break; + 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 ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; + uint8_t stripBrightness = bri; + uint16_t previousLeds, dmxOffset, ledsTotal; + if (previousUniverses == 0) { + 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) { + 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; + 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; + } + + 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); + dmxOffset+=3; + } + } else { + for (uint16_t i = previousLeds; i < ledsTotal; i++) { + setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], e131_data[dmxOffset+3]); + dmxOffset+=4; + } + } + break; + } default: - DEBUG_PRINTLN("unknown E1.31 DMX mode"); + DEBUG_PRINTLN(F("unknown E1.31 DMX mode")); return; // nothing to do break; } 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 da8643d4..c67fdbf3 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -1,51 +1,99 @@ #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 -void initBlynk(const char* auth); -void handleBlynk(); -void updateBlynk(); +#endif //button.cpp -void shortPressAction(); +void shortPressAction(uint8_t b=0); +void longPressAction(uint8_t b=0); +void doublePressAction(uint8_t b=0); +bool isButtonPressed(uint8_t b=0); void handleButton(); void handleIO(); -//colors.cpp -void colorFromUint32(uint32_t in, bool secondary = false); -void colorFromUint24(uint32_t in, bool secondary = false); -void relativeChangeWhite(int8_t amount, byte lowerBoundary = 0); -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); //hue, sat to rgb -void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb +//cfg.cpp +bool deserializeConfig(JsonObject doc, bool fromFS = false); +void deserializeConfigFromFS(); +bool deserializeConfigSec(); +void serializeConfig(); +void serializeConfigSec(); +template +bool getJsonValue(const JsonVariant& element, DestType& destination) { + if (element.isNull()) { + return false; + } + + destination = element.as(); + return true; +} + +template +bool getJsonValue(const JsonVariant& element, DestType& destination, const DefaultType defaultValue) { + if(!getJsonValue(element, destination)) { + destination = defaultValue; + return false; + } + + return true; +} + + +//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); +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); -void colorRGBtoRGBW(byte* rgb); //rgb to rgbw (http://codewelt.com/rgbw). (RGBW_MODE_LEGACY) +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 void initDMX(); void handleDMX(); //e131.cpp -void handleE131Packet(e131_packet_t* p, IPAddress clientIP, bool isArtnet); +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); +bool writeObjectToFileUsingId(const char* file, uint16_t id, JsonDocument* content); +bool writeObjectToFile(const char* file, const char* key, JsonDocument* content); +bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest); +bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest); +void updateFSInfo(); +void closeFile(); //hue.cpp void handleHue(); @@ -55,13 +103,25 @@ void onHueConnect(void* arg, AsyncClient* client); 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 startImprovWifiScan(); +void handleImprovWifiScan(); +void sendImprovIPRPCResult(ImprovRPCType type); + //ir.cpp -bool decodeIRCustom(uint32_t code); void applyRepeatActions(); -void relativeChange(byte* property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF); -void changeEffectSpeed(int8_t amount); -void changeBrightness(int8_t amount); -void changeEffectIntensity(int8_t amount); +byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF); void decodeIR(uint32_t code); void decodeIR24(uint32_t code); void decodeIR24OLD(uint32_t code); @@ -71,6 +131,7 @@ void decodeIR44(uint32_t code); void decodeIR21(uint32_t code); void decodeIR6(uint32_t code); void decodeIR9(uint32_t code); +void decodeIRJson(uint32_t code); void initIR(); void handleIR(); @@ -81,76 +142,159 @@ void handleIR(); #include "src/dependencies/json/AsyncJson-v6.h" #include "FX.h" -void deserializeSegment(JsonObject elem, byte it); -bool deserializeState(JsonObject root); -void serializeSegment(JsonObject& root, WS2812FX::Segment& seg, byte id); -void serializeState(JsonObject root); +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, 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); -void serveLiveLeds(AsyncWebServerRequest* request); +#ifdef WLED_ENABLE_JSONLIVE +bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); +#endif //led.cpp +void setValuesFromSegment(uint8_t s); void setValuesFromMainSeg(); +void setValuesFromFirstSelectedSeg(); void resetTimebase(); void toggleOnOff(); -void setAllLeds(); -void setLedsStandard(bool justColors = false); -bool colorChanged(); -void colorUpdated(int callMode); +void applyBri(); +void applyFinalBri(); +void applyValuesToSelectedSegs(); +void colorUpdated(byte callMode); +void stateUpdated(byte callMode); void updateInterfaces(uint8_t callMode); 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(); void publishMqtt(); //ntp.cpp +void handleTime(); void handleNetworkTime(); void sendNTPPacket(); -bool checkNTPResponse(); +bool checkNTPResponse(); void updateLocalTime(); void getTimeString(char* out); bool checkCountdown(); void setCountdown(); byte weekdayMondayFirst(); void checkTimers(); +void calculateSunriseAndSunset(); +void setTimeFromAPI(uint32_t timein); //overlay.cpp -void initCronixie(); -void handleOverlays(); void handleOverlayDraw(); void _overlayAnalogCountdown(); void _overlayAnalogClock(); -byte getSameCodeLength(char code, int index, char const cronixieDisplay[]); -void setCronixie(); -void _overlayCronixie(); -void _drawOverlayCronixie(); +//playlist.cpp +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 -void _setRandomColor(bool _sec,bool fromButton=false); bool isAsterisksOnly(const char* str, byte maxLen); void handleSettingsSet(AsyncWebServerRequest *request, byte subPage); -bool handleSet(AsyncWebServerRequest *request, const String& req); -int getNumVal(const String* req, uint16_t pos); -bool updateVal(const String* req, const char* key, byte* val, byte minv=0, byte maxv=255); +bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=true); //udp.cpp void notify(byte callMode, bool followUp=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(); + +//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 setup() {} - virtual void connected() {} - virtual void addToJsonState(JsonObject& obj) {} - virtual void addToJsonInfo(JsonObject& obj) {} - virtual void readFromJsonState(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) {} // 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;} }; @@ -161,16 +305,24 @@ class UsermodManager { public: 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); - + void addToConfig(JsonObject& obj); + 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); - byte getModCount(); + Usermod* lookup(uint16_t mod_id); + byte getModCount() {return numMods;}; }; //usermods_list.cpp @@ -181,40 +333,82 @@ 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(); + +#ifdef WLED_ADD_EEPROM_SUPPORT //wled_eeprom.cpp -void commit(); -void clearEEPROM(); -void writeStringToEEPROM(uint16_t pos, char* str, uint16_t len); -void readStringFromEEPROM(uint16_t pos, char* str, uint16_t len); -void saveSettingsToEEPROM(); -void loadSettingsFromEEPROM(bool first); -void savedToPresets(); -bool applyPreset(byte index, bool loadBri = true); -void savePreset(byte index, bool persist = true); -void loadMacro(byte index, char* m); void applyMacro(byte index); -void saveMacro(byte index, String mc, bool persist = true); //only commit on single save, not in settings +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(); +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, String headl, String subl="", byte optionT=255); -String settingsProcessor(const String& var); +void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255); String dmxProcessor(const String& var); -void serveSettings(AsyncWebServerRequest* request); +void serveSettings(AsyncWebServerRequest* request, bool post = false); +void serveSettingsJS(AsyncWebServerRequest* request); + +//ws.cpp +void handleWs(); +void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len); +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 eb0dcb93..4edbdd6f 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -4,27 +4,387 @@ * Utility for SPIFFS filesystem */ -//filesystem -#ifndef WLED_DISABLE_FILESYSTEM -#include -#ifdef ARDUINO_ARCH_ESP32 -#include "SPIFFS.h" +#ifdef ARDUINO_ARCH_ESP32 //FS info bare IDF function until FS wrapper is available for ESP32 +#if WLED_FS != LITTLEFS && ESP_IDF_VERSION_MAJOR < 4 + #include "esp_spiffs.h" #endif -#include "SPIFFSEditor.h" #endif +#define FS_BUFSIZE 256 + +/* + * Structural requirements for files managed by writeObjectToFile() and readObjectFromFile() utilities: + * 1. File must be a string representation of a valid JSON object + * 2. File must have '{' as first character + * 3. There must not be any additional characters between a root-level key and its value object (e.g. space, tab, newline) + * 4. There must not be any characters between an root object-separating ',' and the next object key string + * 5. There may be any number of spaces, tabs, and/or newlines before such object-separating ',' + * 6. There must not be more than 5 consecutive spaces at any point except for those permitted in condition 5 + * 7. If it is desired to delete the first usable object (e.g. preset file), a dummy object '"0":{}' is inserted at the beginning. + * It shall be disregarded by receiving software. + * The reason for it is that deleting the first preset would require special code to handle commas between it and the 2nd preset + */ + +// 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 +constexpr size_t MAX_SPACE = UINT16_MAX * 2U; // smallest supported config has 128Kb flash size +static volatile size_t knownLargestSpace = MAX_SPACE; + +static File f; // don't export to other cpp files + +//wrapper to find out how long closing takes +void closeFile() { + #ifdef WLED_DEBUG_FS + DEBUGFS_PRINT(F("Close -> ")); + uint32_t s = millis(); + #endif + f.close(); + DEBUGFS_PRINTF("took %d ms\n", millis() - s); + doCloseFile = false; +} + +//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 +static bool bufferedFind(const char *target, bool fromStart = true) { + #ifdef WLED_DEBUG_FS + DEBUGFS_PRINT("Find "); + DEBUGFS_PRINTLN(target); + uint32_t s = millis(); + #endif + + if (!f || !f.size()) return false; + size_t targetLen = strlen(target); + + size_t index = 0; + byte buf[FS_BUFSIZE]; + if (fromStart) f.seek(0); + + while (f.position() < f.size() -1) { + 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 + + if(buf[count] == target[index]) { + if(++index >= targetLen) { // return true if all chars in the target match + f.seek((f.position() - bufsize) + count +1); + DEBUGFS_PRINTF("Found at pos %d, took %d ms", f.position(), millis() - s); + return true; + } + } + count++; + } + } + DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + return false; +} + +//find empty spots in file stream in 256-byte blocks. +static bool bufferedFindSpace(size_t targetLen, bool fromStart = true) { + + #ifdef WLED_DEBUG_FS + DEBUGFS_PRINTF("Find %d spaces\n", targetLen); + uint32_t s = millis(); + #endif + + if (knownLargestSpace < targetLen) { + DEBUGFS_PRINT(F("No match, KLS ")); + DEBUGFS_PRINTLN(knownLargestSpace); + return false; + } + + if (!f || !f.size()) return false; + + 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) { + 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 = 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; + } + } else { + if (!fromStart) return false; + if (index) { + if (knownLargestSpace < index || (knownLargestSpace == MAX_SPACE)) knownLargestSpace = index; + index = 0; // reset index if not space + } + } + + count++; + } + } + DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + return false; +} + +//find the closing bracket corresponding to the opening bracket at the file pos when calling this function +static bool bufferedFindObjectEnd() { + #ifdef WLED_DEBUG_FS + DEBUGFS_PRINTLN(F("Find obj end")); + uint32_t s = millis(); + #endif + + if (!f || !f.size()) return false; + + uint16_t objDepth = 0; //num of '{' minus num of '}'. return once 0 + //size_t start = f.position(); + byte buf[FS_BUFSIZE]; + + while (f.position() < f.size() -1) { + 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--; + if (objDepth == 0) { + f.seek((f.position() - bufsize) + count +1); + DEBUGFS_PRINTF("} at pos %d, took %d ms", f.position(), millis() - s); + return true; + } + count++; + } + } + DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + return false; +} + +//fills n bytes from current file pos with ' ' characters +static void writeSpace(size_t l) +{ + byte buf[FS_BUFSIZE]; + memset(buf, ' ', FS_BUFSIZE); + + while (l > 0) { + size_t block = (l>FS_BUFSIZE) ? FS_BUFSIZE : l; + f.write(buf, block); + l -= block; + } + + if (knownLargestSpace < l) knownLargestSpace = l; +} + +bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint32_t contentLen = 0) +{ + #ifdef WLED_DEBUG_FS + DEBUGFS_PRINTLN(F("Append")); + uint32_t s1 = millis(); + #endif + uint32_t pos = 0; + if (!f) return false; + + if (f.size() < 3) { + char init[10]; + strcpy_P(init, PSTR("{\"0\":{}}")); + f.print(init); + } + + if (content->isNull()) { + 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); + if (bufferedFindSpace(contentLen + strlen(key) + 1)) { + if (f.position() > 2) f.write(','); //add comma if not first object + f.print(key); + serializeJson(*content, f); + DEBUGFS_PRINTF("Inserted, took %d ms (total %d)", millis() - s1, millis() - s); + doCloseFile = true; + return true; + } + + //not enough space, append at end + + //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 }"); + f.seek(0); + while (bufferedFind("}",false)) //find last closing bracket in JSON if not last char + { + pos = f.position(); + } + if (pos > 0) pos--; + } + DEBUGFS_PRINT("pos "); DEBUGFS_PRINTLN(pos); + if (pos > 2) + { + f.seek(pos, SeekSet); + f.write(','); + } else { //file content is not valid JSON object + f.seek(0, SeekSet); + f.print('{'); //start JSON + } + + f.print(key); + + //Append object + serializeJson(*content, f); + f.write('}'); + + doCloseFile = true; + DEBUGFS_PRINTF("Appended, took %d ms (total %d)", millis() - s1, millis() - s); + return true; +} + +bool writeObjectToFileUsingId(const char* file, uint16_t id, JsonDocument* content) +{ + char objKey[10]; + sprintf(objKey, "\"%d\":", id); + return writeObjectToFile(file, objKey, content); +} + +bool writeObjectToFile(const char* file, const char* key, JsonDocument* content) +{ + uint32_t s = 0; //timing + #ifdef WLED_DEBUG_FS + DEBUGFS_PRINTF("Write to %s with key %s >>>\n", file, (key==nullptr)?"nullptr":key); + serializeJson(*content, Serial); DEBUGFS_PRINTLN(); + s = millis(); + #endif + + 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(); + size_t pos2 = f.position(); + + uint32_t oldLen = pos2 - pos; + DEBUGFS_PRINTF("Old obj len %d\n", oldLen); + + //Three cases: + //1. The new content is null, overwrite old obj with spaces + //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 + + size_t contentLen = 0; + if (!content->isNull()) contentLen = measureJson(*content); + + if (contentLen && contentLen <= oldLen) { //replace and fill diff with spaces + DEBUGFS_PRINTLN(F("replace")); + f.seek(pos); + serializeJson(*content, f); + writeSpace(pos2 - f.position()); + } else if (contentLen && bufferedFindSpace(contentLen - oldLen, false)) { //enough leading spaces to replace + DEBUGFS_PRINTLN(F("replace (trailing)")); + f.seek(pos); + serializeJson(*content, f); + } else { + DEBUGFS_PRINTLN(F("delete")); + pos -= strlen(key); + if (pos > 3) pos--; //also delete leading comma if not first object + f.seek(pos); + writeSpace(pos2 - pos); + if (contentLen) return appendObjectToFile(key, content, s, contentLen); + } + + doCloseFile = true; + DEBUGFS_PRINTF("Replaced/deleted, took %d ms\n", millis() - s); + return true; +} + +bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest) +{ + char objKey[10]; + sprintf(objKey, "\"%d\":", id); + return readObjectFromFile(file, objKey, dest); +} + +//if the key is a nullptr, deserialize entire object +bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest) +{ + if (doCloseFile) closeFile(); + #ifdef WLED_DEBUG_FS + DEBUGFS_PRINTF("Read from %s with key %s >>>\n", file, (key==nullptr)?"nullptr":key); + uint32_t s = millis(); + #endif + f = WLED_FS.open(file, "r"); + if (!f) return false; + + if (key != nullptr && !bufferedFind(key)) //key does not exist in file + { + f.close(); + dest->clear(); + DEBUGFS_PRINTLN(F("Obj not found.")); + return false; + } + + deserializeJson(*dest, f); + + f.close(); + DEBUGFS_PRINTF("Read, took %d ms\n", millis() - s); + return true; +} + +void updateFSInfo() { + #ifdef ARDUINO_ARCH_ESP32 + #if WLED_FS == LITTLEFS || ESP_IDF_VERSION_MAJOR >= 4 + fsBytesTotal = WLED_FS.totalBytes(); + fsBytesUsed = WLED_FS.usedBytes(); + #else + esp_spiffs_info(nullptr, &fsBytesTotal, &fsBytesUsed); + #endif + #else + FSInfo fsi; + WLED_FS.info(fsi); + fsBytesUsed = fsi.usedBytes; + fsBytesTotal = fsi.totalBytes; + #endif +} + -#if !defined WLED_DISABLE_FILESYSTEM && defined WLED_ENABLE_FS_SERVING //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(".css")) return "text/css"; + 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"; @@ -35,21 +395,18 @@ 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); - String pathWithGz = path + ".gz"; - if(SPIFFS.exists(pathWithGz)){ - request->send(SPIFFS, pathWithGz, contentType); + /*String pathWithGz = path + ".gz"; + if(WLED_FS.exists(pathWithGz)){ + request->send(WLED_FS, pathWithGz, contentType); return true; - } - if(SPIFFS.exists(path)) { - request->send(SPIFFS, path, contentType); + }*/ + if(WLED_FS.exists(path)) { + request->send(WLED_FS, path, contentType); return true; } return false; } - -#else -bool handleFileRead(AsyncWebServerRequest*, String path){return false;} -#endif diff --git a/wled00/html_cpal.h b/wled00/html_cpal.h new file mode 100644 index 00000000..a2009639 --- /dev/null +++ b/wled00/html_cpal.h @@ -0,0 +1,308 @@ +/* + * 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 = 4721; +const uint8_t PAGE_cpal[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x13, 0xbd, 0x3b, 0x7f, 0x73, 0xdb, 0xb6, + 0x92, 0xff, 0xe7, 0x53, 0x20, 0x4c, 0x5f, 0x42, 0xd6, 0x14, 0x45, 0xd2, 0xb6, 0x64, 0x4b, 0xa2, + 0x3b, 0xa9, 0x93, 0x77, 0xce, 0x8d, 0xdd, 0x64, 0x5e, 0x7c, 0x6e, 0x7b, 0x3e, 0xbf, 0x31, 0x4d, + 0x42, 0x12, 0x1b, 0x8a, 0xe0, 0x03, 0x21, 0xd9, 0xae, 0xac, 0xef, 0x7e, 0xbb, 0x00, 0x48, 0x91, + 0x94, 0xe4, 0x24, 0xd7, 0x37, 0xd7, 0xf1, 0x44, 0x20, 0xb0, 0x58, 0xec, 0x2e, 0xf6, 0x17, 0x16, + 0xe8, 0xe8, 0xe5, 0xbb, 0x8f, 0xa7, 0x97, 0xbf, 0x7f, 0x7a, 0x4f, 0xa6, 0x62, 0x96, 0x9e, 0x90, + 0x51, 0xf9, 0x43, 0xc3, 0x18, 0x7e, 0x66, 0x54, 0x84, 0x30, 0x22, 0xf2, 0x0e, 0xfd, 0xd7, 0x3c, + 0x59, 0x04, 0xc6, 0x69, 0x18, 0x4d, 0x69, 0xe7, 0x94, 0x65, 0x82, 0xb3, 0xd4, 0x20, 0x2f, 0x22, + 0x68, 0xd1, 0x4c, 0x04, 0x46, 0xc6, 0x3a, 0x11, 0x8e, 0xd9, 0x04, 0x5a, 0x85, 0x60, 0x1c, 0x5a, + 0xb3, 0x79, 0x21, 0x3a, 0x9c, 0x2e, 0xc2, 0x34, 0x89, 0x43, 0x41, 0x8d, 0x6d, 0x08, 0x3f, 0xf1, + 0x70, 0x32, 0x0b, 0xb7, 0x61, 0xda, 0x0a, 0xfe, 0xfe, 0x21, 0x4f, 0x38, 0x2d, 0x0c, 0x52, 0x81, + 0xbb, 0x08, 0x27, 0x12, 0x91, 0xd2, 0x93, 0x17, 0xbf, 0x9e, 0xbf, 0x7f, 0x47, 0x4e, 0x61, 0x55, + 0x36, 0x23, 0x9f, 0xc2, 0x94, 0x0a, 0x41, 0xc9, 0xfb, 0x38, 0x01, 0x6a, 0x46, 0x5d, 0x05, 0x42, + 0x46, 0x45, 0xc4, 0x93, 0x5c, 0x10, 0xf1, 0x98, 0xd3, 0xc0, 0x10, 0xf4, 0x41, 0x74, 0xff, 0x08, + 0x17, 0xa1, 0xea, 0x35, 0x4e, 0x5e, 0x8c, 0xe7, 0x59, 0x24, 0x12, 0x96, 0x91, 0xc9, 0x87, 0xd8, + 0xa4, 0xd6, 0x92, 0x53, 0x31, 0xe7, 0x19, 0x89, 0x9d, 0x09, 0x15, 0xef, 0x53, 0x3a, 0x83, 0x35, + 0x7f, 0x7e, 0x94, 0x43, 0xab, 0x0a, 0x34, 0x7a, 0xdf, 0x80, 0x8c, 0x38, 0x05, 0x6e, 0x35, 0x30, + 0x02, 0x2e, 0x42, 0x4e, 0xe2, 0x20, 0x66, 0xd1, 0x1c, 0x7b, 0x5e, 0x8c, 0xba, 0x6a, 0x35, 0x24, + 0x46, 0x3c, 0x22, 0xdd, 0x77, 0x2c, 0x7e, 0x5c, 0x8e, 0x81, 0xa3, 0xce, 0x38, 0x9c, 0x25, 0xe9, + 0xe3, 0xe0, 0x2d, 0x4f, 0xc2, 0xd4, 0x2e, 0xc2, 0xac, 0xe8, 0x14, 0x94, 0x27, 0xe3, 0xe1, 0x5d, + 0x18, 0x7d, 0x99, 0x70, 0x36, 0xcf, 0xe2, 0x4e, 0xc4, 0x52, 0xc6, 0x07, 0xaf, 0x3c, 0xcf, 0x1b, + 0xca, 0x29, 0x45, 0xf2, 0x27, 0x1d, 0x78, 0xbd, 0xfc, 0x61, 0xa8, 0x47, 0xe2, 0x38, 0x1e, 0xce, + 0x42, 0x3e, 0x49, 0xb2, 0x81, 0x4b, 0x3c, 0x17, 0x06, 0xd2, 0x24, 0xa3, 0x9d, 0x29, 0x4d, 0x26, + 0x53, 0x31, 0x70, 0x0e, 0x57, 0xaf, 0xf2, 0x90, 0x03, 0x21, 0x1d, 0x94, 0x61, 0x08, 0x43, 0x7c, + 0x99, 0xb3, 0x22, 0x41, 0x56, 0x06, 0x9c, 0xa6, 0xa1, 0x48, 0x16, 0x74, 0x78, 0x9f, 0xc4, 0x62, + 0x3a, 0xf0, 0x5c, 0xf7, 0x6f, 0x43, 0x3d, 0xd1, 0x07, 0x4c, 0xab, 0x57, 0x77, 0x4c, 0x80, 0x74, + 0x4f, 0x37, 0x67, 0x86, 0x77, 0x05, 0x4b, 0xe7, 0x82, 0xea, 0xa5, 0x3b, 0x82, 0xe5, 0x83, 0x43, + 0x39, 0x65, 0xc2, 0xc3, 0x38, 0xc1, 0xf5, 0xee, 0xd8, 0xc3, 0x72, 0x13, 0x2f, 0xb6, 0x57, 0x8e, + 0xa4, 0xbd, 0x03, 0x73, 0xbf, 0x50, 0x6e, 0xeb, 0xaf, 0x3c, 0x89, 0xe0, 0x4b, 0x77, 0x6e, 0x59, + 0xe9, 0x8e, 0xf1, 0x18, 0xc6, 0x11, 0xfd, 0xbc, 0x18, 0xec, 0x03, 0xa3, 0x1b, 0x62, 0x2a, 0x92, + 0x74, 0x41, 0xb9, 0x86, 0x1c, 0xf8, 0xf9, 0x03, 0x81, 0xb9, 0x49, 0x4c, 0xf8, 0xe4, 0x2e, 0x34, + 0x7b, 0x47, 0xb6, 0xfa, 0x73, 0x0e, 0xad, 0xe1, 0x9f, 0x9d, 0x24, 0x8b, 0xe9, 0xc3, 0xc0, 0x6f, + 0xd2, 0xb2, 0xd4, 0x54, 0xee, 0xa3, 0x1c, 0x15, 0xf1, 0x7d, 0x68, 0x29, 0xee, 0xfe, 0x36, 0x14, + 0x1c, 0xf6, 0x68, 0xcc, 0xf8, 0x6c, 0x20, 0x5b, 0x20, 0x3c, 0xfa, 0xbb, 0xd9, 0x81, 0x11, 0x6b, + 0xb5, 0x95, 0x09, 0x8d, 0xad, 0xbf, 0x81, 0xcc, 0x3b, 0x44, 0x29, 0xc4, 0x14, 0x94, 0x96, 0xee, + 0xe6, 0x58, 0x4f, 0x3f, 0xac, 0xa6, 0x63, 0xeb, 0x1b, 0xc4, 0xf0, 0x6a, 0x3c, 0x1e, 0x97, 0x42, + 0xd8, 0xaf, 0x84, 0xf0, 0xea, 0xf8, 0xce, 0x3f, 0xf2, 0x8f, 0xe4, 0xfa, 0xbe, 0x0f, 0xdc, 0x6c, + 0xc8, 0x40, 0x11, 0xbf, 0x9b, 0x10, 0xaf, 0x22, 0xc4, 0xab, 0x08, 0x91, 0xcd, 0x92, 0xa5, 0x0a, + 0xa5, 0x57, 0x92, 0x59, 0x53, 0xdf, 0xad, 0x4a, 0xbd, 0x72, 0xee, 0xe6, 0xa0, 0x62, 0x59, 0x94, + 0x86, 0x45, 0xb1, 0xcc, 0xc3, 0x38, 0x4e, 0xb2, 0xc9, 0xc0, 0xad, 0x34, 0x7a, 0x08, 0xfb, 0x29, + 0x92, 0x28, 0x4c, 0x3b, 0xe0, 0x56, 0x26, 0xd9, 0x40, 0x29, 0xe4, 0x0e, 0x5c, 0x6d, 0x75, 0x25, + 0x45, 0x1e, 0x66, 0xcb, 0x38, 0x29, 0xf2, 0x34, 0x7c, 0x1c, 0x24, 0x99, 0x34, 0x8c, 0x71, 0x4a, + 0x1f, 0x86, 0x12, 0x59, 0x27, 0x11, 0x74, 0x56, 0x0c, 0x22, 0x50, 0x56, 0x50, 0x9a, 0x9a, 0xe8, + 0x6a, 0x86, 0x06, 0x3a, 0xd4, 0x26, 0x61, 0x96, 0xc4, 0x71, 0x4a, 0x57, 0xaf, 0x92, 0x6c, 0xcc, + 0x2a, 0xe4, 0x86, 0x31, 0x44, 0xef, 0xa2, 0x41, 0xbe, 0x8a, 0x72, 0xd3, 0x02, 0x6b, 0x76, 0xb4, + 0x61, 0xc4, 0x20, 0xa5, 0x7b, 0x1e, 0xe6, 0xda, 0x9a, 0x8e, 0x5c, 0x1c, 0xaf, 0x4c, 0x3e, 0x9c, + 0x0b, 0xb6, 0x72, 0x72, 0xe5, 0xff, 0x96, 0x75, 0xeb, 0x2d, 0x3b, 0xff, 0x43, 0x5b, 0x64, 0xb1, + 0x44, 0xde, 0x61, 0x6f, 0x6a, 0x40, 0x9b, 0xea, 0x54, 0x4d, 0x2b, 0x2e, 0x40, 0x88, 0xcb, 0x96, + 0x7d, 0xd7, 0x3c, 0x85, 0x04, 0xbc, 0x64, 0x79, 0xb9, 0xe6, 0x38, 0x51, 0x3e, 0x06, 0x56, 0xfa, + 0x8b, 0xb2, 0x68, 0xf1, 0x0e, 0xcb, 0x94, 0x2c, 0x7c, 0x92, 0xae, 0xac, 0x92, 0xfa, 0xae, 0xbd, + 0xdc, 0x42, 0x51, 0x5b, 0xbe, 0xff, 0x56, 0x0a, 0x95, 0x0e, 0x17, 0xef, 0x92, 0xc5, 0x56, 0x6d, + 0xd3, 0x6b, 0xa7, 0x74, 0xdc, 0x30, 0x66, 0xb9, 0x47, 0x14, 0x02, 0xd6, 0x67, 0x50, 0x53, 0xdb, + 0x29, 0x68, 0x16, 0x63, 0x6b, 0x19, 0xcd, 0x79, 0x01, 0x94, 0xe4, 0x2c, 0x41, 0xba, 0x56, 0x18, + 0x41, 0x64, 0xe0, 0x20, 0xa3, 0xae, 0x0e, 0xd4, 0x18, 0x41, 0xe0, 0x27, 0x4e, 0x16, 0x24, 0x89, + 0x03, 0x03, 0x95, 0x03, 0x62, 0x24, 0x9a, 0x90, 0xfe, 0xd0, 0x83, 0x2f, 0xe4, 0xc4, 0xc0, 0x68, + 0xc8, 0xeb, 0x0f, 0x88, 0x98, 0xc9, 0xf8, 0xb1, 0x94, 0x8c, 0x66, 0x1f, 0xa7, 0x4c, 0xbd, 0xed, + 0x33, 0x36, 0x25, 0x8c, 0xd0, 0xc5, 0x62, 0x52, 0x81, 0x2b, 0x8e, 0xf6, 0x31, 0x2c, 0x95, 0x1e, + 0xb4, 0x57, 0x29, 0x69, 0x87, 0xcb, 0x1e, 0xe8, 0x30, 0xc8, 0x22, 0xa1, 0xf7, 0x3f, 0xb3, 0x07, + 0x08, 0xe4, 0xc4, 0x25, 0xfb, 0x3e, 0xfc, 0x19, 0x27, 0xa3, 0x3c, 0x14, 0x53, 0xf2, 0x62, 0x9c, + 0xa4, 0x69, 0x60, 0xbc, 0x72, 0xdd, 0x7d, 0xd8, 0x02, 0x03, 0x42, 0xa8, 0x71, 0xd1, 0x23, 0xbe, + 0x3f, 0x3d, 0x5a, 0x1c, 0x9c, 0xf5, 0xfe, 0xbc, 0xf0, 0x0e, 0x88, 0x77, 0x30, 0x3d, 0x58, 0x1c, + 0x4d, 0x3b, 0x07, 0xf0, 0x75, 0x04, 0xb1, 0xae, 0xfa, 0xf2, 0x7d, 0xd2, 0x43, 0xb8, 0x69, 0xe7, + 0xe8, 0x4f, 0xa3, 0x7b, 0x02, 0x02, 0x5b, 0x4c, 0x4e, 0x5e, 0x00, 0x89, 0x20, 0x4e, 0x29, 0x21, + 0x94, 0x9b, 0x71, 0xf2, 0x5c, 0xc2, 0x80, 0xa0, 0x52, 0xc2, 0x1e, 0xfe, 0x0b, 0xc2, 0x2b, 0x45, + 0x88, 0xd3, 0xdb, 0x11, 0xd4, 0xa8, 0x09, 0xbf, 0x1e, 0xef, 0x80, 0x17, 0x3d, 0xb5, 0x8e, 0xe1, + 0xfb, 0x36, 0xa1, 0xc4, 0x5b, 0x5a, 0x21, 0x26, 0x4b, 0x6a, 0x67, 0xeb, 0x76, 0xd9, 0x82, 0x04, + 0x33, 0xac, 0x14, 0x40, 0x7f, 0x02, 0xff, 0xa7, 0x73, 0x8e, 0x74, 0xa7, 0x8f, 0x24, 0xc9, 0xc8, + 0xbc, 0xa0, 0x24, 0x52, 0xbc, 0x97, 0x88, 0x48, 0x8b, 0xda, 0xbf, 0x4e, 0x34, 0xfa, 0x44, 0xb9, + 0x72, 0x0a, 0xa1, 0x84, 0x40, 0xb2, 0x24, 0xa6, 0x94, 0x94, 0x12, 0x22, 0x54, 0xca, 0x9a, 0x08, + 0x46, 0xc0, 0xcf, 0x93, 0x8c, 0xde, 0x13, 0x69, 0x73, 0xa4, 0x80, 0xf0, 0x04, 0x79, 0x00, 0x02, + 0xab, 0x19, 0xb2, 0x9b, 0xc6, 0x04, 0x44, 0x4a, 0xee, 0x68, 0xca, 0xee, 0x65, 0xaf, 0x02, 0xc3, + 0xe9, 0xd1, 0x34, 0xcc, 0x26, 0x94, 0x24, 0xa2, 0x50, 0xa0, 0x8e, 0x5e, 0x10, 0xa1, 0x9a, 0xf3, + 0x20, 0x1c, 0x81, 0xeb, 0xc6, 0x55, 0xcd, 0x30, 0x8b, 0x31, 0x8f, 0x1c, 0x27, 0x7c, 0x66, 0x21, + 0x12, 0x15, 0x7d, 0x1d, 0xf2, 0x31, 0x8b, 0x28, 0x19, 0x27, 0x59, 0x52, 0x4c, 0x69, 0x6c, 0x83, + 0x14, 0x4b, 0x4c, 0x21, 0xe7, 0x88, 0x21, 0x42, 0x36, 0x18, 0x99, 0xe7, 0x29, 0x0b, 0x63, 0x40, + 0x08, 0x6d, 0x1c, 0x8d, 0x69, 0x91, 0xe0, 0x5a, 0x45, 0xca, 0x84, 0x43, 0x2e, 0x99, 0xe4, 0x8e, + 0xd0, 0x87, 0x04, 0x64, 0x94, 0x4d, 0x4a, 0x19, 0xd7, 0xf1, 0xe5, 0x34, 0x8b, 0x92, 0x54, 0x22, + 0x74, 0xc8, 0x8b, 0x2d, 0x42, 0xff, 0x7e, 0x99, 0x4b, 0xed, 0x2c, 0x04, 0x38, 0xa5, 0xe8, 0x53, + 0xa5, 0x2f, 0x5f, 0x51, 0x17, 0x04, 0xdf, 0xa9, 0x32, 0x6f, 0x17, 0x61, 0x92, 0x86, 0x77, 0x29, + 0x48, 0x5b, 0x62, 0xfd, 0x9a, 0xae, 0xc8, 0x9f, 0x51, 0x57, 0x3b, 0x24, 0x9d, 0x6d, 0xbf, 0xd8, + 0x95, 0x6e, 0x63, 0x6a, 0x5c, 0x6a, 0x03, 0x7a, 0x01, 0xcc, 0xba, 0x9b, 0x06, 0x64, 0xd9, 0x11, + 0xac, 0x18, 0x05, 0x1d, 0xcf, 0xce, 0x1f, 0x4e, 0x59, 0x1a, 0x2c, 0x57, 0xb6, 0xd0, 0xbf, 0x9c, + 0x46, 0x22, 0xa8, 0x4d, 0xc7, 0x24, 0xfd, 0x67, 0xcc, 0x01, 0x40, 0xde, 0xb0, 0xff, 0xd0, 0xf9, + 0x0f, 0x80, 0x30, 0x2d, 0xbb, 0x84, 0x39, 0xa7, 0xd9, 0x44, 0x4c, 0x03, 0x9c, 0xe7, 0x48, 0x0f, + 0x65, 0xcf, 0x3e, 0x8e, 0xc7, 0x45, 0x70, 0x01, 0xfe, 0xc6, 0x91, 0xd9, 0x83, 0xd9, 0x04, 0xed, + 0xfa, 0x87, 0xbd, 0xae, 0x6f, 0x75, 0x0e, 0x6d, 0xcd, 0xf6, 0x5b, 0xce, 0xc3, 0xc7, 0xe0, 0xfa, + 0xc6, 0x06, 0x87, 0xf2, 0x39, 0x5c, 0xd0, 0xe0, 0x8d, 0x74, 0x7b, 0x0d, 0xaf, 0xe7, 0x1f, 0xae, + 0xbd, 0x1e, 0xb6, 0x5b, 0x4e, 0xce, 0x3f, 0x80, 0xbf, 0xd2, 0xc9, 0x49, 0x1f, 0x87, 0x21, 0x46, + 0xba, 0x37, 0xdf, 0xb7, 0x3d, 0xff, 0xad, 0xe7, 0xda, 0x1e, 0x02, 0xc2, 0x0f, 0xf1, 0x7c, 0xdb, + 0x6f, 0xf6, 0x6c, 0x05, 0x69, 0x42, 0x20, 0xc8, 0x45, 0x1f, 0xfe, 0x39, 0x87, 0x31, 0xaf, 0x7f, + 0xe5, 0x1d, 0x9c, 0x79, 0xbd, 0x2b, 0xcf, 0x3d, 0xf3, 0xfc, 0xab, 0xfe, 0x39, 0x0e, 0xfc, 0x77, + 0xe5, 0x14, 0xdf, 0x20, 0x27, 0xe8, 0xf3, 0xfe, 0xbd, 0x9c, 0x20, 0x51, 0xa7, 0x3d, 0xe7, 0xa0, + 0x6f, 0xfb, 0x40, 0x31, 0x36, 0x24, 0xe1, 0xa7, 0x48, 0x8f, 0x73, 0xb8, 0x4f, 0xd4, 0x90, 0xaf, + 0xf8, 0x3b, 0x95, 0x7d, 0xf8, 0xe9, 0x97, 0xe3, 0xbe, 0x82, 0xd6, 0x53, 0xf5, 0xb8, 0x84, 0xbe, + 0xf0, 0x0e, 0x1d, 0xcf, 0xee, 0x3b, 0x6e, 0xff, 0x14, 0x5a, 0xfe, 0x81, 0x6c, 0x12, 0x68, 0xee, + 0x1f, 0x41, 0xd3, 0xf3, 0xb1, 0x79, 0x08, 0x2d, 0x7f, 0xff, 0xdc, 0xeb, 0x39, 0xfd, 0xbe, 0x7d, + 0xe4, 0x1c, 0xc2, 0x02, 0xf0, 0xd3, 0x87, 0xb1, 0xbe, 0x7d, 0x2c, 0xc1, 0xe5, 0xc8, 0xb1, 0xe3, + 0x1f, 0x9d, 0x03, 0x38, 0x34, 0x3d, 0x57, 0xb6, 0xf7, 0x01, 0x08, 0x20, 0x71, 0xee, 0x01, 0x36, + 0x11, 0xcd, 0x29, 0x34, 0x8f, 0x7c, 0x8d, 0xfb, 0xc0, 0x39, 0xee, 0x55, 0x2b, 0x2a, 0x32, 0x2e, + 0x60, 0x96, 0xb7, 0x0f, 0xb3, 0x8e, 0x3c, 0x44, 0xe6, 0x1d, 0x23, 0xb2, 0xa3, 0xfe, 0xf9, 0x31, + 0xf6, 0xc2, 0x42, 0xc7, 0xfb, 0x67, 0x08, 0x76, 0x85, 0x68, 0xfa, 0xe7, 0x6b, 0xe0, 0xda, 0x1e, + 0x0c, 0xab, 0xb3, 0x24, 0xa8, 0xe6, 0xc7, 0xb1, 0x89, 0xa7, 0xc9, 0xff, 0x37, 0xd5, 0xae, 0x1d, + 0x64, 0xd3, 0xe4, 0xcb, 0xc7, 0xac, 0x4c, 0xad, 0xd4, 0xa1, 0x76, 0xc6, 0x16, 0xf4, 0x92, 0x87, + 0xc5, 0x34, 0x0a, 0x33, 0xe8, 0xb1, 0xc1, 0x51, 0x9f, 0x9a, 0x35, 0xa4, 0xd4, 0x61, 0xb0, 0x0c, + 0x15, 0xbf, 0x75, 0x9b, 0xe8, 0x7f, 0x04, 0xf4, 0x56, 0xed, 0x90, 0x2c, 0xe7, 0x51, 0x1b, 0x4e, + 0xec, 0x86, 0xb5, 0x04, 0x53, 0x22, 0x1c, 0x4d, 0x9a, 0x05, 0x2f, 0x3d, 0xc8, 0xb3, 0xb2, 0x42, + 0x90, 0xb0, 0xc1, 0xee, 0xbf, 0xe6, 0x94, 0x3f, 0x7e, 0x06, 0x87, 0x1c, 0x81, 0xab, 0x7e, 0x9b, + 0xa6, 0xa6, 0xd1, 0x38, 0x96, 0x19, 0xd6, 0x30, 0x19, 0x9b, 0xa1, 0x03, 0x47, 0xaf, 0xf7, 0x61, + 0x34, 0x35, 0x4d, 0x61, 0x73, 0x2b, 0x38, 0x59, 0x0a, 0x94, 0xd3, 0x5b, 0x21, 0x78, 0x02, 0x19, + 0x18, 0x35, 0x8d, 0x38, 0x14, 0x61, 0x47, 0xf0, 0x39, 0x85, 0x8c, 0xcd, 0xb0, 0x82, 0x80, 0xbe, + 0x7e, 0x6d, 0xc2, 0x9a, 0xae, 0xb5, 0x02, 0x4e, 0x9c, 0x54, 0x52, 0x7a, 0xe2, 0xf5, 0xcb, 0x5e, + 0x9b, 0x59, 0xea, 0x18, 0x8f, 0xd8, 0xe9, 0x89, 0xfb, 0xfa, 0x35, 0x1d, 0xf9, 0x87, 0x87, 0x16, + 0x2c, 0x63, 0xa2, 0xab, 0xca, 0x02, 0x6f, 0x98, 0x8d, 0x02, 0xaf, 0xf7, 0xfa, 0x35, 0x1f, 0x41, + 0x73, 0x6f, 0xcf, 0x92, 0x1e, 0x4b, 0x92, 0x76, 0xa1, 0x28, 0xdb, 0xcb, 0xac, 0xa7, 0x27, 0x93, + 0x07, 0x99, 0x35, 0xa4, 0x29, 0x84, 0x58, 0x1e, 0xd0, 0xa1, 0x61, 0x04, 0x81, 0x80, 0x45, 0x80, + 0xfb, 0x57, 0xc6, 0x9e, 0xe9, 0xf5, 0xfa, 0xfd, 0xbe, 0xef, 0x1d, 0xfe, 0xa8, 0xe4, 0x08, 0x71, + 0x88, 0xcd, 0x4c, 0x6b, 0x34, 0x72, 0x2d, 0x47, 0xb0, 0xcf, 0x40, 0x7c, 0x36, 0x01, 0x18, 0x0b, + 0xf2, 0xdc, 0xf8, 0xb3, 0x08, 0xb9, 0x30, 0x7b, 0xb6, 0xe1, 0x1a, 0x96, 0xa5, 0x25, 0x95, 0x06, + 0xd1, 0x7b, 0xd3, 0xc0, 0xfc, 0x04, 0xc4, 0x90, 0x3a, 0xd2, 0x65, 0xff, 0x12, 0xce, 0xc0, 0x6a, + 0x1b, 0x22, 0xb2, 0x53, 0x07, 0xbd, 0x7b, 0x83, 0x36, 0xbe, 0x5e, 0xc0, 0x82, 0xf1, 0x62, 0xb7, + 0xb0, 0x6c, 0xfa, 0x0c, 0x00, 0xe0, 0x34, 0x6c, 0xb1, 0x03, 0x40, 0xe9, 0x83, 0xa1, 0xf4, 0x0f, + 0x61, 0x60, 0xeb, 0xdf, 0x2f, 0x50, 0x31, 0x20, 0x12, 0x52, 0x48, 0x96, 0x40, 0x5e, 0x18, 0x02, + 0x0d, 0x1b, 0x72, 0x8f, 0xfc, 0xef, 0x73, 0x0e, 0xa1, 0x90, 0x7f, 0xe2, 0x2c, 0x97, 0xf8, 0xd0, + 0xfd, 0x38, 0x98, 0x18, 0x3f, 0xaf, 0xb9, 0x3f, 0x52, 0x6b, 0x4f, 0x2e, 0xb0, 0x67, 0x80, 0x5b, + 0xd2, 0x82, 0x49, 0xa4, 0x60, 0x92, 0x2c, 0x9f, 0x0b, 0x54, 0x10, 0x47, 0x45, 0x1d, 0x29, 0x00, + 0xc3, 0x4e, 0x9c, 0x45, 0x98, 0xce, 0x69, 0x20, 0xa0, 0xb5, 0x21, 0x32, 0x75, 0xd0, 0x45, 0xa0, + 0x4a, 0x64, 0x9f, 0x54, 0x57, 0x53, 0x64, 0xc9, 0x16, 0x66, 0xd4, 0x7a, 0xf6, 0x3c, 0xc7, 0x22, + 0x57, 0x69, 0x3c, 0xdb, 0x41, 0x35, 0xdf, 0x51, 0x7e, 0x9a, 0x7e, 0x29, 0x77, 0xb3, 0xa8, 0xef, + 0x66, 0xb1, 0x8b, 0xb4, 0x6a, 0x53, 0x8b, 0x36, 0x85, 0x5b, 0xb7, 0xb6, 0x78, 0x66, 0x71, 0x96, + 0xe2, 0xea, 0x00, 0x52, 0x93, 0x75, 0x5d, 0xf0, 0x40, 0xf9, 0x8e, 0x11, 0x4d, 0x71, 0x5c, 0xa7, + 0x98, 0xa3, 0x95, 0x70, 0xb4, 0x12, 0xd0, 0xef, 0xb8, 0x4e, 0x7e, 0xa3, 0x98, 0x61, 0xd8, 0xb1, + 0x24, 0x5c, 0x75, 0x6e, 0xa5, 0x39, 0xde, 0x4d, 0x33, 0x05, 0xd3, 0x56, 0x33, 0x4f, 0x91, 0x71, + 0x2c, 0xaa, 0x21, 0xfc, 0x0e, 0x32, 0xd7, 0x7a, 0xb4, 0xae, 0x0a, 0xc8, 0x79, 0x81, 0xd6, 0x81, + 0x8a, 0xf7, 0x5d, 0xe3, 0x75, 0x4f, 0x14, 0xe6, 0x90, 0xa6, 0xc5, 0xa7, 0xd3, 0x24, 0x8d, 0xcd, + 0xc4, 0xda, 0x39, 0x94, 0xee, 0x1e, 0x02, 0x23, 0x70, 0x5f, 0x06, 0xfc, 0xf5, 0x6b, 0x10, 0x92, + 0xfc, 0xdd, 0x05, 0x18, 0x5b, 0x76, 0x5d, 0x9c, 0xb3, 0xf0, 0x0b, 0xbd, 0xa0, 0xef, 0x78, 0x38, + 0x31, 0xd1, 0xcb, 0xa0, 0x39, 0x5b, 0xb0, 0x6f, 0x54, 0x5c, 0x32, 0x96, 0x8a, 0x24, 0x57, 0x52, + 0xac, 0x8f, 0x35, 0x75, 0xd0, 0xac, 0xb9, 0xdf, 0xf6, 0xc8, 0x52, 0x6d, 0x25, 0xfd, 0x4e, 0xa7, + 0xbb, 0x91, 0x82, 0xd1, 0x0d, 0x17, 0xac, 0x10, 0x33, 0x99, 0xca, 0xd1, 0x6b, 0x7e, 0x03, 0x94, + 0x39, 0x9c, 0x42, 0xfe, 0x1a, 0xd1, 0xa6, 0xa3, 0xb4, 0x1b, 0x76, 0x66, 0x59, 0x4a, 0xf6, 0xc3, + 0xef, 0x9b, 0xa7, 0xfb, 0x60, 0xf6, 0xf6, 0x1d, 0x65, 0xb6, 0xc4, 0xf5, 0xfc, 0xe0, 0x33, 0x4e, + 0x8e, 0x59, 0x55, 0x78, 0x92, 0xb0, 0xcf, 0x85, 0x17, 0x3b, 0xfb, 0x8a, 0xcf, 0x0a, 0xb5, 0x00, + 0xaf, 0xb3, 0x1b, 0x58, 0x1b, 0x45, 0x78, 0x1d, 0x42, 0x6b, 0xb5, 0x56, 0x1d, 0x65, 0x0c, 0x81, + 0x81, 0xa5, 0x82, 0x90, 0x77, 0xca, 0x6e, 0x13, 0x8e, 0x18, 0xf2, 0xcc, 0x6c, 0xd8, 0x1f, 0xef, + 0xfe, 0xc0, 0x10, 0x0f, 0x9d, 0x3c, 0xa1, 0x85, 0x29, 0xf1, 0x59, 0xeb, 0x4d, 0xb8, 0x86, 0x10, + 0x7b, 0x83, 0xdb, 0xd0, 0xc4, 0xb8, 0x17, 0xdc, 0xda, 0xe4, 0x87, 0xa5, 0x58, 0xc1, 0x3f, 0x74, + 0x95, 0x3f, 0xdc, 0x6e, 0xac, 0xb9, 0x17, 0x18, 0x96, 0xd1, 0x50, 0xe1, 0xb6, 0xcc, 0x82, 0xe6, + 0x84, 0xb5, 0x6e, 0xb5, 0xdc, 0x38, 0xe6, 0x0d, 0xd4, 0xc1, 0x4e, 0xfc, 0x0a, 0x27, 0x21, 0x02, + 0xd5, 0x75, 0x51, 0x79, 0xa0, 0xed, 0xe9, 0xc5, 0x96, 0x89, 0xb6, 0xd4, 0x08, 0xa7, 0xe0, 0x91, + 0xae, 0xa9, 0x37, 0x34, 0xa3, 0x52, 0x0a, 0xd4, 0x01, 0xe9, 0x2e, 0x1a, 0x4b, 0xe5, 0x3b, 0x56, + 0x5a, 0x48, 0x1f, 0xfd, 0x2c, 0x99, 0x35, 0xf3, 0x03, 0x04, 0x18, 0xfe, 0x45, 0xe0, 0xda, 0xfc, + 0x9b, 0xb2, 0x32, 0x16, 0x70, 0x47, 0x6e, 0x98, 0x1d, 0x42, 0x4b, 0x7a, 0xd5, 0x2c, 0x60, 0x9d, + 0x70, 0xcf, 0x5b, 0xa7, 0x7a, 0xa9, 0xc9, 0xad, 0x25, 0x24, 0x0a, 0xfc, 0xe9, 0xe9, 0x1e, 0x4e, + 0xa5, 0xec, 0xde, 0x51, 0x54, 0x39, 0x39, 0x97, 0x8d, 0x77, 0x74, 0x1c, 0xce, 0x53, 0xc4, 0x26, + 0x3a, 0x1c, 0x59, 0x83, 0xbe, 0xdf, 0x20, 0x81, 0x5a, 0xb7, 0x67, 0x0c, 0xce, 0xf0, 0x9f, 0x58, + 0xf1, 0xa1, 0xca, 0xd9, 0x02, 0xd1, 0x31, 0x61, 0x11, 0x98, 0x02, 0x4a, 0x09, 0x23, 0x75, 0x85, + 0xdc, 0x84, 0xee, 0x66, 0x32, 0x4b, 0xb3, 0x59, 0x1a, 0x5f, 0x6a, 0x78, 0xfa, 0xbc, 0x86, 0x6b, + 0xb4, 0xe8, 0xa4, 0x74, 0x53, 0xb9, 0xaa, 0x35, 0x86, 0x97, 0x81, 0x1e, 0xc0, 0x74, 0x47, 0x83, + 0xf4, 0x0e, 0x7e, 0x12, 0xd3, 0xa4, 0xf8, 0x28, 0x13, 0x83, 0xc0, 0x1d, 0x94, 0x58, 0xbc, 0x63, + 0xbf, 0x3e, 0xd0, 0x1f, 0xd4, 0x3e, 0xf6, 0xe5, 0xe6, 0x6c, 0x4b, 0x06, 0x32, 0x69, 0x4b, 0x1a, + 0x47, 0x3d, 0x0b, 0xd0, 0x8a, 0xf2, 0x7f, 0xf2, 0x1b, 0x72, 0x91, 0xfa, 0x8a, 0x5f, 0x45, 0xd6, + 0x88, 0x64, 0x7f, 0x01, 0x4f, 0xcb, 0x09, 0xee, 0x42, 0x43, 0x9f, 0x4d, 0xd6, 0x4a, 0x69, 0x6c, + 0x46, 0x07, 0xba, 0x19, 0x15, 0x6a, 0x3a, 0x9e, 0x40, 0x24, 0x88, 0x1d, 0x96, 0x49, 0xdd, 0x98, + 0xe7, 0x41, 0x36, 0x4f, 0x53, 0xbb, 0xea, 0x40, 0x93, 0x91, 0x5d, 0x2b, 0x5a, 0x76, 0x81, 0x9a, + 0x66, 0x41, 0x39, 0x7d, 0xb7, 0x61, 0x99, 0x34, 0xa0, 0x5f, 0x57, 0x6b, 0xe0, 0xb0, 0x54, 0xe5, + 0x3a, 0x11, 0x49, 0x8b, 0x82, 0x74, 0x55, 0xf3, 0x35, 0x9b, 0xfc, 0x2d, 0xdb, 0xa2, 0x91, 0x57, + 0x7d, 0x86, 0x7d, 0x0b, 0xae, 0xee, 0x59, 0x6d, 0x5e, 0x91, 0x01, 0x79, 0x06, 0x06, 0xfd, 0xbf, + 0xb5, 0xba, 0xad, 0x49, 0xab, 0x99, 0x82, 0x68, 0x97, 0x80, 0xb9, 0x50, 0x9c, 0x2c, 0x20, 0x38, + 0xa2, 0xf6, 0xbe, 0xab, 0x69, 0x45, 0x50, 0x77, 0x5a, 0x36, 0x8e, 0x9e, 0xae, 0x77, 0x5e, 0xc6, + 0xc7, 0xf6, 0x8c, 0x86, 0x9a, 0xa8, 0xd5, 0x4a, 0x0d, 0x01, 0xdd, 0x58, 0xa3, 0xa8, 0xeb, 0xf0, + 0xf7, 0x22, 0xaa, 0x54, 0xad, 0x8d, 0xee, 0x5b, 0x11, 0xed, 0xd0, 0x5c, 0x9b, 0x43, 0x1e, 0x43, + 0xb9, 0x32, 0xdf, 0xdf, 0x02, 0xcf, 0xd5, 0x1d, 0xbf, 0x35, 0xc4, 0xb0, 0xcb, 0x63, 0x3a, 0x0f, + 0x9d, 0xc6, 0x7c, 0x3d, 0xf9, 0xf7, 0x6f, 0x9b, 0xfc, 0xb8, 0x07, 0x67, 0x6a, 0x21, 0x13, 0x4c, + 0x81, 0x9a, 0x68, 0xe0, 0x47, 0x06, 0x19, 0xe4, 0xd9, 0xe5, 0xc5, 0xb9, 0x2e, 0x6c, 0x6c, 0xa9, + 0x5c, 0x90, 0x87, 0x59, 0x9a, 0x15, 0x81, 0x81, 0x37, 0xcc, 0x83, 0x6e, 0xf7, 0xfe, 0xfe, 0xde, + 0xb9, 0xdf, 0x77, 0x18, 0x9f, 0x74, 0x7d, 0xd7, 0x75, 0xf1, 0x68, 0x6e, 0x10, 0x79, 0x96, 0x0e, + 0x0c, 0xbc, 0xff, 0x33, 0x88, 0x2a, 0x85, 0xe8, 0x2f, 0x5d, 0xf7, 0xd0, 0x05, 0x13, 0x2c, 0x7f, + 0x0c, 0x5e, 0x1d, 0x1d, 0xc1, 0x44, 0x77, 0x08, 0x9d, 0x9c, 0x7d, 0xa1, 0x03, 0x02, 0x1d, 0xf8, + 0x5f, 0xd9, 0xd1, 0x51, 0x65, 0x15, 0xd2, 0xc1, 0x4b, 0x04, 0xdd, 0x15, 0x03, 0xbd, 0x21, 0x56, + 0x95, 0x06, 0xc4, 0x75, 0x3c, 0x9b, 0x1c, 0x0d, 0x55, 0xa9, 0xfb, 0xd8, 0xde, 0xbf, 0x3a, 0x38, + 0x3b, 0xb8, 0xea, 0x9d, 0x1d, 0x5e, 0x79, 0xc7, 0x6f, 0x7d, 0xdb, 0x97, 0xe5, 0x1d, 0x97, 0xf4, + 0x6d, 0xdf, 0x3b, 0xf3, 0xfa, 0xb5, 0x1e, 0x2c, 0x39, 0x1c, 0x03, 0xa0, 0xef, 0xc2, 0x0c, 0xef, + 0xf0, 0x6a, 0xff, 0xec, 0xf8, 0xa2, 0x6f, 0xf7, 0xce, 0xb0, 0xf4, 0x73, 0x7c, 0xd6, 0xbf, 0xea, + 0x01, 0xb2, 0xa3, 0x2b, 0xaf, 0x7f, 0xe6, 0x79, 0x57, 0x47, 0x30, 0x86, 0x05, 0x08, 0xf9, 0x79, + 0x08, 0x9f, 0xde, 0x7e, 0xbd, 0x18, 0x24, 0xb4, 0xcf, 0x29, 0x6f, 0x38, 0x02, 0xa3, 0xbc, 0xf3, + 0x33, 0xaa, 0x31, 0xe9, 0x9c, 0xf4, 0xe6, 0x2a, 0xc7, 0x5b, 0x8e, 0x40, 0x30, 0xd5, 0x03, 0xbf, + 0xab, 0x81, 0xd8, 0xc1, 0x42, 0x60, 0x23, 0xc9, 0x05, 0xef, 0x20, 0x9e, 0xcf, 0xf4, 0x85, 0xa3, + 0xca, 0xeb, 0xbf, 0xb0, 0x98, 0x3a, 0xca, 0xbf, 0xac, 0xa7, 0xb6, 0xf5, 0x73, 0x17, 0x68, 0x0b, + 0x6e, 0x87, 0xf1, 0x3c, 0x37, 0x7d, 0x03, 0xd8, 0x6a, 0xdb, 0xf0, 0x57, 0x67, 0xef, 0x58, 0xfb, + 0x1b, 0x57, 0xdd, 0x92, 0xcf, 0x6f, 0x4f, 0x90, 0x9e, 0x39, 0x39, 0x35, 0xdd, 0xf3, 0x57, 0x32, + 0x9e, 0x8d, 0x74, 0x6c, 0x29, 0xad, 0x49, 0x55, 0x65, 0x95, 0x61, 0x21, 0x06, 0x11, 0x72, 0x30, + 0x44, 0x0c, 0xf4, 0xd0, 0x83, 0x89, 0x80, 0xfc, 0x31, 0xe5, 0xef, 0x4e, 0xd6, 0x70, 0x10, 0x29, + 0x55, 0x9d, 0xdf, 0x44, 0x6c, 0x3d, 0x8f, 0x9b, 0x7e, 0xf9, 0xb5, 0x7e, 0x68, 0x41, 0x82, 0xe4, + 0xbd, 0x16, 0x46, 0x11, 0xf9, 0x25, 0x2f, 0x74, 0xac, 0x61, 0x59, 0x8e, 0xfa, 0x15, 0x0d, 0x6d, + 0xd4, 0x73, 0xdd, 0x9f, 0x4a, 0xdd, 0xd4, 0x45, 0x74, 0x7c, 0x60, 0x92, 0x51, 0x63, 0xb0, 0xd1, + 0xad, 0xee, 0xe7, 0x8c, 0xda, 0x9a, 0x61, 0x1a, 0xfd, 0xe7, 0xe7, 0x8f, 0xbf, 0x98, 0xaa, 0x5e, + 0x45, 0x83, 0x37, 0xcb, 0xb2, 0x84, 0x6e, 0x0c, 0xae, 0xdf, 0x0c, 0xf5, 0x83, 0x8f, 0x56, 0x42, + 0x2e, 0x5a, 0xf9, 0x38, 0x9c, 0x8a, 0x64, 0x3e, 0x2e, 0x30, 0x67, 0x32, 0x29, 0xa4, 0xd9, 0x36, + 0x0a, 0x11, 0x12, 0x72, 0x4c, 0xc7, 0x6d, 0xe3, 0x87, 0x25, 0x77, 0x0a, 0x60, 0x9f, 0x9a, 0x9e, + 0xb5, 0x32, 0x30, 0x2f, 0x47, 0x98, 0x9b, 0x15, 0x98, 0x42, 0x2d, 0x4c, 0x67, 0x60, 0x8c, 0xa0, + 0x09, 0xff, 0x25, 0xaf, 0x1c, 0x70, 0x63, 0xd4, 0xe5, 0x83, 0x24, 0x6f, 0x4d, 0xa7, 0x7d, 0xdb, + 0xd5, 0x04, 0x62, 0x96, 0xef, 0xfc, 0x51, 0xb0, 0xec, 0xb6, 0x71, 0x06, 0xac, 0xe6, 0xc0, 0x29, + 0x41, 0xc5, 0x2f, 0x1e, 0xe0, 0xad, 0xcb, 0x6f, 0x17, 0xe7, 0x67, 0xe0, 0x03, 0xff, 0x41, 0xe1, + 0x04, 0x58, 0x08, 0xc8, 0x5e, 0xb1, 0xf3, 0xe7, 0x94, 0xdd, 0xc1, 0x79, 0xe2, 0xc6, 0x5e, 0x62, + 0x1d, 0x65, 0x60, 0x80, 0x11, 0xa7, 0x78, 0x75, 0x02, 0xa8, 0xba, 0x88, 0xda, 0x58, 0xc1, 0xe9, + 0x7f, 0x8b, 0xe6, 0xe1, 0x22, 0x86, 0x6d, 0x96, 0x67, 0x41, 0x86, 0x1e, 0x83, 0x4d, 0xa4, 0x72, + 0xc3, 0xee, 0x17, 0x39, 0xf4, 0xd1, 0x4b, 0xfa, 0x20, 0x6c, 0x83, 0x74, 0x88, 0x21, 0x6d, 0xc3, + 0xc1, 0xbb, 0x85, 0x39, 0x16, 0x8b, 0x18, 0x70, 0xf3, 0x19, 0x4e, 0x9f, 0xe1, 0xa4, 0xd4, 0x9f, + 0x0f, 0x82, 0xce, 0x60, 0xb3, 0x53, 0x1a, 0x7f, 0x0a, 0x53, 0xbc, 0x0f, 0xd0, 0x59, 0x05, 0x82, + 0x22, 0x2d, 0xce, 0x94, 0xd3, 0x71, 0x60, 0x74, 0x81, 0x1c, 0x7b, 0x1b, 0x39, 0x94, 0x73, 0x2c, + 0xff, 0xd0, 0x16, 0x39, 0xc6, 0x7b, 0xec, 0x1f, 0x10, 0x59, 0xe8, 0x6a, 0x0c, 0x90, 0xcf, 0x92, + 0x98, 0x41, 0x9b, 0x36, 0x4c, 0x3d, 0x92, 0x19, 0x65, 0x73, 0x61, 0x4a, 0xe6, 0x56, 0xb6, 0x47, + 0xf7, 0x2d, 0xb9, 0x2a, 0x03, 0xf7, 0x66, 0x1a, 0x9f, 0x3e, 0x7e, 0xbe, 0x84, 0xdd, 0xed, 0x2a, + 0x39, 0x83, 0x32, 0xa2, 0x80, 0x43, 0x29, 0xcb, 0xbf, 0x33, 0x3e, 0x7b, 0x07, 0x89, 0x45, 0xa9, + 0x34, 0xa1, 0x76, 0x89, 0x2a, 0xdd, 0x80, 0x63, 0x26, 0x56, 0xd3, 0xb8, 0xbc, 0xf1, 0x35, 0x43, + 0xcb, 0x7e, 0xe9, 0xad, 0xc2, 0xe2, 0x31, 0x8b, 0xc8, 0xfa, 0x39, 0x12, 0x15, 0x1f, 0xb2, 0x31, + 0x03, 0x5d, 0x4c, 0xc6, 0xe6, 0xb4, 0x10, 0xc1, 0x9a, 0x7d, 0x06, 0x3b, 0x06, 0x3d, 0x65, 0x35, + 0xd3, 0xb5, 0x04, 0x7f, 0xac, 0x2c, 0x25, 0xbc, 0x0f, 0x13, 0x41, 0xc6, 0x54, 0x80, 0x32, 0x96, + 0x71, 0xce, 0xd8, 0x03, 0xf0, 0x3d, 0x43, 0x6e, 0x62, 0x57, 0x5e, 0xd0, 0xa1, 0x15, 0x29, 0x48, + 0x2a, 0xb5, 0xc6, 0xb4, 0x86, 0x72, 0x4a, 0x79, 0x85, 0x64, 0x9a, 0xea, 0x12, 0x46, 0x38, 0xf2, + 0x17, 0x42, 0xb0, 0xb0, 0x3a, 0xa0, 0xaf, 0x40, 0x02, 0xe0, 0xa5, 0x56, 0x25, 0x59, 0x29, 0x6c, + 0x2c, 0xcf, 0xc8, 0xd2, 0x67, 0xb3, 0xd7, 0x00, 0x9b, 0xce, 0x98, 0x20, 0x49, 0x0c, 0xfb, 0x93, + 0x8c, 0x1f, 0x09, 0x52, 0x0e, 0x19, 0x56, 0x8b, 0xd3, 0xe6, 0xc2, 0x80, 0xbb, 0x7e, 0xf3, 0xa2, + 0x99, 0x0c, 0xdc, 0x21, 0x96, 0x64, 0xd1, 0x2c, 0xe1, 0x3c, 0x31, 0x14, 0xa3, 0x80, 0x0e, 0xc5, + 0xde, 0xde, 0xda, 0x41, 0xdc, 0x6a, 0x56, 0x7f, 0x58, 0x02, 0xab, 0xab, 0xb5, 0x55, 0x08, 0x6d, + 0x15, 0xc3, 0xb5, 0x8c, 0x44, 0x43, 0x46, 0xa0, 0x0c, 0x5c, 0x77, 0x88, 0x52, 0x14, 0x0d, 0x02, + 0xf2, 0x79, 0x31, 0x85, 0x83, 0x9b, 0x66, 0x5d, 0xb4, 0x59, 0xbf, 0x95, 0x6a, 0xa5, 0x90, 0xe1, + 0xad, 0x1f, 0x5a, 0x1b, 0x19, 0x73, 0x36, 0x93, 0x07, 0xef, 0x01, 0xb9, 0x85, 0x8d, 0x5e, 0xad, + 0xb6, 0xb0, 0x34, 0xf2, 0xc0, 0x3f, 0x6c, 0xae, 0x54, 0x72, 0x3f, 0xb8, 0x76, 0xed, 0x7e, 0xf9, + 0x07, 0x47, 0xae, 0xea, 0xe3, 0x66, 0x55, 0x56, 0x28, 0x44, 0x80, 0x8b, 0xa1, 0x03, 0x2e, 0xa8, + 0xd9, 0x30, 0x24, 0x54, 0x9e, 0x96, 0x15, 0xc9, 0xfa, 0x39, 0x50, 0x8f, 0x62, 0xd4, 0x42, 0xc3, + 0x3b, 0x61, 0x08, 0xbd, 0x52, 0xc3, 0xe8, 0x89, 0x7f, 0x70, 0x68, 0xe9, 0x9a, 0x1b, 0xf6, 0x82, + 0x1f, 0xc0, 0x65, 0x44, 0x92, 0xcd, 0xe9, 0x4a, 0x4d, 0xe0, 0x81, 0xee, 0xc7, 0x6d, 0xc0, 0xf2, + 0xf9, 0xb0, 0x8e, 0x8c, 0x8d, 0x09, 0x97, 0xa8, 0x5e, 0x2a, 0x6e, 0x92, 0x42, 0xfe, 0x82, 0x80, + 0x9f, 0x9e, 0x0e, 0x5e, 0x06, 0x01, 0xd5, 0x7c, 0x5b, 0x4b, 0x79, 0x07, 0x70, 0xc7, 0x69, 0xf8, + 0x65, 0xb5, 0x46, 0x20, 0x10, 0x01, 0xb5, 0x60, 0xbe, 0x91, 0xcd, 0x67, 0x77, 0x90, 0x61, 0x42, + 0xbc, 0x01, 0x37, 0x04, 0xbd, 0xe2, 0xe9, 0x49, 0x8c, 0x5c, 0xf8, 0xe7, 0x04, 0xe4, 0xf0, 0xf4, + 0xf4, 0xf2, 0x17, 0x39, 0x0e, 0x0b, 0x7c, 0xc8, 0x04, 0x9d, 0x80, 0xc9, 0x0b, 0xab, 0x81, 0x74, + 0x85, 0x44, 0xb0, 0xaf, 0x30, 0x03, 0xc7, 0xf4, 0x6b, 0xae, 0x49, 0xea, 0x78, 0x37, 0x28, 0x1d, + 0x59, 0xad, 0x0b, 0xc2, 0x6b, 0xf7, 0x66, 0xad, 0x57, 0xd7, 0x8e, 0xe3, 0x84, 0x37, 0x43, 0x0a, + 0x9d, 0x01, 0xee, 0x02, 0x57, 0xbb, 0x04, 0x0a, 0xbf, 0x2a, 0x41, 0xda, 0xf1, 0x00, 0x24, 0xea, + 0xcc, 0xc2, 0x7c, 0x5d, 0x9a, 0x31, 0x97, 0xb0, 0x3e, 0xc4, 0x9f, 0x71, 0x1a, 0xca, 0x90, 0xbe, + 0x4d, 0xc1, 0x60, 0x99, 0xd2, 0x88, 0x60, 0x8c, 0x0b, 0xd3, 0xb8, 0xc4, 0x5b, 0x70, 0x7c, 0x17, + 0x89, 0x82, 0xa9, 0xee, 0x60, 0x21, 0xd8, 0x92, 0x59, 0x52, 0x14, 0xc9, 0x44, 0x29, 0xd9, 0x23, + 0x9b, 0x73, 0x72, 0xc7, 0xd9, 0x7d, 0x01, 0x12, 0x21, 0xbf, 0xb3, 0x39, 0x29, 0xa6, 0x6c, 0x9e, + 0xc6, 0x24, 0xe7, 0xec, 0x2e, 0xbc, 0x4b, 0x1f, 0x89, 0x76, 0x40, 0xfa, 0xce, 0x7a, 0x16, 0xc2, + 0xa6, 0x43, 0x2a, 0x00, 0xcb, 0x64, 0x31, 0xc1, 0x8d, 0x04, 0xc5, 0x97, 0xd7, 0xda, 0x30, 0x21, + 0xa7, 0x1c, 0x26, 0x8c, 0xf1, 0x82, 0x1e, 0x2f, 0xab, 0xcb, 0x35, 0x15, 0x15, 0x58, 0x91, 0x02, + 0x69, 0x83, 0x8b, 0x85, 0xb8, 0x44, 0xee, 0x28, 0x80, 0x51, 0x8d, 0x1c, 0xf5, 0x7e, 0x4a, 0x39, + 0x75, 0xc0, 0x19, 0x5e, 0x20, 0x71, 0xf0, 0x2d, 0x27, 0xc5, 0x15, 0x92, 0x97, 0xe0, 0x1d, 0xcb, + 0xc9, 0xda, 0xd6, 0xdf, 0x25, 0x8b, 0xa2, 0x9e, 0x8c, 0x6c, 0x1d, 0xae, 0x36, 0x62, 0xe3, 0x41, + 0xe6, 0xfa, 0x79, 0x04, 0x3a, 0xb3, 0xcd, 0xe1, 0xd6, 0x9d, 0x38, 0x9a, 0xb9, 0x92, 0x37, 0xca, + 0x0d, 0x0e, 0xe8, 0x11, 0xa6, 0x29, 0x90, 0xba, 0x40, 0xec, 0x4e, 0x52, 0x81, 0x07, 0xcb, 0xe0, + 0x44, 0x1e, 0xdb, 0x67, 0xd2, 0xd0, 0xbb, 0xff, 0xd4, 0xf8, 0xff, 0x27, 0xfe, 0xa1, 0x0b, 0x5b, + 0xd6, 0xd2, 0x54, 0x6e, 0xb5, 0x33, 0xd5, 0xca, 0x41, 0x71, 0x70, 0x50, 0x7c, 0xb4, 0xc5, 0xe2, + 0x87, 0x7c, 0xed, 0xb1, 0x58, 0x50, 0x07, 0xb8, 0xe6, 0x37, 0x76, 0x18, 0xb4, 0x5f, 0x92, 0xea, + 0x53, 0x66, 0xe8, 0xd4, 0x1e, 0x83, 0x18, 0x7b, 0xdc, 0x0e, 0x55, 0xd1, 0x1d, 0x23, 0x1e, 0x86, + 0xbf, 0x4a, 0x12, 0x46, 0xe9, 0x18, 0xb2, 0x52, 0x27, 0xbf, 0xd0, 0xc7, 0xc2, 0x64, 0x16, 0x28, + 0x2f, 0x60, 0xc1, 0xc0, 0x03, 0x21, 0x0d, 0x2b, 0xbc, 0xf2, 0xf8, 0xa1, 0xbc, 0x47, 0x21, 0x6b, + 0x7b, 0xe0, 0x9d, 0x4d, 0x76, 0x9d, 0xdd, 0xac, 0x6f, 0x9c, 0x76, 0x10, 0x93, 0xd6, 0x89, 0x29, + 0xf3, 0x54, 0x20, 0xaa, 0xba, 0x90, 0xd9, 0x31, 0x4f, 0xdd, 0xb3, 0xac, 0x5f, 0x49, 0x21, 0x1f, + 0x49, 0x9b, 0x8f, 0xda, 0xf0, 0xfa, 0xb2, 0x64, 0x03, 0x61, 0x75, 0x73, 0x22, 0x9f, 0x32, 0xe8, + 0x27, 0x53, 0x88, 0xaf, 0x70, 0xf0, 0x15, 0x21, 0x24, 0x96, 0xeb, 0x7a, 0x05, 0x38, 0xa4, 0x66, + 0x26, 0x05, 0x8e, 0x1c, 0x2f, 0x03, 0xb6, 0x95, 0x0f, 0x8c, 0xcf, 0x14, 0xdf, 0x87, 0xa8, 0x17, + 0x33, 0xb5, 0x97, 0x2a, 0xf8, 0xbc, 0x83, 0x00, 0x7e, 0xbc, 0x41, 0x59, 0x9f, 0x31, 0xf5, 0x73, + 0x00, 0xbb, 0x68, 0x33, 0x51, 0x51, 0x54, 0xb2, 0x10, 0xed, 0x64, 0x21, 0x92, 0x2c, 0x94, 0xef, + 0xbf, 0x90, 0x85, 0x68, 0x1b, 0x0b, 0x48, 0x38, 0xe4, 0x13, 0x78, 0x67, 0x2f, 0xe9, 0x8f, 0x76, + 0x94, 0x3f, 0x4e, 0x59, 0xfe, 0xa8, 0xa8, 0x85, 0x1c, 0x73, 0x55, 0x9a, 0x1e, 0xb2, 0xa0, 0x98, + 0xb9, 0x85, 0xbc, 0xa7, 0xc9, 0x01, 0xa2, 0x84, 0xbe, 0x16, 0x07, 0x15, 0x41, 0x78, 0xaf, 0xb2, + 0x5d, 0xd5, 0xaa, 0x07, 0x85, 0xa8, 0x12, 0xa0, 0xf3, 0x79, 0x60, 0x18, 0x95, 0x01, 0x50, 0x30, + 0x00, 0x3a, 0x42, 0x75, 0x2a, 0x15, 0x1f, 0x32, 0x5c, 0xdf, 0xaa, 0xc2, 0x2f, 0x8e, 0xa0, 0x47, + 0x46, 0x5f, 0xaf, 0xd4, 0x0f, 0x6f, 0x4b, 0x95, 0xaf, 0x57, 0x63, 0x7b, 0xde, 0x8d, 0x95, 0x43, + 0xc6, 0xfc, 0xea, 0x87, 0x65, 0xd5, 0x81, 0x95, 0x6c, 0xd1, 0x05, 0x1f, 0xfc, 0xa3, 0xe7, 0xba, + 0xab, 0xbf, 0xd9, 0xe4, 0x56, 0x5e, 0xb7, 0x2e, 0x11, 0x4e, 0xbe, 0xee, 0xad, 0xc3, 0x62, 0xed, + 0x5b, 0x7f, 0xf9, 0x8d, 0xaf, 0x7d, 0xfc, 0xf2, 0xac, 0x0d, 0x5c, 0x98, 0x84, 0xfb, 0x10, 0xa7, + 0x83, 0x5c, 0x27, 0xe7, 0xae, 0xdd, 0xf1, 0xb7, 0xdd, 0x2b, 0x7d, 0x98, 0x81, 0xf3, 0x0c, 0x6e, + 0x77, 0xd5, 0xeb, 0x71, 0xa9, 0x7c, 0x65, 0xdd, 0x96, 0x46, 0xaa, 0x6e, 0xc6, 0x36, 0xde, 0x2f, + 0x82, 0xb6, 0x95, 0x46, 0x1b, 0x04, 0xd9, 0x4f, 0x49, 0xeb, 0xfa, 0x68, 0x10, 0xe9, 0x75, 0xd5, + 0xf3, 0xb9, 0x73, 0x3c, 0x7f, 0x1b, 0xf2, 0x3d, 0x86, 0xcd, 0x5f, 0x06, 0x32, 0x25, 0x7b, 0xfd, + 0xba, 0x39, 0x29, 0xc2, 0xfb, 0xed, 0xd6, 0xfd, 0x54, 0xd8, 0xbe, 0xcb, 0x6a, 0x2c, 0x4a, 0x1b, + 0xa3, 0xa1, 0x05, 0x11, 0xaa, 0xd9, 0x51, 0xab, 0xc2, 0xd5, 0x55, 0x90, 0x62, 0x05, 0x71, 0xe3, + 0xe6, 0xe8, 0x8d, 0xbc, 0x15, 0xbd, 0x4e, 0xe2, 0x7f, 0x36, 0xaf, 0x53, 0x6f, 0xde, 0xac, 0x0f, + 0x47, 0x98, 0x92, 0xd3, 0x5d, 0x07, 0x46, 0x7d, 0xc5, 0xb7, 0x89, 0x18, 0x8d, 0xa5, 0x86, 0x57, + 0x57, 0xa3, 0x6e, 0x6c, 0xd2, 0x1a, 0x68, 0xd4, 0x79, 0x1b, 0xc3, 0x8d, 0x32, 0xd6, 0x77, 0x11, + 0x34, 0x54, 0x19, 0x67, 0x2d, 0xcf, 0x9a, 0xd4, 0x42, 0x8f, 0xb1, 0x47, 0xad, 0x6d, 0xc5, 0xc4, + 0xd2, 0xc3, 0x1a, 0x96, 0xd5, 0x32, 0x0a, 0xb1, 0xcd, 0x22, 0x20, 0xaf, 0xaa, 0xb2, 0xaa, 0xad, + 0x46, 0x21, 0x94, 0x45, 0x30, 0xf9, 0x98, 0x40, 0x7d, 0x28, 0xdd, 0x67, 0x01, 0x68, 0xfe, 0x25, + 0x3b, 0xa3, 0x0f, 0xa6, 0xea, 0xb6, 0x85, 0xd4, 0x78, 0xf9, 0xb3, 0x7f, 0x63, 0x29, 0xad, 0x96, + 0x2f, 0x31, 0xb8, 0xcd, 0xaa, 0x93, 0x54, 0x11, 0x71, 0x06, 0x92, 0x75, 0x6d, 0xb7, 0xbe, 0xc7, + 0x15, 0x2a, 0x48, 0x5b, 0x6c, 0xbe, 0x0e, 0x54, 0x26, 0x1d, 0x8d, 0xbc, 0x1e, 0xa4, 0x60, 0xa3, + 0xa3, 0x27, 0xde, 0x7c, 0xb0, 0xa0, 0x4f, 0x37, 0x48, 0x96, 0xe1, 0x1a, 0x58, 0x37, 0x04, 0x37, + 0x67, 0xf6, 0x3a, 0xac, 0xcc, 0xf5, 0xf6, 0xd8, 0xaa, 0x3a, 0xc8, 0x94, 0xab, 0xef, 0x3a, 0x40, + 0xe2, 0x91, 0xff, 0x19, 0x20, 0x38, 0x49, 0x26, 0x7f, 0xd2, 0x12, 0xac, 0x71, 0xa3, 0xba, 0xf3, + 0xe6, 0xbb, 0xf1, 0xbc, 0x45, 0x3f, 0x65, 0x71, 0xf5, 0x2f, 0x3e, 0xf3, 0xd8, 0xa8, 0xb7, 0xd4, + 0xff, 0x5f, 0x8d, 0xae, 0xfa, 0x5f, 0x63, 0xfe, 0x17, 0x66, 0xba, 0xb1, 0x98, 0x32, 0x33, 0x00, + 0x00 +}; diff --git a/wled00/html_other.h b/wled00/html_other.h index 5e34357d..5821b42b 100644 --- a/wled00/html_other.h +++ b/wled00/html_other.h @@ -1,21 +1,27 @@ /* * More web UI HTML source arrays. * This file is auto generated, please don't make any changes manually. - * Instead, see https://github.com/Aircoookie/WLED/wiki/Add-own-functionality#web-ui + * Instead, 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/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 @@ -24,7 +30,7 @@ function B(){window.history.back()}function RS(){window.location="/settings"}fun const char PAGE_dmxmap[] PROGMEM = R"=====( DMX Map
...
)====="; @@ -35,90 +41,339 @@ 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.10.1
-Download the latest binary: -
-
)====="; +const uint16_t PAGE_update_length = 616; +const uint8_t PAGE_update[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0x75, 0x53, 0x4d, 0x6f, 0xd4, 0x30, + 0x10, 0xbd, 0xe7, 0x57, 0x18, 0x9f, 0x76, 0x25, 0xd6, 0x2e, 0x1f, 0x17, 0x4a, 0x92, 0x42, 0x69, + 0x85, 0x2a, 0x21, 0xb5, 0x52, 0x5b, 0x10, 0x27, 0xe4, 0xd8, 0x93, 0x8d, 0x59, 0xc7, 0x4e, 0xed, + 0xc9, 0xae, 0x56, 0xa8, 0xff, 0x9d, 0x89, 0xb3, 0x5b, 0x10, 0x1f, 0x97, 0x28, 0xce, 0xbc, 0x79, + 0x9e, 0x79, 0xef, 0xa5, 0x7c, 0x76, 0x71, 0xfd, 0xe1, 0xee, 0xeb, 0xcd, 0x25, 0xeb, 0xb0, 0x77, + 0x75, 0x79, 0x78, 0x82, 0x32, 0x75, 0xd9, 0x03, 0x2a, 0xa6, 0x83, 0x47, 0xf0, 0x58, 0xf1, 0x9d, + 0x35, 0xd8, 0x55, 0x06, 0xb6, 0x56, 0xc3, 0x2a, 0x1f, 0x38, 0xf3, 0xaa, 0x87, 0x8a, 0x6f, 0x2d, + 0xec, 0x86, 0x10, 0x91, 0xd7, 0x45, 0x89, 0x16, 0x1d, 0xd4, 0x5f, 0x3e, 0x5d, 0x5e, 0xb0, 0xfb, + 0xc1, 0x28, 0x84, 0x52, 0xce, 0x9f, 0xca, 0xa4, 0xa3, 0x1d, 0xb0, 0x2e, 0xda, 0xd1, 0x6b, 0xb4, + 0xc1, 0xb3, 0xf3, 0xc5, 0xf2, 0xc7, 0xce, 0x7a, 0x13, 0x76, 0xa2, 0xb3, 0x09, 0x43, 0xdc, 0x8b, + 0x46, 0xe9, 0xcd, 0x62, 0xf9, 0xf8, 0x04, 0xb9, 0x27, 0x88, 0x09, 0x7a, 0xec, 0x69, 0x02, 0xb1, + 0x06, 0xbc, 0x74, 0x30, 0xbd, 0x9e, 0xef, 0xaf, 0xcc, 0x82, 0x8f, 0x2d, 0x5f, 0x8a, 0x84, 0x7b, + 0x07, 0xc2, 0xd8, 0x34, 0x38, 0xb5, 0xaf, 0xb8, 0x0f, 0x1e, 0xf8, 0xf3, 0xff, 0xb6, 0xf4, 0x69, + 0xfd, 0x77, 0x4f, 0xe3, 0x82, 0xde, 0xf0, 0xc7, 0xa2, 0x94, 0x87, 0x11, 0x0f, 0xa3, 0xb2, 0x14, + 0x75, 0xc5, 0x65, 0x02, 0x44, 0xeb, 0xd7, 0x49, 0x26, 0xf1, 0x3d, 0x9d, 0x0d, 0xd5, 0x1b, 0x5e, + 0xff, 0x86, 0x9c, 0xa8, 0xea, 0xe2, 0x9d, 0xed, 0x27, 0x01, 0xd8, 0x18, 0xdd, 0x82, 0xcf, 0xf4, + 0x3a, 0x25, 0xbe, 0x7c, 0x4b, 0xc8, 0x8c, 0x28, 0xe5, 0x2c, 0x69, 0x13, 0xcc, 0x9e, 0x05, 0xef, + 0x82, 0x32, 0x15, 0xff, 0x08, 0xf8, 0x79, 0xb1, 0x24, 0xba, 0xee, 0x65, 0x5d, 0x64, 0xc9, 0x6e, + 0x43, 0x8b, 0x3b, 0x15, 0xe1, 0x49, 0x3b, 0xaa, 0x94, 0x6d, 0x88, 0x3d, 0x23, 0x2f, 0xba, 0x40, + 0x3d, 0x37, 0xd7, 0xb7, 0x77, 0x9c, 0xa9, 0x2c, 0x4f, 0xc5, 0x85, 0x1c, 0x33, 0x90, 0x33, 0x4b, + 0x35, 0x12, 0x84, 0x15, 0x40, 0xd2, 0xed, 0x07, 0x72, 0xa5, 0x1f, 0x1d, 0xda, 0x41, 0x45, 0x94, + 0x13, 0xc1, 0x8a, 0x60, 0x8a, 0xd3, 0xd5, 0x69, 0x6c, 0x7a, 0x4b, 0x76, 0xde, 0x4f, 0x37, 0x5f, + 0xf9, 0x84, 0xca, 0x39, 0x30, 0x6c, 0x0b, 0x31, 0x11, 0xe5, 0x29, 0x2b, 0xd3, 0xa0, 0x3c, 0x2b, + 0xb4, 0x53, 0x29, 0x55, 0x3c, 0xd9, 0x81, 0xd7, 0x27, 0xe2, 0xc5, 0x6b, 0x71, 0xb2, 0x6a, 0x5e, + 0xd1, 0x36, 0x54, 0xa4, 0x2d, 0x62, 0x7d, 0x11, 0x76, 0x79, 0x0b, 0x86, 0x1d, 0x30, 0x47, 0x23, + 0x24, 0x64, 0x8d, 0xf5, 0x2a, 0xee, 0x89, 0x42, 0xb1, 0xa2, 0x8b, 0xd0, 0x56, 0xbc, 0x43, 0x1c, + 0xd2, 0xa9, 0x94, 0x6b, 0x8b, 0xdd, 0xd8, 0x08, 0x1d, 0x7a, 0xf9, 0xde, 0x46, 0x1d, 0x42, 0xd8, + 0x58, 0x90, 0xd3, 0xca, 0x32, 0x82, 0x03, 0x95, 0x20, 0x71, 0x86, 0x2a, 0x92, 0x5f, 0x15, 0xff, + 0xd6, 0x38, 0xe5, 0x37, 0x24, 0x8b, 0xed, 0xd7, 0xac, 0xc8, 0x26, 0x1c, 0x79, 0xe8, 0x8b, 0x48, + 0x9d, 0x05, 0x67, 0x92, 0xb0, 0xe1, 0x40, 0x7b, 0xa4, 0xf8, 0x93, 0x5a, 0xa4, 0xed, 0xfa, 0x2c, + 0xcb, 0x5f, 0xb5, 0x34, 0xe1, 0x2a, 0x3d, 0x8c, 0x24, 0xed, 0x14, 0x52, 0xa9, 0xf2, 0x0e, 0xa5, + 0xf5, 0xc3, 0x88, 0x6c, 0x96, 0xab, 0xb5, 0x0e, 0x8e, 0x81, 0x3e, 0x8a, 0x1a, 0xe1, 0x61, 0xb4, + 0x11, 0xcc, 0x8c, 0x6e, 0x46, 0x44, 0xca, 0xe4, 0x0c, 0x9f, 0x65, 0x24, 0xb2, 0xd9, 0xa9, 0x67, + 0xa5, 0x9c, 0xcb, 0xff, 0x80, 0xce, 0x87, 0x49, 0x7b, 0xed, 0xac, 0xde, 0x54, 0xfc, 0x7c, 0x92, + 0xfe, 0x9c, 0xa2, 0xfe, 0xab, 0x29, 0x7b, 0x54, 0x97, 0xc6, 0x6e, 0x8b, 0x6c, 0xe5, 0x14, 0x54, + 0xa2, 0xa9, 0x33, 0x3b, 0xa5, 0x4f, 0x08, 0x41, 0xe0, 0x4c, 0x7e, 0x93, 0x97, 0x65, 0x26, 0x30, + 0x1f, 0x90, 0x69, 0x17, 0xe8, 0x10, 0x22, 0xcd, 0xda, 0x46, 0x48, 0x5d, 0xf6, 0x63, 0x50, 0x6b, + 0x60, 0xa7, 0xcb, 0x52, 0x12, 0xdf, 0xb4, 0xee, 0x94, 0xba, 0x29, 0x82, 0xd3, 0xbf, 0xfd, 0x13, + 0x46, 0x22, 0xf9, 0xe1, 0xf1, 0x03, 0x00, 0x00 +}; // Autogenerated from wled00/data/welcome.htm, do not edit!! -const char PAGE_welcome[] PROGMEM = R"=====(WLED Setup - - - -

-

Welcome to WLED!

Thank you for installing my application!

-If you encounter a bug or have a question/feature suggestion, feel free to open a GitHub issue! -

Next steps:

Connect the module to your local WiFi here! -

Just trying this out in AP mode?
-)====="; +const uint16_t PAGE_welcome_length = 1531; +const uint8_t PAGE_welcome[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0x95, 0x56, 0x5b, 0x93, 0xaa, 0x3a, + 0x16, 0x7e, 0xef, 0x5f, 0xc1, 0x76, 0x6a, 0xea, 0x3c, 0xb8, 0xbb, 0xb9, 0x89, 0xa8, 0x6d, 0xf7, + 0x19, 0xc5, 0x4b, 0x7b, 0x03, 0x6f, 0x78, 0x7b, 0x0b, 0x10, 0x20, 0x08, 0x04, 0x93, 0x80, 0x97, + 0xae, 0xfe, 0xef, 0x27, 0xe8, 0xee, 0xa9, 0x7d, 0xea, 0x3c, 0x4c, 0x4d, 0x2c, 0x21, 0xf9, 0x92, + 0xb5, 0xd6, 0xb7, 0x92, 0xb8, 0x3e, 0xdb, 0x3f, 0x7a, 0x96, 0xb1, 0xde, 0xcf, 0xfb, 0x42, 0xc8, + 0x92, 0xf8, 0xbd, 0xfd, 0xeb, 0x09, 0x81, 0xf7, 0xde, 0x4e, 0x20, 0x03, 0x82, 0x1b, 0x02, 0x42, + 0x21, 0x7b, 0xab, 0xe4, 0xcc, 0x7f, 0x6e, 0x54, 0x7e, 0xa1, 0x4f, 0x2e, 0x4e, 0x19, 0x4c, 0x39, + 0x7c, 0x46, 0x1e, 0x0b, 0xdf, 0x3c, 0x58, 0x20, 0x17, 0x3e, 0xdf, 0x07, 0x15, 0x21, 0x05, 0x09, + 0x7c, 0xab, 0x14, 0x08, 0x9e, 0x33, 0x4c, 0xd8, 0xb7, 0xcd, 0x03, 0x65, 0x21, 0x4c, 0xe0, 0xb3, + 0x8b, 0x63, 0x4c, 0x2a, 0xbf, 0xb9, 0xf9, 0x97, 0x72, 0x6f, 0x7c, 0x2d, 0x43, 0x2c, 0x86, 0xef, + 0x5b, 0x18, 0xbb, 0x38, 0x81, 0x3f, 0xda, 0xe2, 0x63, 0xdc, 0xa6, 0xec, 0xca, 0x5f, 0x4f, 0x0e, + 0xf6, 0xae, 0x9f, 0x3e, 0xb7, 0x7a, 0xf6, 0x41, 0x82, 0xe2, 0x6b, 0x6b, 0x03, 0x89, 0x07, 0x52, + 0xf0, 0xf3, 0x03, 0xc6, 0x05, 0x64, 0xc8, 0x05, 0x3f, 0x29, 0x48, 0xe9, 0x33, 0x85, 0x04, 0xf9, + 0xaf, 0x0c, 0x5e, 0xd8, 0x33, 0x88, 0x51, 0x90, 0xb6, 0x5c, 0x1e, 0x06, 0x92, 0x57, 0x07, 0xb8, + 0xc7, 0x80, 0xe0, 0x3c, 0xf5, 0x1e, 0x1c, 0x5a, 0x65, 0xe0, 0xd7, 0x04, 0x90, 0x00, 0xa5, 0x2d, + 0xe9, 0xf5, 0x17, 0xe6, 0xfb, 0xfe, 0x97, 0x93, 0x33, 0x86, 0xd3, 0x4f, 0x9c, 0xb3, 0x18, 0xa5, + 0xb0, 0x9c, 0xcb, 0x09, 0xe5, 0x93, 0x19, 0x46, 0x77, 0x4f, 0x19, 0xf0, 0x3c, 0x94, 0x06, 0xad, + 0x46, 0x76, 0xf9, 0xb6, 0x97, 0x25, 0xde, 0xbf, 0xef, 0x41, 0x4b, 0x51, 0xcb, 0xfe, 0x3d, 0x3e, + 0x23, 0x9c, 0x90, 0x8f, 0x49, 0xd2, 0xca, 0xb3, 0x0c, 0x12, 0x17, 0x50, 0xf8, 0xfa, 0x7b, 0x06, + 0xe1, 0x37, 0xf3, 0x07, 0x4a, 0xd1, 0x0d, 0xb6, 0xe4, 0x26, 0xb7, 0xfe, 0x27, 0x57, 0x55, 0x55, + 0x7f, 0xa3, 0xf8, 0xea, 0x60, 0xe2, 0x41, 0xd2, 0x92, 0x04, 0x8a, 0x63, 0xe4, 0x09, 0xbf, 0x61, + 0xcf, 0x04, 0x78, 0x28, 0xa7, 0x2d, 0x45, 0xcb, 0x2e, 0x5f, 0x28, 0x09, 0x3e, 0x1f, 0xac, 0x9a, + 0x9a, 0x74, 0x67, 0x7b, 0x79, 0x9c, 0x54, 0xab, 0xa1, 0xfc, 0xfb, 0x15, 0x25, 0x20, 0x80, 0xcf, + 0x04, 0xa6, 0xdc, 0xac, 0xcc, 0x27, 0x43, 0x17, 0x18, 0x03, 0x06, 0xbd, 0x7f, 0xcc, 0xb8, 0x04, + 0xd1, 0xec, 0x19, 0x7a, 0x01, 0xa4, 0xdf, 0x19, 0xd7, 0x8a, 0x50, 0x90, 0xca, 0xcf, 0x2b, 0x48, + 0xf9, 0x72, 0x86, 0x70, 0xda, 0xf2, 0x91, 0x20, 0xd3, 0xaf, 0xff, 0x1c, 0xe1, 0xd5, 0x27, 0xfc, + 0xc8, 0xa9, 0xe0, 0xa3, 0x4f, 0x9f, 0xe0, 0xe4, 0x13, 0x67, 0xc0, 0x45, 0xec, 0xda, 0x92, 0xbe, + 0x18, 0xfe, 0xef, 0x40, 0xfe, 0xfa, 0x7a, 0x49, 0x00, 0x4a, 0x3f, 0xff, 0xee, 0xe0, 0x45, 0xa3, + 0xc2, 0x8b, 0x4e, 0x05, 0x07, 0xb3, 0xf0, 0xeb, 0xa9, 0x2d, 0x3e, 0x8e, 0xbf, 0x2d, 0x3e, 0x6e, + 0x66, 0x79, 0x0b, 0xde, 0xdb, 0x3c, 0x2f, 0x01, 0xc4, 0xfc, 0xf2, 0xf0, 0x8b, 0x44, 0x89, 0xfb, + 0x56, 0xf1, 0x00, 0x03, 0xad, 0x3b, 0x6b, 0x31, 0x4b, 0x03, 0xbe, 0x7f, 0x14, 0xd6, 0x6b, 0x3f, + 0xd1, 0xa6, 0x6b, 0x2d, 0xcf, 0xd2, 0x64, 0x18, 0xe0, 0x0e, 0x6f, 0xe6, 0xca, 0x0e, 0xfb, 0x76, + 0xc0, 0x7b, 0x43, 0xa9, 0x1c, 0xfb, 0x46, 0x67, 0xc6, 0x5f, 0x3d, 0x70, 0x9b, 0x5a, 0x79, 0x09, + 0x74, 0x76, 0xe6, 0x6a, 0x29, 0x8d, 0x3a, 0x84, 0xd6, 0xdc, 0xfa, 0xa2, 0x04, 0x96, 0xe9, 0xc2, + 0x96, 0xbb, 0x9d, 0x8e, 0x71, 0x89, 0xce, 0x45, 0x63, 0xbf, 0xb0, 0x39, 0xd6, 0x9d, 0xda, 0xfd, + 0x8b, 0xbd, 0xbc, 0xcf, 0x77, 0x1b, 0x72, 0x60, 0xd8, 0xe2, 0x6d, 0x72, 0x12, 0x45, 0x31, 0xc1, + 0x3a, 0xdd, 0xce, 0xcc, 0x86, 0x63, 0x55, 0x0f, 0xa3, 0x3d, 0x3b, 0x80, 0xce, 0xbc, 0x49, 0x3a, + 0xf3, 0xea, 0xc7, 0x8c, 0x1a, 0x68, 0x58, 0x5d, 0x77, 0x46, 0x56, 0x3a, 0x5b, 0x49, 0x93, 0x93, + 0x69, 0xeb, 0x93, 0x79, 0x3a, 0xb5, 0x0f, 0x16, 0x39, 0xd5, 0x0b, 0x6e, 0x59, 0x33, 0x3a, 0xc1, + 0x30, 0xc4, 0x60, 0x5a, 0x15, 0x8b, 0xba, 0x11, 0xe0, 0xfe, 0x65, 0xb6, 0xbe, 0x13, 0x8a, 0x93, + 0x9a, 0xd5, 0x28, 0x3b, 0x07, 0x6f, 0x30, 0xb6, 0x6c, 0xf1, 0x7f, 0xb4, 0x73, 0xa7, 0x6b, 0x76, + 0x4e, 0x6a, 0x69, 0x60, 0xec, 0xba, 0xa3, 0xed, 0xae, 0xcc, 0x4f, 0xef, 0xf1, 0x87, 0x75, 0x3e, + 0x7f, 0x7c, 0x38, 0xf5, 0xf0, 0x58, 0x4e, 0x99, 0x52, 0xdc, 0x5f, 0x6c, 0x96, 0xa3, 0x95, 0xae, + 0x6e, 0xa2, 0xcd, 0xd4, 0x98, 0x75, 0x3b, 0xfd, 0xfd, 0x88, 0x34, 0x8c, 0xfd, 0xe4, 0x48, 0xbc, + 0x48, 0xf5, 0xe5, 0xb1, 0xaa, 0xdf, 0xc0, 0x6e, 0x60, 0x64, 0x6b, 0xab, 0x9a, 0x21, 0xd0, 0x0b, + 0x9c, 0xf9, 0x89, 0x5f, 0xcc, 0xe6, 0x66, 0x21, 0x9d, 0xae, 0xa4, 0x98, 0x44, 0xb5, 0x53, 0x3d, + 0x91, 0x0e, 0x44, 0x0e, 0xab, 0x33, 0xfd, 0x32, 0x90, 0x6f, 0xcb, 0x24, 0xdd, 0xde, 0x4e, 0x9b, + 0xa6, 0x28, 0x79, 0x4a, 0xc4, 0xd8, 0x10, 0x33, 0x4b, 0xce, 0x8b, 0xa6, 0x67, 0x5b, 0xce, 0x19, + 0x46, 0x1a, 0x3e, 0x65, 0xba, 0x7f, 0xdb, 0x6e, 0xe6, 0x71, 0x23, 0xad, 0x37, 0x41, 0x46, 0x6e, + 0xd8, 0xb2, 0x6d, 0xc7, 0x29, 0xbc, 0x91, 0xb3, 0x51, 0xad, 0xe9, 0x19, 0xb1, 0x9d, 0x5b, 0x2f, + 0x56, 0x43, 0xd9, 0xd3, 0x93, 0xc6, 0x48, 0xf5, 0xe1, 0xaa, 0x6f, 0x4a, 0x91, 0x62, 0x40, 0xd3, + 0xb1, 0xf6, 0xb5, 0xd9, 0x05, 0x05, 0x91, 0x35, 0x35, 0xdc, 0x1a, 0x3d, 0xd0, 0xf5, 0x46, 0x89, + 0x65, 0xd7, 0xb8, 0x5e, 0x6b, 0xe7, 0xd1, 0x68, 0x3a, 0x9d, 0x46, 0x9d, 0x0b, 0xbb, 0x1e, 0x63, + 0x76, 0x52, 0x88, 0xb3, 0xb6, 0xab, 0x27, 0x24, 0xc9, 0xa6, 0x46, 0x76, 0xa6, 0xa5, 0xc4, 0x10, + 0x0c, 0xac, 0x25, 0x46, 0x11, 0x50, 0x62, 0x6d, 0x36, 0xd3, 0x80, 0xa4, 0x00, 0xb7, 0xb9, 0x07, + 0x72, 0x7d, 0x75, 0xd4, 0x58, 0x00, 0xe6, 0xc4, 0xce, 0xa2, 0x43, 0xee, 0x48, 0xdd, 0x69, 0x7d, + 0x7f, 0x5a, 0x5d, 0x26, 0x67, 0xe7, 0x43, 0xd7, 0x77, 0xb6, 0x9d, 0xac, 0x8e, 0xe3, 0xdd, 0x2a, + 0x6e, 0x2c, 0x18, 0x98, 0xe5, 0xd7, 0x71, 0x78, 0xd2, 0x12, 0x30, 0xd5, 0xd2, 0xf5, 0x64, 0x93, + 0x1d, 0x0c, 0x59, 0xdd, 0x24, 0x6c, 0x96, 0xad, 0x07, 0x6b, 0x25, 0xa8, 0x15, 0xe3, 0x68, 0x9d, + 0x0f, 0xfd, 0xd9, 0xed, 0xb6, 0xf3, 0x19, 0xb2, 0x0f, 0x69, 0xe8, 0xb1, 0xc0, 0x91, 0x2f, 0xd8, + 0x2f, 0xae, 0xd9, 0x12, 0xa7, 0xda, 0x3a, 0x32, 0xd3, 0xcb, 0xde, 0x6c, 0xde, 0xc6, 0xb8, 0x3e, + 0xd1, 0x48, 0xbe, 0x1a, 0xdd, 0x16, 0x6c, 0x98, 0x6f, 0x0e, 0xa9, 0x74, 0x69, 0xca, 0x64, 0x52, + 0x78, 0x1f, 0xdd, 0x22, 0x51, 0x9b, 0x7d, 0x7d, 0x75, 0x3d, 0xd4, 0xae, 0x52, 0x7d, 0x78, 0x6b, + 0x74, 0x7b, 0xdd, 0xc1, 0xe4, 0x46, 0x77, 0x49, 0xe8, 0x9e, 0xaf, 0x3e, 0x1d, 0x1e, 0x9a, 0x9b, + 0xcc, 0x09, 0x31, 0x32, 0x50, 0x0a, 0x2e, 0x73, 0x33, 0x19, 0x6e, 0x93, 0xed, 0x96, 0x98, 0xb6, + 0x12, 0x75, 0xa5, 0x53, 0xfd, 0xa3, 0xb0, 0x42, 0x53, 0x1e, 0xdb, 0xcc, 0x40, 0x97, 0x05, 0xcb, + 0x94, 0x40, 0x93, 0x0f, 0x1b, 0x7b, 0x37, 0x9a, 0xaf, 0x94, 0xe5, 0xac, 0xd3, 0xab, 0x56, 0xd7, + 0x4a, 0xba, 0x17, 0x7b, 0xa4, 0x17, 0x59, 0xb3, 0x9e, 0x55, 0x3b, 0x1b, 0x7a, 0x94, 0x98, 0xfb, + 0x48, 0xf5, 0x74, 0x35, 0x25, 0xbb, 0x3c, 0x68, 0xec, 0x59, 0x33, 0x37, 0xbb, 0x8d, 0x8b, 0x69, + 0xcb, 0xee, 0xc4, 0xdc, 0x6d, 0x63, 0x7b, 0x60, 0xee, 0x8c, 0x68, 0x23, 0x67, 0x87, 0x70, 0xb8, + 0xee, 0x37, 0xd4, 0x44, 0x29, 0xb6, 0xfe, 0xde, 0x17, 0xcd, 0x61, 0x54, 0xeb, 0x06, 0xf2, 0x2d, + 0xd7, 0xc6, 0x3d, 0x55, 0x9c, 0xa5, 0x1f, 0xda, 0x61, 0xeb, 0x4f, 0xad, 0x23, 0x75, 0x38, 0xa5, + 0x61, 0x14, 0xec, 0x66, 0xa6, 0x26, 0x1a, 0xca, 0x70, 0xb7, 0x1f, 0x0e, 0x06, 0xdb, 0xa6, 0x99, + 0xf0, 0xfa, 0x5d, 0xdf, 0xe5, 0x4c, 0x1a, 0x26, 0xd5, 0x31, 0x05, 0x57, 0x23, 0x6a, 0xdc, 0xa4, + 0x34, 0x8c, 0x86, 0xc5, 0x7e, 0x7c, 0xdb, 0xf8, 0x35, 0xb0, 0xba, 0x71, 0x26, 0x94, 0x54, 0x73, + 0x7d, 0xa1, 0x8c, 0x47, 0x1e, 0xde, 0xa9, 0x7b, 0x6b, 0x11, 0x59, 0x34, 0x4e, 0xe8, 0x56, 0x8e, + 0xc6, 0xaa, 0xac, 0x48, 0x69, 0xbf, 0x19, 0xf8, 0x58, 0x6f, 0x86, 0x6b, 0x60, 0x7a, 0x2e, 0x3d, + 0x6d, 0x96, 0x75, 0x59, 0x8a, 0x0b, 0xab, 0xa6, 0x67, 0x71, 0x0c, 0x60, 0x73, 0x09, 0x0f, 0x0d, + 0x49, 0xbb, 0x4d, 0xb0, 0x02, 0x54, 0x04, 0xf6, 0x9a, 0xab, 0x6b, 0xd9, 0x3a, 0xd9, 0x18, 0x9d, + 0xba, 0x37, 0xd6, 0x3e, 0x74, 0x53, 0xa2, 0x44, 0x04, 0x74, 0x7e, 0xee, 0xc2, 0xd0, 0xd1, 0x91, + 0xdf, 0x0f, 0x73, 0xba, 0xbc, 0xff, 0xb4, 0xfa, 0xf1, 0x60, 0x7d, 0x5c, 0xe5, 0x8b, 0xc4, 0x30, + 0x2a, 0xef, 0x4f, 0x6d, 0x0f, 0x15, 0x82, 0x1b, 0x03, 0x4a, 0xdf, 0x2a, 0x65, 0x51, 0xe2, 0x72, + 0x14, 0xca, 0xdf, 0x5a, 0x24, 0x30, 0x2c, 0x6c, 0xa7, 0xfd, 0x1e, 0xd7, 0x24, 0x0e, 0xb6, 0x43, + 0xf5, 0xfd, 0x69, 0x1d, 0x82, 0xf4, 0x28, 0x5c, 0x71, 0x2e, 0xf0, 0xfa, 0x2e, 0xa0, 0x94, 0x32, + 0x10, 0x73, 0xb5, 0x08, 0x84, 0xe4, 0x2a, 0x80, 0x2c, 0x8b, 0x79, 0x51, 0x2f, 0x2b, 0x5a, 0x69, + 0xa1, 0xf2, 0xaa, 0xf5, 0x6e, 0x72, 0x3d, 0x10, 0x28, 0x83, 0x19, 0x6d, 0xb5, 0x45, 0x87, 0x23, + 0xe4, 0xfe, 0x7d, 0x32, 0x70, 0x9a, 0x42, 0x97, 0x09, 0x5c, 0x1c, 0x85, 0x04, 0x7b, 0x79, 0x7c, + 0x0f, 0xc6, 0xfd, 0x12, 0x21, 0xc6, 0x2e, 0x88, 0x85, 0x2d, 0x1a, 0x20, 0x21, 0x84, 0x84, 0x0b, + 0xe2, 0xdd, 0xe6, 0xae, 0x4d, 0xc2, 0x13, 0x4e, 0x5d, 0x1e, 0xe3, 0xf8, 0xf6, 0xc7, 0x19, 0xa5, + 0x1e, 0x3e, 0xbf, 0x94, 0x8b, 0xcb, 0x80, 0x2f, 0x21, 0x81, 0xfe, 0x5b, 0xe5, 0x45, 0xe4, 0xca, + 0xcd, 0x38, 0x1f, 0x2a, 0x9e, 0x91, 0x8f, 0x2a, 0x7f, 0xbc, 0xdf, 0xfd, 0x7c, 0x83, 0x9c, 0xc2, + 0xdd, 0xcf, 0x83, 0x07, 0x7a, 0x7f, 0x1a, 0xe7, 0x94, 0x73, 0x20, 0xd7, 0x32, 0x03, 0x16, 0x22, + 0x2a, 0x70, 0xf1, 0xe3, 0x59, 0x09, 0x9d, 0x79, 0xc9, 0x0a, 0xfe, 0xd9, 0x16, 0xd1, 0xfb, 0xff, + 0x17, 0xff, 0x4f, 0xca, 0x85, 0x09, 0x12, 0xca, 0x43, 0xaf, 0xf1, 0x3d, 0xbd, 0x52, 0xf1, 0x09, + 0x8e, 0xe9, 0x8f, 0xbf, 0x47, 0x17, 0xf9, 0xce, 0xf3, 0xfd, 0x17, 0x1f, 0x95, 0x5d, 0xbc, 0xff, + 0x0d, 0xf9, 0x0b, 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/liveviewws2D.htm, do not edit!! +const uint16_t PAGE_liveviewws2D_length = 870; +const uint8_t PAGE_liveviewws2D[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0x6d, 0x54, 0x6d, 0x6f, 0xdb, 0x36, + 0x10, 0xfe, 0xee, 0x5f, 0xa1, 0x70, 0x43, 0x2a, 0xc6, 0xb2, 0x64, 0xbb, 0xed, 0x96, 0xc5, 0xa2, + 0x87, 0x35, 0x0d, 0xb0, 0x02, 0xd9, 0x6a, 0x20, 0x19, 0x82, 0x21, 0x30, 0x50, 0x5a, 0x3a, 0x5b, + 0x5c, 0x25, 0xd2, 0x20, 0xcf, 0x96, 0x35, 0x47, 0xff, 0x7d, 0x47, 0xc9, 0xc9, 0x32, 0x74, 0xfe, + 0x20, 0x93, 0xf7, 0xf2, 0xdc, 0xdb, 0x73, 0x4c, 0xcf, 0x3e, 0x7e, 0xbe, 0xbe, 0xff, 0x73, 0x71, + 0x13, 0x14, 0x58, 0x95, 0xf3, 0xf4, 0xf4, 0x05, 0x99, 0xcf, 0xd3, 0x0a, 0x50, 0x06, 0x5a, 0x56, + 0x20, 0xd8, 0x5e, 0x41, 0xbd, 0x35, 0x16, 0x59, 0x30, 0xc8, 0x8c, 0x46, 0xd0, 0x28, 0x58, 0xad, + 0x72, 0x2c, 0x44, 0x0e, 0x7b, 0x95, 0xc1, 0xa8, 0xbb, 0x44, 0x4a, 0x2b, 0x54, 0xb2, 0x1c, 0xb9, + 0x4c, 0x96, 0x20, 0x26, 0x51, 0x45, 0x82, 0x6a, 0x57, 0x3d, 0xdf, 0xd9, 0x09, 0x73, 0x90, 0x15, + 0xd2, 0x3a, 0x20, 0x8c, 0x1d, 0xae, 0x47, 0x97, 0xec, 0x3f, 0xa1, 0xb0, 0x80, 0x0a, 0x46, 0x99, + 0x29, 0x8d, 0x65, 0xc1, 0x4b, 0xb0, 0xef, 0xa6, 0xdd, 0x8f, 0x4c, 0x51, 0x61, 0x09, 0xf3, 0xc1, + 0xc3, 0xed, 0xcd, 0xc7, 0xe0, 0x56, 0xed, 0x21, 0x58, 0x58, 0xf0, 0xe9, 0xa5, 0x49, 0xaf, 0x49, + 0x1d, 0x36, 0xf4, 0xb7, 0x32, 0x79, 0x73, 0xac, 0xa4, 0xdd, 0x28, 0x7d, 0x35, 0x6e, 0xd3, 0xa4, + 0x97, 0xa6, 0x49, 0x5f, 0x9a, 0xd7, 0xce, 0xd3, 0x4c, 0xea, 0xbd, 0x74, 0xc1, 0x40, 0xe5, 0x82, + 0xf9, 0x33, 0xa1, 0x27, 0xbd, 0x8c, 0x50, 0x32, 0xab, 0xb6, 0x38, 0x1f, 0xec, 0xa5, 0x0d, 0x32, + 0x91, 0x9b, 0x6c, 0x57, 0x51, 0x22, 0xf1, 0x06, 0xf0, 0xa6, 0x04, 0x7f, 0xfc, 0xd0, 0x7c, 0xca, + 0xc3, 0xde, 0x8d, 0x47, 0x25, 0xe4, 0x4e, 0x30, 0x16, 0x61, 0x61, 0x0d, 0x52, 0x16, 0xb9, 0x38, + 0x9b, 0xcc, 0xd6, 0x3b, 0x9d, 0xa1, 0x32, 0x3a, 0xa0, 0x52, 0xaf, 0x3b, 0xd8, 0x90, 0x1f, 0xb3, + 0xb8, 0xef, 0x5b, 0xfc, 0xd3, 0xe5, 0x45, 0xad, 0x74, 0x6e, 0xea, 0x58, 0x69, 0x0d, 0xf6, 0xa1, + 0x6b, 0x60, 0x16, 0x17, 0xa0, 0x36, 0x05, 0x7e, 0xa3, 0xfe, 0xb5, 0x13, 0xb7, 0xaf, 0x90, 0x66, + 0x5d, 0x66, 0x78, 0x10, 0x99, 0x4f, 0xea, 0xda, 0x37, 0xea, 0x80, 0x21, 0x9b, 0xe6, 0x8c, 0xcf, + 0xd4, 0x3a, 0x24, 0x0d, 0x3f, 0x7a, 0x93, 0xda, 0xcd, 0xd0, 0x36, 0xc7, 0xda, 0x09, 0x34, 0xdb, + 0xf8, 0x84, 0x59, 0xbb, 0x36, 0x93, 0x98, 0x15, 0x21, 0xf2, 0x63, 0x4b, 0xd6, 0xb5, 0x3b, 0x3f, + 0xaf, 0x5d, 0x6c, 0xa9, 0x39, 0xcd, 0x1d, 0x4a, 0x04, 0x21, 0xc4, 0x03, 0xac, 0xee, 0x4c, 0xf6, + 0x15, 0x30, 0xfe, 0xbc, 0xb8, 0xf9, 0x9d, 0x93, 0xda, 0x81, 0xa6, 0x92, 0x8f, 0x6f, 0xca, 0xfd, + 0x9b, 0x2b, 0xb4, 0x3b, 0x68, 0x29, 0x14, 0x94, 0x0e, 0x8e, 0x25, 0x60, 0x80, 0xe2, 0x84, 0x5d, + 0x1a, 0x82, 0xa6, 0xb2, 0x23, 0x10, 0x18, 0x6f, 0x25, 0x16, 0x7e, 0xae, 0x91, 0x15, 0x10, 0xbb, + 0x92, 0xa8, 0x12, 0x4e, 0x22, 0x88, 0x09, 0xc8, 0x3d, 0x28, 0x2c, 0x42, 0x96, 0x30, 0xfe, 0xf3, + 0x68, 0x72, 0xb5, 0x37, 0x2a, 0x0f, 0xc6, 0x3c, 0x76, 0xdb, 0x52, 0x61, 0x27, 0x8d, 0x34, 0xb9, + 0x1b, 0xab, 0x68, 0x80, 0x94, 0xd7, 0xb6, 0x94, 0xe4, 0xca, 0x0a, 0xc4, 0x2d, 0x8b, 0x58, 0xed, + 0x28, 0xb2, 0x8d, 0x4b, 0xd0, 0x1b, 0x2c, 0xe6, 0x93, 0xf3, 0xf3, 0x50, 0x0f, 0x05, 0x39, 0x0d, + 0xed, 0xe3, 0x78, 0xc9, 0x23, 0x2a, 0x47, 0x68, 0xa8, 0x83, 0x97, 0x0a, 0x48, 0xcd, 0x12, 0xef, + 0xc4, 0x63, 0xa3, 0xcd, 0x16, 0xb4, 0x08, 0xb9, 0x98, 0x1f, 0xff, 0xbf, 0xa4, 0xb6, 0x25, 0xf9, + 0x4a, 0x69, 0x69, 0x9b, 0xfb, 0x66, 0x4b, 0x8c, 0x94, 0xd6, 0xca, 0x66, 0xb5, 0x5b, 0xaf, 0xc1, + 0xb2, 0x88, 0x74, 0x32, 0xcf, 0x6f, 0xf6, 0xc4, 0x80, 0x5b, 0xe5, 0x88, 0x9c, 0x60, 0x43, 0x56, + 0x81, 0x73, 0x72, 0x03, 0x34, 0x7f, 0x42, 0xf5, 0xdd, 0xa6, 0x96, 0xb2, 0x47, 0xb3, 0xfa, 0x0b, + 0x32, 0x0c, 0x7e, 0xf1, 0xee, 0x1f, 0x3a, 0xf7, 0x25, 0xa3, 0xbe, 0xa2, 0xb9, 0x43, 0xab, 0xf4, + 0x26, 0xa6, 0xa5, 0x28, 0x43, 0x8c, 0x73, 0x89, 0x92, 0xf3, 0x53, 0x0f, 0x7d, 0xda, 0x7f, 0x28, + 0x8d, 0x97, 0x9d, 0x57, 0x08, 0x3e, 0x4e, 0x6f, 0xe1, 0x87, 0xfa, 0xe3, 0x0f, 0x67, 0x02, 0xa9, + 0xc4, 0xa7, 0xa7, 0xa9, 0x3f, 0x4c, 0xe8, 0x70, 0xe6, 0x07, 0x6d, 0x01, 0x77, 0x56, 0xcf, 0x3c, + 0x84, 0x25, 0xf9, 0x74, 0xe9, 0x9b, 0xf7, 0xf8, 0x76, 0x19, 0x49, 0xf1, 0x1b, 0x0d, 0x20, 0xa6, + 0x3d, 0x0c, 0x4f, 0xc4, 0x4b, 0xec, 0x0b, 0xc9, 0x12, 0xcd, 0x23, 0xd5, 0x1b, 0xac, 0x4b, 0x63, + 0x6c, 0xf8, 0x6c, 0x33, 0x92, 0x17, 0x96, 0x27, 0xd3, 0x9e, 0x62, 0x20, 0xde, 0xcd, 0xd6, 0xa4, + 0x6c, 0x44, 0xfc, 0x7e, 0xd6, 0xa4, 0x7a, 0xd6, 0x0c, 0x87, 0xdc, 0x0b, 0x0e, 0x5e, 0x70, 0x48, + 0xed, 0xec, 0x40, 0x02, 0xca, 0x22, 0x5e, 0xab, 0xb2, 0xbc, 0xf3, 0x6b, 0x26, 0xbe, 0xd8, 0xcd, + 0x2a, 0xfc, 0xfe, 0x88, 0x8f, 0xb0, 0x6c, 0xa3, 0xee, 0x7f, 0x38, 0x79, 0x39, 0x4d, 0x97, 0x2d, + 0xff, 0x12, 0x79, 0x87, 0x15, 0xd0, 0x70, 0x17, 0x14, 0x3f, 0xe4, 0xdd, 0x5d, 0xda, 0x2c, 0x3c, + 0x5c, 0xc8, 0xa1, 0x8a, 0x9a, 0x0b, 0x19, 0xc5, 0xef, 0xe8, 0x33, 0x8e, 0xa6, 0x17, 0x5d, 0x86, + 0x8b, 0x4f, 0xbd, 0x8d, 0x0f, 0x42, 0xe6, 0x30, 0x14, 0x6f, 0xdb, 0x7f, 0x39, 0x4c, 0xef, 0x84, + 0x33, 0x25, 0xf1, 0xca, 0x5a, 0xca, 0x8c, 0x2d, 0x00, 0xbe, 0x06, 0x0f, 0x77, 0x41, 0x77, 0xbd, + 0xa2, 0xb1, 0xd0, 0x50, 0x79, 0x7b, 0xe2, 0xe7, 0xb7, 0xe3, 0xb3, 0xe0, 0xd4, 0xdf, 0xcf, 0xd3, + 0x7b, 0xde, 0xe0, 0xa7, 0xa7, 0xf0, 0xd5, 0xb6, 0xbd, 0xde, 0xec, 0x71, 0x44, 0x8a, 0x7b, 0x55, + 0x81, 0xd9, 0x61, 0xd8, 0x11, 0xe9, 0xf5, 0xda, 0xb7, 0xd1, 0xf4, 0xfd, 0x98, 0xf3, 0x96, 0x0f, + 0xe8, 0xd1, 0xe9, 0x1f, 0x91, 0x34, 0xe9, 0xdf, 0x9b, 0xa4, 0x7b, 0x5d, 0xff, 0x01, 0x47, 0xe8, + 0xd7, 0x03, 0x73, 0x05, 0x00, 0x00 +}; + + +// Autogenerated from wled00/data/404.htm, do not edit!! +const uint16_t PAGE_404_length = 870; +const uint8_t PAGE_404[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0x65, 0x54, 0x5b, 0x73, 0xaa, 0x3a, + 0x14, 0x7e, 0xef, 0xaf, 0xe0, 0xb8, 0x67, 0xcf, 0x7e, 0x68, 0x15, 0x54, 0xac, 0x8a, 0xe8, 0x19, + 0x40, 0x14, 0x7b, 0xf1, 0x4e, 0xad, 0x7d, 0x0b, 0x24, 0x42, 0x2a, 0x10, 0x9a, 0x04, 0xc5, 0x76, + 0xfa, 0xdf, 0x4f, 0x80, 0x76, 0x4e, 0x67, 0xf6, 0x9a, 0x81, 0x95, 0x7c, 0x2b, 0xeb, 0xbe, 0x12, + 0xfd, 0x9f, 0xf1, 0xc2, 0xda, 0xee, 0x97, 0xb6, 0x14, 0xf2, 0x38, 0x1a, 0xe9, 0x5f, 0x7f, 0x04, + 0xe0, 0x48, 0x8f, 0x11, 0x07, 0x92, 0x1f, 0x02, 0xca, 0x10, 0x1f, 0xd6, 0x32, 0x7e, 0xa8, 0xf7, + 0x6a, 0x5f, 0xe8, 0x95, 0x4f, 0x12, 0x8e, 0x12, 0x01, 0x9f, 0x31, 0xe4, 0xe1, 0x10, 0xa2, 0x13, + 0xf6, 0x51, 0xbd, 0xdc, 0xd4, 0xa4, 0x04, 0xc4, 0x68, 0x58, 0x3b, 0x61, 0x74, 0x4e, 0x09, 0xe5, + 0xdf, 0x3a, 0x15, 0xca, 0x43, 0x14, 0xa3, 0xba, 0x4f, 0x22, 0x42, 0x6b, 0x3f, 0xcc, 0xfc, 0x6a, + 0x95, 0x24, 0xce, 0x72, 0xcc, 0x23, 0x34, 0x9a, 0x13, 0x2e, 0x1d, 0x48, 0x96, 0x40, 0x5d, 0xae, + 0x00, 0x9d, 0xf1, 0x8b, 0x60, 0x57, 0x1e, 0x81, 0x97, 0x8f, 0x83, 0x50, 0xab, 0x1f, 0x40, 0x8c, + 0xa3, 0x8b, 0xf6, 0x84, 0x28, 0x04, 0x09, 0xb8, 0x71, 0x50, 0x74, 0x42, 0x1c, 0xfb, 0xe0, 0x86, + 0x81, 0x84, 0xd5, 0x19, 0xa2, 0xf8, 0x30, 0xe0, 0x28, 0xe7, 0x75, 0x10, 0xe1, 0x20, 0xd1, 0x7c, + 0xe1, 0x07, 0xd1, 0x81, 0x07, 0xfc, 0x63, 0x40, 0x0b, 0xcb, 0x55, 0x10, 0x5a, 0xe1, 0x79, 0x10, + 0x03, 0x1a, 0xe0, 0x44, 0x53, 0x06, 0x5f, 0xd8, 0xe1, 0x70, 0xf8, 0xc4, 0x71, 0xf0, 0x51, 0x26, + 0xa4, 0xa9, 0x8a, 0x92, 0xe6, 0xe2, 0x4c, 0x5e, 0x25, 0xa8, 0x75, 0x94, 0xdf, 0x03, 0x1c, 0x83, + 0x00, 0xd5, 0x29, 0x4a, 0xa0, 0x70, 0x94, 0x04, 0x5a, 0x8a, 0x73, 0x14, 0x01, 0x8e, 0xe0, 0x5f, + 0x12, 0x9f, 0x62, 0x96, 0xd6, 0x11, 0x0c, 0x10, 0xfb, 0xf6, 0xd3, 0xea, 0xa4, 0xb9, 0xa4, 0x48, + 0xf5, 0xa6, 0x52, 0xf0, 0x4f, 0x2f, 0xe3, 0x9c, 0x24, 0x1f, 0x24, 0xe3, 0x11, 0x4e, 0x50, 0x11, + 0x45, 0x46, 0x99, 0x08, 0x23, 0x25, 0xb8, 0x8c, 0x39, 0x05, 0x10, 0x16, 0x96, 0x7a, 0x65, 0x14, + 0xa5, 0x85, 0x42, 0x73, 0x50, 0x45, 0xd3, 0x6a, 0x17, 0xeb, 0x32, 0x53, 0x4e, 0x45, 0xea, 0x07, + 0x42, 0x63, 0x2d, 0x4b, 0x53, 0x44, 0x7d, 0xc0, 0xd0, 0xe0, 0x67, 0xad, 0xc2, 0xef, 0x1a, 0x55, + 0x28, 0xc3, 0xef, 0x48, 0x6b, 0xf6, 0x85, 0xf6, 0xdf, 0x55, 0x69, 0xb7, 0xdb, 0x3f, 0x8a, 0x31, + 0xf0, 0x08, 0x15, 0xe9, 0x68, 0x8a, 0xc4, 0x48, 0x84, 0xa1, 0xf4, 0x03, 0xab, 0x53, 0x00, 0x71, + 0xc6, 0xca, 0x9c, 0x3e, 0xaf, 0x74, 0xb9, 0xea, 0x93, 0x2e, 0x57, 0x33, 0x54, 0xb4, 0x6b, 0xa4, + 0x8b, 0x52, 0x4a, 0x20, 0x12, 0x6d, 0x16, 0x2d, 0x67, 0xd4, 0x1f, 0xd6, 0x20, 0xe0, 0x40, 0x2b, + 0x0b, 0x25, 0xa7, 0x49, 0x20, 0xdc, 0x33, 0x74, 0xab, 0xde, 0xe0, 0x27, 0x73, 0xb1, 0x3e, 0x2b, + 0xf7, 0xd3, 0x80, 0x18, 0x82, 0xe6, 0x1b, 0x37, 0xb4, 0xdd, 0x40, 0xac, 0xac, 0x62, 0x6b, 0x04, + 0x96, 0xf1, 0x28, 0x98, 0x69, 0xa7, 0x33, 0x3a, 0x2d, 0x91, 0xe7, 0xf9, 0x66, 0xad, 0xcc, 0x0c, + 0xca, 0x54, 0xff, 0x76, 0x55, 0x00, 0xeb, 0x64, 0xe5, 0x36, 0x4d, 0xa1, 0x90, 0xbf, 0x9e, 0x4f, + 0xbd, 0xfd, 0xca, 0x2d, 0x40, 0xcf, 0xb5, 0x73, 0x77, 0x5d, 0xca, 0xcd, 0x5e, 0x33, 0xb0, 0x5c, + 0xf9, 0xfd, 0xfe, 0x4d, 0x2e, 0xa8, 0xef, 0xed, 0x9a, 0xc4, 0x32, 0x82, 0x69, 0x48, 0x40, 0x21, + 0x9e, 0x2e, 0x1f, 0x9e, 0x7b, 0xa5, 0xe5, 0x3b, 0x38, 0xb9, 0x5b, 0xb8, 0xf2, 0xff, 0x64, 0x4c, + 0xe6, 0x4b, 0x64, 0xce, 0x4a, 0x99, 0x6f, 0x87, 0x2f, 0xfe, 0xd9, 0x30, 0xc6, 0xac, 0xd8, 0x76, + 0x0d, 0x63, 0x47, 0x77, 0x78, 0x75, 0x2c, 0x02, 0x85, 0x1b, 0x77, 0x6d, 0x3e, 0x8d, 0xc3, 0x65, + 0xee, 0xf7, 0xbd, 0x31, 0x71, 0x03, 0xdb, 0x98, 0xaf, 0x90, 0xb7, 0x94, 0x27, 0x6e, 0xe6, 0x3c, + 0xbe, 0x9a, 0xd3, 0xbd, 0x6c, 0x5e, 0xb7, 0xed, 0xfd, 0xba, 0xbb, 0x76, 0x94, 0x37, 0x4b, 0x7e, + 0x31, 0xfd, 0x5b, 0xe7, 0x6c, 0x45, 0xaf, 0x81, 0xb3, 0xb8, 0xce, 0x5f, 0x66, 0x4f, 0x9b, 0x59, + 0x8b, 0xed, 0x03, 0x07, 0x4c, 0xbb, 0xb6, 0xb9, 0x0b, 0x7b, 0xaf, 0x3b, 0x92, 0x6f, 0xa9, 0x65, + 0x4e, 0xe0, 0xf8, 0xee, 0xda, 0x1c, 0xab, 0x91, 0x37, 0x73, 0x72, 0xc3, 0x7f, 0xef, 0x19, 0x4b, + 0xe3, 0xe9, 0x61, 0xcb, 0xe8, 0x8b, 0xad, 0xa2, 0xd5, 0xb8, 0xf3, 0xf6, 0xce, 0xdb, 0xbe, 0x31, + 0xd9, 0xee, 0xc9, 0xd1, 0x52, 0xf7, 0xd6, 0xbc, 0x3f, 0xbd, 0x78, 0x41, 0xa6, 0x5e, 0x8c, 0x15, + 0x37, 0x27, 0x0f, 0xab, 0x67, 0x27, 0x73, 0x0c, 0xd3, 0xe8, 0xde, 0x3d, 0xa2, 0x85, 0x6d, 0xc9, + 0xb6, 0xb2, 0xeb, 0x64, 0x97, 0x7e, 0x70, 0x52, 0x4f, 0xa4, 0xb3, 0x72, 0xee, 0x5b, 0xb8, 0x7b, + 0x79, 0x6b, 0x59, 0x3d, 0xd7, 0x30, 0x1f, 0x55, 0x27, 0x7e, 0xb8, 0xb6, 0x36, 0xdb, 0x67, 0x6b, + 0x3b, 0x69, 0x8e, 0xa9, 0xf5, 0x7c, 0x7b, 0x3d, 0x4d, 0xfb, 0xef, 0x66, 0x07, 0x96, 0xd9, 0x1a, + 0x76, 0x34, 0xd9, 0x1e, 0x37, 0xd9, 0x2a, 0xb6, 0xac, 0xda, 0xe8, 0x4a, 0x0f, 0x9b, 0x23, 0x55, + 0x51, 0xa5, 0xe2, 0xbe, 0x4e, 0xaa, 0xfb, 0x2a, 0x10, 0xdd, 0x1b, 0x19, 0x47, 0x14, 0x63, 0x09, + 0x12, 0xc4, 0xa4, 0x44, 0xc8, 0x8e, 0x09, 0x39, 0x4b, 0xe7, 0x10, 0x51, 0x24, 0x5d, 0x48, 0x26, + 0x01, 0xc1, 0x8b, 0x01, 0x41, 0xb0, 0xd1, 0x68, 0xe8, 0xb2, 0x27, 0x34, 0x68, 0xf9, 0x5d, 0xe9, + 0xd5, 0x65, 0x90, 0x48, 0xe2, 0x47, 0xd8, 0x3f, 0x0e, 0xff, 0x9c, 0x71, 0x02, 0xc9, 0xb9, 0x11, + 0x11, 0x1f, 0x70, 0x4c, 0x92, 0x46, 0x48, 0xd1, 0x61, 0x58, 0x6b, 0x34, 0xe4, 0x7f, 0x99, 0x98, + 0x44, 0x44, 0x59, 0xed, 0xcf, 0xc8, 0x14, 0xe3, 0x2b, 0x71, 0x22, 0x15, 0xcf, 0x09, 0x25, 0x11, + 0x13, 0x16, 0x4b, 0x2b, 0xc2, 0x9c, 0x5c, 0x4d, 0xa0, 0x5c, 0x3e, 0x6c, 0xff, 0x01, 0xd9, 0xa5, + 0x2c, 0x90, 0xee, 0x04, 0x00, 0x00 +}; // Autogenerated from wled00/data/favicon.ico, do not edit!! @@ -136,3 +391,755 @@ 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 = 9992; +const uint8_t iroJs[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xc5, 0x7d, 0x79, 0x77, 0xe3, 0x36, + 0x96, 0xef, 0xff, 0x73, 0xce, 0x7c, 0x07, 0x99, 0x49, 0x7c, 0x48, 0x0b, 0xa2, 0x25, 0x79, 0xa9, + 0x32, 0x55, 0x1c, 0x9d, 0xa4, 0xb2, 0x55, 0x77, 0x2a, 0x95, 0x49, 0x55, 0xa7, 0xa7, 0xa3, 0x28, + 0x39, 0x14, 0x05, 0x49, 0x2c, 0xd3, 0xa4, 0xc2, 0x45, 0xb6, 0x63, 0xe9, 0xbb, 0xbf, 0xdf, 0xc5, + 0x42, 0x82, 0x5a, 0x6c, 0x27, 0x6f, 0xfa, 0xbd, 0x93, 0x94, 0x08, 0x62, 0xb9, 0x00, 0x2e, 0xee, + 0x0e, 0x80, 0x3e, 0x3d, 0x39, 0xfa, 0xcf, 0xff, 0x68, 0x9d, 0xb4, 0xa2, 0x2c, 0x75, 0x3f, 0xe6, + 0xad, 0xd5, 0x85, 0x7b, 0xe1, 0xf6, 0x45, 0x4e, 0xbf, 0xdb, 0xbb, 0xec, 0xf4, 0xbb, 0xfd, 0x5e, + 0xeb, 0x6f, 0xc1, 0x0d, 0xcf, 0x5b, 0x5f, 0x06, 0x49, 0xc4, 0x63, 0x51, 0xf4, 0x5d, 0x14, 0xf2, + 0x24, 0xe7, 0xd3, 0x56, 0x99, 0x4c, 0x79, 0xd6, 0x7a, 0xfb, 0xc3, 0x77, 0xad, 0xbe, 0xdb, 0x15, + 0x45, 0xf3, 0xa8, 0x58, 0x94, 0x13, 0x37, 0x4c, 0x6f, 0x4e, 0x3f, 0x06, 0xd4, 0xee, 0x54, 0x42, + 0xa6, 0xc2, 0xd3, 0xff, 0xfc, 0x8f, 0xa3, 0x59, 0x99, 0x84, 0x45, 0x94, 0x26, 0x76, 0xc1, 0x12, + 0xe7, 0xc1, 0x4a, 0x27, 0x1f, 0x79, 0x58, 0x58, 0xbe, 0x5f, 0xdc, 0x2f, 0x79, 0x3a, 0x6b, 0xf1, + 0xbb, 0x65, 0x9a, 0x15, 0xf9, 0xf1, 0xb1, 0x45, 0xa0, 0x67, 0x51, 0xc2, 0xa7, 0xd6, 0x91, 0x2e, + 0xbc, 0x49, 0xa7, 0x65, 0xcc, 0x87, 0xf2, 0xe1, 0xaa, 0xaa, 0x7e, 0x62, 0x3b, 0x9e, 0xa5, 0xc1, + 0xd6, 0x90, 0x64, 0xeb, 0xe3, 0x63, 0xf9, 0x74, 0x83, 0x9b, 0xe9, 0x50, 0x26, 0xed, 0xc4, 0xf1, + 0xec, 0xc2, 0x2f, 0xd6, 0xeb, 0x9c, 0xc7, 0x33, 0xc7, 0xc5, 0xf8, 0x08, 0xc6, 0xc6, 0x2e, 0x16, + 0x51, 0xce, 0xaa, 0xf1, 0x61, 0x70, 0x65, 0xce, 0x5b, 0x79, 0x91, 0x45, 0x18, 0xe0, 0x60, 0x15, + 0x64, 0xad, 0x1b, 0x96, 0xb3, 0x84, 0x45, 0x2c, 0x65, 0x77, 0xfe, 0xc3, 0x86, 0x7d, 0xf4, 0x47, + 0x63, 0x96, 0xf9, 0xa7, 0x41, 0x18, 0x15, 0x6b, 0x7e, 0x67, 0x0f, 0xbd, 0x7c, 0x3d, 0x5f, 0x27, + 0xeb, 0xe5, 0xfa, 0x53, 0x67, 0x9d, 0x2d, 0x17, 0xeb, 0x79, 0x16, 0x4d, 0xd7, 0xe9, 0x6d, 0xbe, + 0xbe, 0x49, 0xc2, 0x75, 0x52, 0xdc, 0xae, 0xd1, 0xfb, 0x28, 0x5c, 0x8c, 0xd7, 0x7f, 0xa4, 0xe9, + 0xfa, 0xd7, 0x34, 0x9b, 0xae, 0x7f, 0xed, 0x74, 0x4e, 0xa3, 0x81, 0xee, 0xb3, 0xf5, 0x56, 0x62, + 0x65, 0x96, 0x66, 0x36, 0xf5, 0x17, 0xb5, 0xa2, 0xa4, 0x95, 0x38, 0xc5, 0x28, 0x1a, 0xfb, 0x09, + 0x7e, 0x06, 0x19, 0x2f, 0xca, 0x2c, 0x69, 0x15, 0x9b, 0xaa, 0xc5, 0xbd, 0x5d, 0x38, 0x0f, 0x54, + 0x37, 0xf1, 0x0b, 0x77, 0x19, 0x64, 0x3c, 0x29, 0xbe, 0x4f, 0xa7, 0x7c, 0x90, 0x1c, 0x1f, 0x27, + 0x6e, 0xc6, 0x6f, 0xd2, 0x15, 0x7f, 0xbd, 0x88, 0xe2, 0x29, 0xaa, 0xd5, 0x8d, 0x16, 0xd4, 0x0d, + 0x8b, 0x64, 0xc3, 0x8c, 0x71, 0x56, 0x62, 0x4a, 0xb1, 0x1f, 0x64, 0xf3, 0xf2, 0x06, 0x00, 0xf2, + 0x41, 0x34, 0xb3, 0x13, 0xff, 0xad, 0x8d, 0x39, 0x26, 0x0e, 0x3b, 0x7b, 0x55, 0x15, 0xb8, 0x31, + 0x4f, 0xe6, 0xc5, 0xc2, 0xa1, 0x01, 0x46, 0x3e, 0x46, 0x84, 0xe9, 0x9f, 0x0d, 0xb2, 0x9d, 0x0a, + 0x83, 0xac, 0xdd, 0x76, 0x22, 0x77, 0x59, 0xe6, 0x0b, 0x3b, 0x1e, 0x65, 0x63, 0x47, 0x40, 0x2c, + 0xe3, 0xf8, 0xc8, 0x8f, 0x8e, 0x8f, 0xed, 0xc4, 0x0d, 0x69, 0x4c, 0x18, 0xac, 0x1f, 0x39, 0x4c, + 0xe6, 0x17, 0x18, 0xb0, 0x4c, 0xb8, 0x58, 0xa6, 0xa0, 0x8c, 0x8b, 0x1f, 0xb2, 0x74, 0x99, 0x8b, + 0xae, 0x38, 0xe1, 0x61, 0x2b, 0x7f, 0x95, 0x46, 0xd3, 0x56, 0xd7, 0xf7, 0x81, 0x18, 0x3e, 0x26, + 0x98, 0x78, 0x6c, 0xb5, 0x45, 0x8e, 0xa3, 0x51, 0x86, 0x25, 0x76, 0xaf, 0xf9, 0xbd, 0xea, 0xcc, + 0x2e, 0x7d, 0xc2, 0xce, 0xcc, 0x21, 0xf2, 0x88, 0x79, 0xc1, 0x5b, 0xe2, 0x55, 0x95, 0xa6, 0x46, + 0x2e, 0xb5, 0x09, 0x05, 0xba, 0x52, 0x56, 0x1a, 0x28, 0x94, 0x79, 0x11, 0xcb, 0x24, 0x12, 0xb9, + 0xff, 0x40, 0x64, 0xe7, 0x15, 0x6c, 0x49, 0x3d, 0x7b, 0x09, 0x43, 0x43, 0x0f, 0xc5, 0x7c, 0xe6, + 0x65, 0x2c, 0xf1, 0x08, 0x30, 0x8b, 0xe4, 0x83, 0x7b, 0x5d, 0x96, 0xca, 0x64, 0x2c, 0x1f, 0xa1, + 0x7a, 0xa4, 0x09, 0xa8, 0xad, 0x0c, 0x8b, 0x34, 0xf3, 0xe4, 0xf4, 0x36, 0x7a, 0xf8, 0x37, 0xee, + 0x2a, 0xc1, 0xb2, 0x1e, 0x1f, 0xab, 0x84, 0xcd, 0x1d, 0xc6, 0xeb, 0xd1, 0xbc, 0x23, 0x2a, 0xd0, + 0xc4, 0x51, 0x21, 0xb7, 0x2e, 0x7f, 0x23, 0xe9, 0x8a, 0x08, 0xdc, 0x15, 0x03, 0xf4, 0x0b, 0x26, + 0x5e, 0xd0, 0x65, 0xc1, 0xef, 0x0a, 0xdf, 0xa8, 0x7b, 0x2b, 0xeb, 0xaa, 0x05, 0x03, 0x7e, 0x9d, + 0x0a, 0x70, 0x34, 0x44, 0xa1, 0x1b, 0x31, 0xfc, 0x73, 0x13, 0x37, 0x02, 0x83, 0xde, 0xbd, 0x9b, + 0xa1, 0xeb, 0x76, 0xcf, 0x11, 0x33, 0x18, 0x54, 0x84, 0x3b, 0x48, 0x5e, 0x15, 0xa8, 0xa2, 0xa8, + 0x21, 0x21, 0x6a, 0xd0, 0x04, 0x00, 0xc2, 0x41, 0xd1, 0x28, 0x19, 0x3b, 0x7a, 0xc5, 0x23, 0x37, + 0xd5, 0x7d, 0x20, 0xa9, 0xa6, 0xbc, 0x87, 0xa3, 0x0b, 0x97, 0x12, 0x34, 0x06, 0xd9, 0x5d, 0x3d, + 0xe6, 0xa0, 0xe2, 0x02, 0x16, 0xd5, 0xa4, 0x46, 0x4c, 0xee, 0x46, 0x4e, 0x4d, 0x58, 0xa1, 0x64, + 0xad, 0xc2, 0x4d, 0xe9, 0xc5, 0x9d, 0x04, 0x39, 0xf7, 0x05, 0xe6, 0x13, 0xbf, 0xfb, 0xa7, 0x47, + 0xfc, 0xd0, 0x00, 0x43, 0x03, 0x9f, 0x64, 0x3c, 0xb8, 0xde, 0xa8, 0x99, 0xd0, 0x90, 0x36, 0xf5, + 0x08, 0x39, 0x8d, 0xd0, 0x3e, 0x2a, 0xdc, 0x19, 0x68, 0x15, 0xbf, 0xfe, 0x51, 0x17, 0xd0, 0x7a, + 0xa0, 0xdf, 0x5c, 0xb2, 0x49, 0xe1, 0xac, 0xd7, 0xd1, 0x91, 0xef, 0xdf, 0x80, 0x84, 0x27, 0x29, + 0x9a, 0xf1, 0x1f, 0x39, 0x09, 0xd7, 0x28, 0x99, 0xa3, 0x22, 0x86, 0xb0, 0xa7, 0x80, 0xd9, 0x7b, + 0x32, 0xd7, 0xeb, 0xc4, 0xb1, 0x4b, 0xc7, 0xa0, 0xd5, 0xd2, 0x96, 0xc8, 0x51, 0x14, 0xab, 0xf9, + 0x5d, 0xac, 0x56, 0xee, 0xe6, 0x10, 0xa2, 0x76, 0x53, 0x2a, 0xab, 0x29, 0x24, 0xee, 0xd4, 0xe5, + 0x9d, 0x82, 0x7e, 0x37, 0xce, 0xa0, 0xa0, 0x91, 0xa6, 0x4b, 0xdb, 0x19, 0x38, 0x72, 0x16, 0x99, + 0x1f, 0xf9, 0x92, 0x4c, 0x59, 0xe9, 0xdb, 0xdc, 0x87, 0xc8, 0x28, 0x1c, 0x77, 0xea, 0xb8, 0x29, + 0x23, 0x6e, 0x5b, 0x42, 0xa2, 0x24, 0x6e, 0xc9, 0xf0, 0xcf, 0x3f, 0xea, 0xb1, 0x54, 0x4c, 0x42, + 0x08, 0xcc, 0x6b, 0x3b, 0xc5, 0x18, 0x84, 0x78, 0x01, 0x19, 0x27, 0xee, 0x2d, 0x93, 0x60, 0x30, + 0xfb, 0xd4, 0x4d, 0x6f, 0x13, 0x9e, 0xbd, 0xff, 0xe9, 0x9b, 0xaf, 0x62, 0x4e, 0x12, 0x85, 0x49, + 0xce, 0x61, 0x31, 0x93, 0xd4, 0x58, 0x62, 0xf9, 0xb9, 0xe3, 0x95, 0x0e, 0x9b, 0xda, 0x11, 0x35, + 0xcf, 0x8e, 0xfc, 0xf2, 0xf8, 0x38, 0x40, 0xa6, 0x39, 0xe5, 0xf7, 0x36, 0x4d, 0xb5, 0xa8, 0x27, + 0xcb, 0x72, 0x89, 0x83, 0x90, 0x05, 0x6c, 0xc6, 0x16, 0x6c, 0xc5, 0xa6, 0x6c, 0xce, 0x26, 0x24, + 0x76, 0xb0, 0xb6, 0xeb, 0xf5, 0x47, 0xb6, 0xf4, 0x27, 0x7a, 0xf5, 0xb1, 0xf2, 0xe8, 0xea, 0x0e, + 0x23, 0x8e, 0x7d, 0xb9, 0xe0, 0xe5, 0xb0, 0x1c, 0x75, 0xc7, 0xde, 0x92, 0x88, 0x8f, 0x75, 0x25, + 0xf9, 0x39, 0x2c, 0xf4, 0xbb, 0x0c, 0x9c, 0xe0, 0x7f, 0x6e, 0xe3, 0xb7, 0xd6, 0x1c, 0x45, 0xc5, + 0x3d, 0x20, 0x3a, 0x91, 0x06, 0x19, 0xfa, 0xc4, 0x36, 0x44, 0x27, 0xbc, 0xdd, 0x53, 0x73, 0xf1, + 0xed, 0x99, 0x3f, 0x19, 0x85, 0x63, 0xac, 0xfc, 0x8c, 0x86, 0x01, 0x91, 0xe1, 0xfb, 0x33, 0x7a, + 0xd0, 0x1b, 0xd1, 0xba, 0x4f, 0xef, 0x94, 0x70, 0xa8, 0x9e, 0xc2, 0xf6, 0x80, 0xc7, 0xd0, 0x4b, + 0xb4, 0x78, 0x01, 0xc8, 0x36, 0x78, 0xb5, 0x1c, 0x04, 0xa0, 0x56, 0xea, 0x46, 0xc0, 0x0b, 0x88, + 0x4c, 0x1f, 0x07, 0xf6, 0x40, 0xb5, 0x34, 0x34, 0x49, 0xb5, 0x33, 0x31, 0xd1, 0x0d, 0x80, 0x2c, + 0xb0, 0x3e, 0x09, 0x50, 0x37, 0xf3, 0x67, 0xeb, 0xf5, 0x5d, 0x85, 0x41, 0x29, 0xaf, 0x80, 0x46, + 0x86, 0x6e, 0x0b, 0x25, 0x3c, 0x67, 0xf4, 0x3c, 0xf2, 0x03, 0x60, 0x6a, 0xee, 0x83, 0xf2, 0x46, + 0x63, 0x47, 0x52, 0x73, 0x80, 0xc9, 0x86, 0xeb, 0xf5, 0x82, 0x15, 0x5a, 0xbe, 0x2f, 0x0c, 0x91, + 0x32, 0x45, 0xfd, 0x29, 0x72, 0xb4, 0xe8, 0x77, 0x63, 0x67, 0x41, 0xbf, 0x68, 0x24, 0x11, 0x2e, + 0xa7, 0x88, 0xfa, 0xa5, 0x4f, 0xa3, 0x58, 0x1c, 0xf9, 0x31, 0xc8, 0x5a, 0xb4, 0x5d, 0x18, 0x5a, + 0x0e, 0x3c, 0xe8, 0x55, 0x40, 0x51, 0x23, 0x36, 0xca, 0x8e, 0x48, 0x72, 0x25, 0x6e, 0xb0, 0x5c, + 0x82, 0x31, 0xa4, 0x0a, 0x5c, 0x38, 0x02, 0xae, 0x54, 0xaf, 0x7e, 0xcc, 0x08, 0x7d, 0x48, 0xac, + 0xdc, 0x04, 0x22, 0xf0, 0x7d, 0x34, 0x89, 0x25, 0xaf, 0x49, 0x8c, 0xfa, 0x7d, 0x12, 0x00, 0x2b, + 0x74, 0xe8, 0x08, 0x0c, 0xb5, 0x8a, 0x01, 0x89, 0xbc, 0x9c, 0x67, 0xc5, 0x17, 0x1c, 0x10, 0xb8, + 0xbd, 0x60, 0xb1, 0xb3, 0xb1, 0xd2, 0xa5, 0x12, 0x52, 0x91, 0xc0, 0xad, 0xd0, 0x6e, 0xab, 0x20, + 0x2e, 0xb9, 0x6f, 0x59, 0xce, 0x26, 0xc6, 0x78, 0x0d, 0xe8, 0x6c, 0x8f, 0x54, 0xab, 0xda, 0x45, + 0x98, 0xfc, 0x02, 0x32, 0x43, 0x71, 0x5f, 0xd8, 0x6e, 0xb3, 0x62, 0xe3, 0x80, 0xc0, 0x52, 0x7f, + 0xaa, 0x30, 0x05, 0x4a, 0xaf, 0x21, 0x1c, 0x35, 0x21, 0x08, 0x4d, 0x19, 0xfa, 0xa5, 0x26, 0xe2, + 0xb0, 0xd3, 0x19, 0x38, 0xaa, 0x19, 0x88, 0xe7, 0xf8, 0xf8, 0xde, 0xa6, 0xa7, 0x33, 0x90, 0xf5, + 0x96, 0x66, 0x85, 0x89, 0xa8, 0xf0, 0xbd, 0x4d, 0x4f, 0x26, 0x48, 0x92, 0x78, 0x60, 0xae, 0x40, + 0x76, 0x07, 0xe1, 0xab, 0x79, 0x05, 0x16, 0xb4, 0xf6, 0x95, 0x3d, 0xa7, 0x8a, 0xf3, 0x51, 0xbb, + 0x5d, 0x3d, 0x0c, 0xe6, 0xfb, 0x5c, 0x9b, 0x17, 0xd5, 0xda, 0x44, 0x8a, 0xf9, 0x1d, 0x45, 0xf9, + 0xb0, 0xbd, 0xac, 0x49, 0x9a, 0xc6, 0x3c, 0x30, 0xc5, 0xbb, 0x03, 0xa3, 0x45, 0x19, 0x0d, 0x89, + 0x68, 0xe8, 0x38, 0x15, 0x21, 0x7c, 0x9e, 0x65, 0xc1, 0xbd, 0x1b, 0xe5, 0xe2, 0x09, 0x0e, 0x73, + 0xb4, 0xaa, 0xc9, 0x30, 0xbc, 0x0c, 0xa2, 0xdb, 0x30, 0x3c, 0xd0, 0x3f, 0x6c, 0x0e, 0x31, 0x04, + 0xd5, 0x5e, 0x01, 0x1d, 0x26, 0xf6, 0x1e, 0x26, 0x3d, 0x38, 0x1c, 0x2d, 0x06, 0x89, 0x20, 0x51, + 0xd7, 0x22, 0x53, 0x30, 0x99, 0x1b, 0x35, 0xd0, 0x2c, 0x29, 0x6f, 0x26, 0x3c, 0xdb, 0xd3, 0x2a, + 0x14, 0xb0, 0x99, 0x12, 0x60, 0x62, 0x36, 0x83, 0xba, 0x43, 0x37, 0x95, 0xaa, 0xc4, 0x17, 0x5a, + 0x49, 0x2b, 0xd8, 0x81, 0x34, 0xe4, 0x60, 0x5b, 0x88, 0x15, 0x05, 0x3b, 0x08, 0x65, 0xcd, 0x8a, + 0xca, 0x74, 0xa9, 0xec, 0x99, 0x44, 0x28, 0x1f, 0xb0, 0xe5, 0x86, 0x90, 0xe1, 0x15, 0x55, 0x41, + 0x54, 0xaf, 0xc3, 0x4c, 0xaf, 0x83, 0xd5, 0xb1, 0x84, 0xa1, 0xd4, 0x1d, 0x0f, 0x0b, 0x37, 0xe7, + 0xc2, 0x3a, 0x02, 0x1d, 0xdf, 0x93, 0x90, 0x44, 0x5b, 0x68, 0x36, 0x7f, 0x67, 0x22, 0x58, 0xb2, + 0x23, 0x52, 0x4f, 0x99, 0x5b, 0xf0, 0xbc, 0x80, 0xcd, 0x3c, 0x8c, 0xda, 0xd6, 0xf2, 0xce, 0xf2, + 0xd4, 0x8a, 0x0e, 0x2d, 0xcb, 0x33, 0xfa, 0xfa, 0xd1, 0xae, 0xb4, 0x8b, 0x14, 0xb4, 0x4a, 0xea, + 0xb2, 0x50, 0xa0, 0x0e, 0xe3, 0xa7, 0x21, 0x40, 0x45, 0xf0, 0xa1, 0x15, 0xc6, 0x41, 0x9e, 0x7f, + 0x0f, 0xdf, 0x40, 0x8c, 0x4a, 0xbd, 0x03, 0xb0, 0xa7, 0x52, 0x46, 0xae, 0xa8, 0xe5, 0x25, 0x90, + 0x90, 0x96, 0xb6, 0x6a, 0x44, 0x71, 0x4d, 0x18, 0x58, 0x95, 0xfb, 0x58, 0x42, 0x72, 0x84, 0xc0, + 0xc0, 0x0c, 0x29, 0x87, 0xed, 0x2c, 0x57, 0xe4, 0x94, 0x6e, 0x98, 0xe7, 0x1f, 0xc8, 0xde, 0x89, + 0xa4, 0x24, 0xd8, 0xb7, 0xaa, 0x19, 0x68, 0xb5, 0xae, 0x68, 0x59, 0x50, 0x58, 0x52, 0xda, 0x67, + 0x82, 0xe6, 0x52, 0xb2, 0x45, 0x33, 0x07, 0xe8, 0x11, 0xa9, 0x08, 0xb2, 0xdb, 0xa6, 0xb9, 0x82, + 0xdb, 0x69, 0xa6, 0x91, 0xa8, 0x14, 0x8b, 0x22, 0x07, 0xa0, 0xa2, 0x51, 0x3c, 0x26, 0x2c, 0xe2, + 0x21, 0x6b, 0x42, 0x99, 0x21, 0xed, 0x6c, 0xa8, 0x7f, 0x2b, 0xd5, 0xeb, 0x02, 0xbe, 0x96, 0x13, + 0x1b, 0xf5, 0xc6, 0x43, 0x1b, 0x3e, 0xcd, 0x91, 0x40, 0x16, 0x59, 0xa5, 0xcb, 0x38, 0x08, 0xb9, + 0x7d, 0xfa, 0x3a, 0x58, 0x62, 0x7d, 0xf9, 0xa7, 0xa7, 0xd4, 0x13, 0xf8, 0xc8, 0xb7, 0xc1, 0x99, + 0x89, 0x5b, 0xa4, 0xdf, 0xa5, 0xb7, 0x3c, 0x7b, 0x0d, 0x1b, 0xc4, 0x76, 0x1c, 0x32, 0x93, 0x87, + 0xb0, 0x26, 0x1d, 0x37, 0x8f, 0xe1, 0xa2, 0xd9, 0x7d, 0x88, 0x8f, 0xa1, 0x9d, 0xad, 0xd7, 0x85, + 0x1b, 0x4c, 0xa7, 0x5f, 0xad, 0x20, 0x1b, 0xbf, 0x8b, 0xf2, 0x82, 0x43, 0xc5, 0x62, 0xed, 0x57, + 0x42, 0x9a, 0x83, 0xd6, 0xd6, 0x6b, 0xfa, 0x85, 0x4b, 0xe3, 0x38, 0x44, 0x0b, 0x44, 0x13, 0xca, + 0x77, 0xd8, 0xd7, 0x04, 0xde, 0x56, 0x8c, 0x1c, 0x8b, 0x24, 0x2c, 0xc6, 0x5d, 0x04, 0x73, 0xb1, + 0x4a, 0xea, 0x15, 0xd3, 0xbf, 0x51, 0xe9, 0x23, 0x48, 0xb5, 0x44, 0x98, 0xee, 0x43, 0x41, 0x63, + 0x26, 0xe5, 0x78, 0xfb, 0xe4, 0x18, 0x9a, 0x4f, 0x83, 0x64, 0xce, 0xb3, 0xb4, 0xcc, 0xe3, 0xfb, + 0xf7, 0xbc, 0x78, 0x93, 0xa0, 0xdb, 0x6f, 0x3f, 0xbc, 0xfd, 0x4e, 0x41, 0xb4, 0x77, 0xf1, 0xf2, + 0xeb, 0x1d, 0x04, 0xeb, 0xb5, 0x37, 0x94, 0x78, 0x19, 0xaa, 0x3e, 0xd6, 0x6b, 0x41, 0xbc, 0xd1, + 0x50, 0x4f, 0xe4, 0xf3, 0x02, 0xcb, 0x3c, 0x29, 0x0b, 0xfe, 0xfd, 0x7b, 0xdb, 0x5a, 0x14, 0xc5, + 0xd2, 0x3b, 0x3d, 0xbd, 0xbd, 0xbd, 0x75, 0x6f, 0xcf, 0xdc, 0x34, 0x9b, 0x9f, 0xf6, 0xae, 0xae, + 0xae, 0x4e, 0x05, 0x24, 0x8b, 0x6d, 0x23, 0xd5, 0x13, 0x3c, 0xf3, 0xd7, 0x01, 0x10, 0x8f, 0x3d, + 0x39, 0x2c, 0x72, 0x4a, 0x9b, 0xfd, 0x08, 0xe6, 0x34, 0xa4, 0xea, 0xca, 0xb4, 0xf1, 0xc9, 0x76, + 0x2f, 0x46, 0x52, 0x4c, 0x8c, 0x61, 0x05, 0x72, 0x5a, 0xa7, 0xa1, 0x7a, 0x92, 0x7d, 0x6c, 0xba, + 0x7b, 0xd7, 0xf6, 0x96, 0xe5, 0x47, 0x6c, 0x29, 0xb9, 0xb4, 0x61, 0x0c, 0xb1, 0x25, 0xbb, 0x67, + 0xb7, 0xec, 0x9a, 0xc8, 0x0a, 0x70, 0x89, 0x9c, 0x2b, 0xc3, 0x2c, 0x71, 0x0d, 0xef, 0xa4, 0x21, + 0x19, 0x61, 0x0e, 0xa0, 0x5f, 0xd2, 0x9b, 0x98, 0xc3, 0xa0, 0xc8, 0xee, 0xa5, 0x52, 0xde, 0xa3, + 0xe9, 0xae, 0x85, 0xc8, 0x5d, 0x92, 0x6d, 0x28, 0xc4, 0xda, 0xbd, 0x8f, 0xb6, 0xd7, 0xda, 0x07, + 0xf9, 0x40, 0x3a, 0xec, 0xf8, 0x38, 0x1b, 0x05, 0x2e, 0x94, 0xca, 0xad, 0x1f, 0x0c, 0xef, 0x87, + 0xf7, 0xb2, 0xa6, 0x54, 0xa8, 0x5e, 0xe0, 0x46, 0x70, 0xa6, 0x22, 0x37, 0x1c, 0x4e, 0xc8, 0x74, + 0xc2, 0x90, 0xa0, 0x74, 0x43, 0x38, 0xf0, 0x64, 0xe7, 0x78, 0xb6, 0x85, 0xba, 0x45, 0x4a, 0x7d, + 0x59, 0xa0, 0xb9, 0xeb, 0xe3, 0xe3, 0x6b, 0xb7, 0xca, 0x01, 0xba, 0xc9, 0x3c, 0x1e, 0x52, 0x1b, + 0xb4, 0xe4, 0xb7, 0x40, 0xca, 0x92, 0xdd, 0x3a, 0x9e, 0x5d, 0xe7, 0xbc, 0x11, 0x39, 0x6c, 0x66, + 0xce, 0xd4, 0xbf, 0x66, 0x33, 0xd5, 0xd6, 0xff, 0xc3, 0x61, 0xb0, 0xa5, 0xee, 0xdd, 0xbc, 0x9c, + 0xd8, 0x33, 0xaa, 0x27, 0x3d, 0xa9, 0x25, 0x52, 0x79, 0x11, 0x14, 0x1c, 0x5c, 0xa4, 0x52, 0xc4, + 0x49, 0x12, 0x8e, 0x70, 0xae, 0x6e, 0x91, 0xbe, 0xf5, 0x33, 0xb6, 0xc0, 0x38, 0xc9, 0xf0, 0xc7, + 0xeb, 0x8d, 0xa1, 0x0c, 0x67, 0xee, 0x47, 0x50, 0x36, 0x7e, 0x7d, 0xd5, 0x5c, 0xdb, 0x45, 0xd7, + 0xee, 0x9c, 0x17, 0x5f, 0xc2, 0xa8, 0x5f, 0xf1, 0xe9, 0x7b, 0x2a, 0xf8, 0x3a, 0x4b, 0x6f, 0x84, + 0x67, 0x7b, 0x7c, 0xfc, 0x56, 0x34, 0xd0, 0x2d, 0x86, 0xf4, 0x22, 0x0c, 0x6a, 0x24, 0x1c, 0x0f, + 0x3f, 0xec, 0x70, 0x63, 0x7b, 0x29, 0x6a, 0x39, 0x6c, 0xe1, 0xc8, 0x01, 0x3c, 0xd6, 0x8f, 0x1c, + 0x09, 0xcd, 0xe5, 0x66, 0x99, 0x26, 0x20, 0xae, 0x7f, 0x46, 0x71, 0xfc, 0x16, 0x1e, 0x47, 0x41, + 0xd6, 0xdf, 0x6e, 0xae, 0xad, 0x07, 0x6f, 0x14, 0x7e, 0x19, 0x4d, 0x55, 0x8b, 0x54, 0x2a, 0xe2, + 0x99, 0x53, 0x89, 0xdf, 0xe7, 0x8d, 0x00, 0xd6, 0xdd, 0x81, 0xa1, 0xfc, 0xc8, 0x43, 0x8e, 0x56, + 0xaa, 0xee, 0x23, 0x85, 0x72, 0x71, 0x8f, 0x0c, 0x38, 0xf9, 0x22, 0x2d, 0xe3, 0xe9, 0x6b, 0xdd, + 0xe0, 0x1f, 0xcb, 0x29, 0x3a, 0x56, 0x5a, 0xef, 0x40, 0xa9, 0x44, 0x1d, 0x00, 0x49, 0x47, 0x72, + 0x87, 0x04, 0x68, 0x2d, 0x99, 0x58, 0xe4, 0x1e, 0x43, 0xe9, 0x14, 0x3a, 0x09, 0x7a, 0x5a, 0x76, + 0x98, 0x0f, 0xf3, 0x23, 0xb2, 0x12, 0xd3, 0x61, 0xee, 0xe1, 0x57, 0xba, 0xfc, 0x09, 0xbc, 0x07, + 0xf2, 0x1d, 0xa4, 0x25, 0x9f, 0xd4, 0x0e, 0x28, 0x99, 0xf4, 0x78, 0x85, 0x9d, 0x2e, 0x2c, 0x4a, + 0x4a, 0x80, 0xce, 0xc1, 0x60, 0xca, 0x18, 0xdd, 0xec, 0xc5, 0x86, 0x9e, 0xc3, 0xde, 0xec, 0x6a, + 0xf0, 0x1b, 0x69, 0x00, 0xab, 0xd1, 0xb3, 0xa9, 0xa6, 0xa2, 0x2d, 0x92, 0xdd, 0x37, 0x37, 0xc1, + 0xec, 0x6f, 0x15, 0xb3, 0xeb, 0x99, 0x8a, 0x89, 0x52, 0x03, 0xbf, 0xc0, 0x44, 0x34, 0xbf, 0x68, + 0xec, 0xb0, 0x1d, 0xe8, 0x8e, 0x98, 0xf6, 0xe7, 0xca, 0x43, 0x82, 0xf3, 0x10, 0x28, 0xff, 0xe4, + 0x9d, 0x5e, 0xed, 0x80, 0x0c, 0x9e, 0x61, 0xa0, 0xb8, 0x5f, 0x6b, 0x7e, 0x2f, 0xa8, 0x89, 0x0b, + 0xf4, 0x22, 0x8c, 0xfa, 0xd7, 0x12, 0xa4, 0x70, 0x47, 0xdf, 0xda, 0x82, 0x05, 0x32, 0x1a, 0xd8, + 0x56, 0x39, 0xc4, 0x38, 0xe8, 0x5d, 0xfb, 0x10, 0xa2, 0xf8, 0x7d, 0x12, 0x2c, 0xb1, 0xc8, 0xca, + 0x9a, 0x97, 0x28, 0x02, 0x0b, 0xcf, 0x0f, 0x97, 0xda, 0x90, 0x93, 0x00, 0xf3, 0x7e, 0x4b, 0x9c, + 0x92, 0x30, 0x45, 0x8f, 0x32, 0x9a, 0xe0, 0xa6, 0x03, 0x42, 0xc1, 0x8d, 0x76, 0x95, 0x35, 0x73, + 0x57, 0x28, 0x74, 0x18, 0xe4, 0x5b, 0x10, 0xc7, 0xc4, 0x04, 0xd5, 0x80, 0x56, 0xf5, 0xc8, 0x4c, + 0xae, 0xd1, 0x83, 0xda, 0x97, 0x6b, 0x0b, 0xa1, 0xed, 0xb0, 0x89, 0x00, 0x7f, 0x8d, 0x96, 0x91, + 0xb4, 0x53, 0x84, 0x65, 0x21, 0xec, 0x43, 0xd3, 0xa9, 0x37, 0xc5, 0xbf, 0x14, 0xfd, 0x18, 0xb6, + 0xf4, 0x85, 0x41, 0x80, 0x72, 0xa1, 0x56, 0x5a, 0x32, 0x93, 0xe4, 0x87, 0xff, 0x92, 0xaf, 0xe6, + 0xc2, 0x26, 0x11, 0xab, 0xb3, 0x5e, 0x73, 0x6d, 0xbc, 0x6b, 0x16, 0x2a, 0x85, 0xad, 0x93, 0x83, + 0x74, 0xf3, 0x57, 0x95, 0xdb, 0x91, 0x37, 0x22, 0x27, 0xf0, 0x47, 0x46, 0x39, 0x39, 0xa4, 0x8a, + 0xd1, 0x15, 0xb0, 0xe1, 0x19, 0x92, 0xa1, 0x4b, 0x91, 0x2c, 0x12, 0xfb, 0x5e, 0xe8, 0xc6, 0x29, + 0x90, 0x42, 0x76, 0x44, 0x55, 0x07, 0x3c, 0x56, 0xf8, 0x21, 0xa3, 0xf6, 0xd2, 0x27, 0x94, 0x8e, + 0x6a, 0x6d, 0x43, 0x1b, 0x06, 0xbc, 0x6e, 0xa2, 0xf4, 0xd2, 0x34, 0x0d, 0x45, 0x38, 0xd2, 0x0d, + 0xd1, 0xa4, 0xe0, 0x64, 0xc8, 0x91, 0x4b, 0x68, 0xaf, 0x28, 0x8a, 0xc1, 0x87, 0x5b, 0xc5, 0x2a, + 0xd2, 0xb0, 0x5f, 0xaf, 0xf7, 0xbb, 0xdd, 0xee, 0x29, 0x21, 0x82, 0xa9, 0x2e, 0xbc, 0xfd, 0xad, + 0x6d, 0x55, 0xcc, 0x4a, 0xe9, 0x48, 0x1b, 0x1a, 0xb2, 0x9e, 0x35, 0xfc, 0x58, 0x7f, 0xa5, 0x70, + 0x21, 0x9c, 0x39, 0x78, 0x64, 0xa5, 0x11, 0x47, 0x1b, 0x2b, 0x4b, 0xb3, 0x70, 0xb1, 0xc2, 0x81, + 0xbf, 0x82, 0xdd, 0x70, 0x24, 0x9d, 0x27, 0xa3, 0x85, 0xff, 0x51, 0x1a, 0x77, 0x92, 0x8a, 0x54, + 0xb4, 0x8f, 0xa6, 0x07, 0xcb, 0x0c, 0xec, 0x67, 0x57, 0xeb, 0x09, 0xef, 0xdd, 0x71, 0x0f, 0xd8, + 0x53, 0xf0, 0xee, 0x57, 0x07, 0xcb, 0x62, 0xd2, 0x64, 0xeb, 0x75, 0x40, 0xcb, 0x36, 0x03, 0x77, + 0x92, 0x3c, 0x79, 0x47, 0x4c, 0xf9, 0x4e, 0x18, 0x8a, 0x91, 0xae, 0xe8, 0xcf, 0x44, 0x09, 0x6c, + 0x73, 0xb2, 0x48, 0x77, 0xa9, 0x4d, 0x39, 0x02, 0xc2, 0xe1, 0x2c, 0xa5, 0x45, 0x2c, 0x1e, 0xc9, + 0x7a, 0x4d, 0xee, 0x42, 0xa9, 0x62, 0x3b, 0xa3, 0x72, 0x0c, 0x9e, 0xad, 0x6b, 0x25, 0x0e, 0xdf, + 0xeb, 0xe6, 0x26, 0xa8, 0x88, 0xbe, 0x84, 0x2d, 0x40, 0x74, 0x59, 0x0a, 0xa7, 0x80, 0x87, 0xd7, + 0x7c, 0xaa, 0x5e, 0x09, 0x94, 0x30, 0xa2, 0xa9, 0xa2, 0xea, 0x82, 0xa0, 0xab, 0x2e, 0xe0, 0x2a, + 0xc1, 0xd4, 0x59, 0x80, 0x0d, 0x62, 0x29, 0x82, 0x92, 0x2d, 0x01, 0xc3, 0x30, 0xeb, 0x9a, 0xbb, + 0xc9, 0x94, 0xe5, 0xd1, 0x3c, 0x79, 0x27, 0xb7, 0x28, 0x8e, 0xf4, 0x32, 0x1e, 0x1f, 0x4b, 0x3e, + 0xba, 0x23, 0x30, 0x84, 0x2b, 0x35, 0x24, 0x0c, 0x1d, 0x8b, 0x5b, 0x59, 0x4c, 0x2b, 0x69, 0xb5, + 0x20, 0x47, 0x26, 0x8e, 0xc8, 0xd7, 0x53, 0x59, 0xb6, 0x4a, 0x29, 0x93, 0x58, 0xd5, 0x20, 0xc3, + 0x58, 0x25, 0x1d, 0x56, 0x4d, 0x6d, 0x07, 0xac, 0x2a, 0x20, 0xc0, 0x2a, 0x29, 0x40, 0x57, 0xd9, + 0x76, 0x95, 0xae, 0x2b, 0x93, 0xf8, 0x2b, 0x36, 0x76, 0x44, 0x1e, 0xa3, 0x21, 0x0a, 0x42, 0x47, + 0x9a, 0x6f, 0xd3, 0x68, 0x36, 0x43, 0x25, 0x29, 0xd6, 0x37, 0x61, 0x50, 0x84, 0x14, 0x90, 0x7c, + 0xb8, 0x71, 0x53, 0xe5, 0x42, 0x6e, 0x6a, 0xb7, 0xb3, 0xb6, 0x2b, 0xa7, 0x5b, 0xbb, 0x15, 0x03, + 0x8a, 0x93, 0xea, 0x10, 0x21, 0xec, 0xc0, 0x68, 0xd7, 0x04, 0xb0, 0x77, 0xa0, 0x47, 0xee, 0xd4, + 0xd9, 0xdc, 0xb8, 0x21, 0x85, 0xb6, 0x43, 0xea, 0xbd, 0x02, 0xff, 0x95, 0x76, 0x5f, 0x09, 0xd6, + 0xbe, 0x70, 0xf0, 0xb0, 0x90, 0x56, 0x73, 0x58, 0x66, 0x14, 0xf5, 0xf1, 0x93, 0x1d, 0xd8, 0x66, + 0x04, 0xf6, 0xfb, 0x9d, 0x4d, 0x0f, 0x12, 0x73, 0x37, 0x6e, 0x99, 0xdc, 0x48, 0xe3, 0xa4, 0x4a, + 0x02, 0x00, 0x83, 0x3a, 0xd1, 0x41, 0xae, 0xaf, 0xec, 0x4c, 0x39, 0xf1, 0x30, 0xe5, 0x41, 0x73, + 0x87, 0x02, 0xd3, 0xa0, 0x84, 0x48, 0xe9, 0x7a, 0x48, 0x4f, 0x38, 0xe7, 0x84, 0x73, 0xe1, 0xa6, + 0xc7, 0x7e, 0x15, 0x04, 0x40, 0x59, 0x26, 0x7c, 0x7e, 0x21, 0xbd, 0xb2, 0x2d, 0x3d, 0x2d, 0xfb, + 0x17, 0xc8, 0xdb, 0x5f, 0xb4, 0x8b, 0x3f, 0x60, 0x2c, 0x93, 0xba, 0x27, 0x83, 0x02, 0xd6, 0x41, + 0x3c, 0xea, 0x23, 0x11, 0xe2, 0xb9, 0x84, 0x78, 0x2e, 0x5f, 0x65, 0x5a, 0x3c, 0x97, 0x10, 0xcf, + 0x19, 0xd8, 0x81, 0xa2, 0x3d, 0xf4, 0x94, 0x51, 0x12, 0x39, 0x32, 0x4e, 0x31, 0x22, 0x6e, 0xac, + 0xc0, 0x1f, 0x1a, 0x65, 0xa6, 0xdb, 0x61, 0x58, 0xc7, 0x12, 0xc5, 0x55, 0xf5, 0xf9, 0x16, 0x3d, + 0xa0, 0xe7, 0xe8, 0x55, 0x65, 0xd1, 0x44, 0x14, 0xa4, 0x94, 0x11, 0x1b, 0xb9, 0x97, 0xe5, 0xf2, + 0x04, 0x82, 0x34, 0x0b, 0x26, 0x31, 0x8d, 0xbd, 0x7e, 0x21, 0x27, 0x89, 0xd1, 0xf4, 0x93, 0x59, + 0x34, 0x2f, 0x65, 0x39, 0x8c, 0xe6, 0x9a, 0xcb, 0xc8, 0x43, 0xcf, 0xdc, 0xdb, 0x2c, 0x2a, 0x54, + 0x99, 0xc3, 0x24, 0x8f, 0xba, 0x72, 0x83, 0xaf, 0x0a, 0x6f, 0x14, 0x00, 0x43, 0x91, 0x93, 0xcc, + 0xa4, 0x83, 0x89, 0xad, 0xe7, 0x63, 0x4f, 0x7c, 0xd5, 0x2e, 0xc8, 0x73, 0x30, 0x3a, 0x14, 0xad, + 0x11, 0x1d, 0xd2, 0xd3, 0x48, 0xea, 0x7d, 0x31, 0x16, 0xf9, 0x3d, 0x4c, 0x69, 0x67, 0xb7, 0x6b, + 0x7b, 0x6a, 0xba, 0x29, 0x97, 0xb1, 0x02, 0xd5, 0x49, 0xed, 0x8c, 0x2c, 0x82, 0xfc, 0xdd, 0x6d, + 0xa2, 0x87, 0x29, 0x05, 0x39, 0xc9, 0x4b, 0xe2, 0x5d, 0xda, 0xc6, 0xca, 0x68, 0xe7, 0x6a, 0x53, + 0x6d, 0xf6, 0x39, 0x14, 0xc5, 0x8c, 0xef, 0xe5, 0xfe, 0x64, 0xd5, 0x39, 0xb8, 0x86, 0x36, 0x21, + 0xdf, 0x18, 0x70, 0xe1, 0x3e, 0xbe, 0x97, 0x76, 0x46, 0x23, 0x9c, 0x2f, 0x17, 0x43, 0x2c, 0xde, + 0x47, 0x12, 0x15, 0x94, 0x10, 0xf6, 0xc8, 0xf1, 0xb1, 0xcc, 0x24, 0xb1, 0x2e, 0x12, 0xd2, 0x87, + 0xa8, 0x2b, 0x38, 0xce, 0x60, 0x1f, 0xb9, 0xc3, 0x48, 0xa6, 0xbd, 0x15, 0x3b, 0x62, 0xf5, 0x8e, + 0x92, 0xe3, 0xac, 0xd7, 0x6f, 0x29, 0xc7, 0xd8, 0xd0, 0x13, 0xa5, 0x42, 0x22, 0x51, 0x42, 0x6c, + 0x03, 0x24, 0x2a, 0xf7, 0x46, 0x85, 0xe4, 0x1c, 0xc6, 0x45, 0x29, 0x3c, 0xdd, 0xc6, 0x54, 0x80, + 0xc2, 0x50, 0x99, 0x5d, 0xbe, 0xb9, 0x2a, 0x35, 0xc8, 0x26, 0x20, 0x74, 0xab, 0x3b, 0xe9, 0x1e, + 0x00, 0xa9, 0xdc, 0xb8, 0x77, 0x2c, 0xa7, 0x6d, 0x88, 0xc4, 0xdf, 0x33, 0x33, 0x2c, 0xc9, 0x4d, + 0x94, 0xf3, 0xa1, 0x7a, 0x1a, 0x8d, 0x8b, 0x05, 0x4f, 0xdc, 0x09, 0x54, 0xb5, 0xad, 0xcb, 0x32, + 0x9e, 0xa7, 0xf1, 0x4a, 0x04, 0x08, 0x80, 0xf7, 0x0f, 0xd1, 0x0d, 0x4f, 0x4b, 0xf0, 0xc3, 0xde, + 0x7d, 0x9a, 0x9b, 0x1d, 0x6b, 0xac, 0xa6, 0xaf, 0x6c, 0x40, 0x8a, 0x28, 0x1a, 0x90, 0xd5, 0x04, + 0xbe, 0x85, 0x3f, 0x0a, 0x3a, 0x38, 0xca, 0xdc, 0x48, 0xca, 0x50, 0x29, 0x24, 0x2a, 0x96, 0xd3, + 0xb6, 0x57, 0x23, 0x73, 0x9f, 0xf7, 0xf4, 0x55, 0x96, 0x91, 0xa3, 0x5e, 0xd1, 0x84, 0xfd, 0xbc, + 0x16, 0x14, 0x4d, 0xdc, 0x72, 0xce, 0xb2, 0x86, 0x18, 0x7f, 0x4d, 0xe2, 0xc7, 0x21, 0x4b, 0x3e, + 0x4a, 0x4a, 0x3e, 0xd8, 0x53, 0x48, 0x1b, 0x5f, 0x8a, 0x76, 0xa9, 0xd7, 0x6b, 0x3f, 0xd3, 0x42, + 0x8b, 0xb6, 0x20, 0x21, 0xa5, 0x8b, 0x45, 0x96, 0xde, 0x82, 0xae, 0x59, 0xea, 0xdf, 0x89, 0x90, + 0x67, 0xe1, 0x5b, 0xf6, 0xd0, 0x1b, 0x75, 0x7e, 0xf9, 0xa5, 0x3d, 0x1e, 0xfe, 0xf2, 0xcb, 0xf4, + 0xe4, 0x97, 0x5f, 0x5c, 0x3c, 0xda, 0x9f, 0x0d, 0x9d, 0x75, 0xa3, 0x80, 0x72, 0x2c, 0x16, 0xfb, + 0xd6, 0xe8, 0x97, 0x5f, 0xf2, 0xf5, 0x2f, 0xbf, 0xd8, 0xe3, 0xb6, 0x6d, 0xb5, 0x8b, 0xb6, 0xe5, + 0x8c, 0x18, 0x5e, 0xf3, 0x43, 0xaf, 0x48, 0x03, 0xa6, 0x33, 0xb4, 0xd8, 0xf2, 0x4f, 0xb7, 0x3d, + 0x08, 0xea, 0x37, 0x11, 0x35, 0xf8, 0x91, 0xcf, 0xbf, 0xba, 0x5b, 0xda, 0x56, 0x36, 0x9f, 0x58, + 0x6d, 0x18, 0x04, 0xdf, 0x6e, 0xe7, 0x06, 0x56, 0x7b, 0xe9, 0xb0, 0x1f, 0x1a, 0xd9, 0x8b, 0x3c, + 0x16, 0x95, 0x3f, 0xdd, 0xce, 0x95, 0x95, 0x3f, 0xf8, 0xd6, 0xaf, 0x98, 0xf7, 0x27, 0xc3, 0x75, + 0xf7, 0x8e, 0x26, 0xfc, 0x4f, 0xe0, 0x67, 0xd4, 0xed, 0x5c, 0x05, 0x9d, 0xd9, 0xe7, 0x9d, 0xaf, + 0xc7, 0x0f, 0xbd, 0x0d, 0x32, 0x5f, 0x6f, 0x65, 0xf6, 0x29, 0xf3, 0x4b, 0x13, 0xe0, 0x87, 0xf6, + 0x3f, 0xc5, 0x7f, 0xd6, 0xa7, 0x96, 0xc3, 0xbe, 0xde, 0x57, 0xa2, 0xca, 0xbe, 0x6b, 0x96, 0xbd, + 0x16, 0xff, 0x89, 0x92, 0x2f, 0xf6, 0x95, 0xa8, 0xb2, 0xdf, 0xfd, 0xb7, 0x41, 0xb1, 0x80, 0xe5, + 0x3e, 0x67, 0xdf, 0xc8, 0x24, 0x6c, 0xc9, 0x64, 0xca, 0x7e, 0x96, 0x2f, 0xb3, 0x38, 0x4d, 0xb3, + 0xfa, 0x68, 0xc3, 0xdf, 0xb6, 0x74, 0x89, 0xa8, 0x73, 0x13, 0x25, 0xb6, 0x4c, 0x04, 0x77, 0x42, + 0x56, 0x35, 0xf4, 0xc9, 0xdf, 0x4d, 0xf1, 0xd5, 0xe9, 0xbd, 0x2a, 0x2a, 0x53, 0xd9, 0xfa, 0x0c, + 0xfd, 0x67, 0xfe, 0x32, 0xc8, 0x72, 0xfe, 0x75, 0x9c, 0x06, 0xa4, 0xb2, 0xab, 0x00, 0xf8, 0x30, + 0x39, 0xed, 0x75, 0xbb, 0x27, 0x99, 0x97, 0xd5, 0xa0, 0xfe, 0xdb, 0x08, 0x9e, 0x89, 0x56, 0x6f, + 0x48, 0xcd, 0xb3, 0xde, 0xa5, 0xd1, 0xdd, 0x3f, 0x1a, 0x9b, 0xe8, 0x45, 0xfa, 0x5e, 0x04, 0x86, + 0x6d, 0xd4, 0x71, 0x97, 0x01, 0xb1, 0x49, 0x56, 0xd8, 0x7d, 0x66, 0x75, 0x2d, 0x67, 0x43, 0x43, + 0xfa, 0xc9, 0x37, 0x8e, 0x8a, 0x54, 0x40, 0x62, 0x63, 0xa7, 0xfd, 0x53, 0xff, 0x61, 0xe1, 0x75, + 0x59, 0x8e, 0x7f, 0x2b, 0xfc, 0x0b, 0xbc, 0x1e, 0x64, 0xab, 0x12, 0x5b, 0x60, 0xcb, 0x4a, 0x66, + 0xa5, 0xc9, 0xeb, 0x05, 0x99, 0xe2, 0x70, 0xa2, 0xc5, 0x7b, 0x94, 0x44, 0x45, 0x14, 0xc4, 0x3f, + 0x09, 0x0b, 0x71, 0x52, 0x49, 0xe4, 0x4f, 0x65, 0xbf, 0x85, 0x1f, 0xd7, 0x72, 0xa9, 0x3a, 0x11, + 0x42, 0x00, 0xfd, 0xad, 0xcd, 0x8d, 0xdd, 0x0d, 0x0b, 0xe7, 0xd4, 0xa0, 0x2b, 0x93, 0x78, 0xce, + 0xd8, 0xcb, 0xcd, 0xa7, 0xa7, 0x32, 0xd4, 0x5f, 0x38, 0x43, 0xd1, 0xdf, 0x82, 0xdf, 0x49, 0x14, + 0xf8, 0x85, 0x77, 0xfa, 0x2b, 0x51, 0xf2, 0x70, 0xab, 0x06, 0xf2, 0x8c, 0x1a, 0x44, 0xbe, 0x75, + 0x0d, 0x2d, 0xf5, 0x91, 0xab, 0xeb, 0xd4, 0x82, 0x45, 0x1f, 0xfb, 0x39, 0xaa, 0x47, 0x26, 0x85, + 0x02, 0x51, 0x9b, 0x94, 0x43, 0xd6, 0x9b, 0x04, 0x0a, 0x1f, 0x46, 0x6f, 0x98, 0xc6, 0x69, 0xd6, + 0x92, 0xca, 0x1f, 0xfe, 0x1b, 0x54, 0x2a, 0x54, 0x13, 0xa4, 0x2b, 0x5a, 0xc5, 0x6a, 0xa0, 0xf9, + 0x8a, 0xb6, 0x24, 0xe9, 0xe1, 0x59, 0x19, 0x59, 0x08, 0xc0, 0xb2, 0x35, 0xd7, 0x89, 0x89, 0x25, + 0x43, 0xd0, 0x6a, 0xc4, 0x18, 0xab, 0xb5, 0xd0, 0x65, 0xb9, 0x4e, 0xac, 0x8c, 0x4a, 0x00, 0xb4, + 0xbf, 0x52, 0xdc, 0xa8, 0x14, 0x53, 0xa5, 0x6b, 0x1e, 0xaf, 0xa2, 0x44, 0x55, 0x90, 0x33, 0x96, + 0x59, 0x7e, 0xa1, 0x12, 0xb0, 0x3d, 0x98, 0x58, 0x1e, 0x5a, 0xe4, 0x84, 0xc7, 0x3b, 0x8a, 0x40, + 0x2a, 0x01, 0x6a, 0x3a, 0x2a, 0xc6, 0xf5, 0x7a, 0xe3, 0x85, 0x91, 0x42, 0x78, 0xd8, 0xc8, 0xa0, + 0x3c, 0xac, 0x18, 0x87, 0x20, 0x41, 0xef, 0x98, 0x4b, 0xad, 0x68, 0x4d, 0xe2, 0x60, 0x9b, 0x7a, + 0xa8, 0x7e, 0x18, 0x43, 0x3a, 0x9b, 0xf5, 0xb5, 0x29, 0x0f, 0x54, 0xc7, 0x52, 0x51, 0x52, 0xb5, + 0x32, 0x21, 0xed, 0xb6, 0x03, 0xb7, 0xa2, 0x4d, 0x75, 0x20, 0x85, 0xc5, 0xd4, 0xd5, 0x87, 0xf4, + 0x47, 0x20, 0xd2, 0x24, 0x37, 0x7d, 0x04, 0x69, 0x71, 0x7a, 0xd9, 0x65, 0xe4, 0x07, 0xe4, 0xc4, + 0x84, 0x8c, 0xec, 0xd0, 0x95, 0x48, 0x71, 0xff, 0x67, 0xd2, 0xf6, 0x70, 0x8f, 0x3b, 0x1c, 0x82, + 0x3f, 0x3b, 0xb1, 0x7b, 0x9d, 0x08, 0xfe, 0x93, 0x4c, 0x95, 0x27, 0x48, 0xe7, 0x32, 0x4d, 0xaf, + 0x0e, 0xbd, 0x87, 0x3e, 0xff, 0xec, 0x12, 0x2e, 0xed, 0x28, 0x87, 0xc3, 0x92, 0xb1, 0x18, 0x0e, + 0x4b, 0x3a, 0xa6, 0x8d, 0xc7, 0x99, 0x3f, 0x4a, 0x45, 0xe0, 0x45, 0x64, 0x53, 0x96, 0xe2, 0x83, + 0x87, 0xcc, 0xfb, 0x9b, 0xdd, 0xbf, 0xb8, 0x38, 0x19, 0xa9, 0xea, 0x54, 0x47, 0x34, 0xe9, 0x32, + 0x64, 0x3b, 0x6c, 0xae, 0xca, 0x03, 0x9d, 0x31, 0x51, 0x19, 0x33, 0x95, 0xb1, 0xa1, 0x19, 0x82, + 0x4a, 0x3e, 0xa4, 0xdf, 0x82, 0x0a, 0xf6, 0xcd, 0x30, 0x3b, 0x45, 0x3d, 0x31, 0xc5, 0xb9, 0x48, + 0xd1, 0x14, 0x27, 0x22, 0xc5, 0xfd, 0x4a, 0x96, 0xc9, 0xa3, 0x41, 0x98, 0x6d, 0x25, 0xe6, 0x54, + 0x4e, 0xea, 0xf3, 0x4e, 0x89, 0x59, 0x43, 0x2c, 0xf8, 0x9c, 0x0e, 0x10, 0xc0, 0x5f, 0xe5, 0xc3, + 0xae, 0x97, 0x9e, 0xf2, 0x41, 0x7e, 0x1b, 0x91, 0x96, 0x84, 0xdf, 0x1c, 0xc2, 0x9e, 0x6f, 0x95, + 0x1e, 0xaa, 0xc9, 0xa8, 0xc7, 0x40, 0x64, 0x24, 0xc8, 0xb0, 0xa3, 0x4e, 0xe6, 0x9c, 0xa6, 0x6d, + 0x3b, 0x7a, 0x95, 0x0d, 0x2f, 0xbd, 0xae, 0x63, 0x56, 0x88, 0xa8, 0x42, 0xd6, 0x49, 0xa8, 0x42, + 0xdf, 0x2c, 0xc8, 0xa8, 0x20, 0x01, 0xbe, 0x51, 0x70, 0xae, 0x94, 0x33, 0x64, 0xd3, 0x65, 0xf7, + 0x24, 0xfe, 0xec, 0xec, 0x92, 0x44, 0xd4, 0xdf, 0x6c, 0x92, 0x99, 0x21, 0xb0, 0x80, 0xa7, 0x03, + 0x79, 0x25, 0x33, 0x72, 0x95, 0xb1, 0xa9, 0x16, 0xfe, 0xdb, 0x3c, 0xde, 0x8b, 0x16, 0xb9, 0xde, + 0x51, 0xb5, 0xde, 0x99, 0x6f, 0xf7, 0x31, 0x92, 0x93, 0x08, 0x68, 0xc9, 0x5e, 0xf9, 0xbd, 0x61, + 0xe6, 0xf5, 0x3b, 0x19, 0x30, 0xc2, 0x5f, 0xf5, 0x78, 0xe7, 0x0a, 0x53, 0x4e, 0x4e, 0xa2, 0x53, + 0x2d, 0xc0, 0x30, 0x18, 0xd0, 0x4e, 0x35, 0x8e, 0x52, 0x8f, 0x23, 0x46, 0xc6, 0x05, 0x64, 0x79, + 0x73, 0x18, 0xf1, 0xa1, 0xd5, 0xe9, 0x9f, 0xd0, 0x29, 0x02, 0x41, 0x7e, 0x27, 0x76, 0x82, 0x5e, + 0xbb, 0xdd, 0x61, 0xe2, 0xf5, 0xbb, 0x5d, 0xc2, 0x89, 0x1c, 0x55, 0xd2, 0x8e, 0xf4, 0x00, 0xfa, + 0x18, 0x80, 0x8d, 0x77, 0x67, 0xff, 0x20, 0x32, 0x13, 0x19, 0xa2, 0xde, 0x69, 0xdf, 0x1c, 0x87, + 0x64, 0xef, 0x03, 0xac, 0x20, 0x9d, 0x6c, 0xbf, 0xa0, 0x5e, 0xb5, 0x90, 0xce, 0x30, 0xf7, 0xcb, + 0xcb, 0xa1, 0x8d, 0x71, 0x0a, 0x0a, 0xea, 0xf4, 0x2e, 0x2e, 0xdc, 0xfe, 0xc5, 0xf9, 0xcb, 0x8b, + 0x8b, 0xcb, 0xfe, 0x8b, 0xee, 0x55, 0xef, 0xc5, 0x55, 0xc7, 0x3d, 0x3f, 0xbf, 0xb8, 0xba, 0xbc, + 0xba, 0xe8, 0x9e, 0xe3, 0xe7, 0xc5, 0x55, 0xef, 0xec, 0xec, 0x04, 0x5e, 0x25, 0xef, 0xf4, 0x9d, + 0x76, 0xaf, 0x7b, 0xee, 0x9e, 0x5f, 0xf5, 0x7b, 0x97, 0xbd, 0xab, 0xab, 0xb3, 0xab, 0xb3, 0x97, + 0x2f, 0x5f, 0x9e, 0xfc, 0x6e, 0x83, 0x4d, 0xf8, 0xab, 0x7e, 0x17, 0xb3, 0x71, 0x5f, 0xf6, 0x5f, + 0x9c, 0x77, 0xaf, 0x2e, 0xbb, 0x97, 0xe7, 0xdd, 0xee, 0x8b, 0xb3, 0xab, 0x8b, 0x13, 0x08, 0x0f, + 0xde, 0xe9, 0x75, 0x9d, 0x0e, 0xfa, 0x70, 0x5f, 0x5c, 0x5e, 0x9d, 0x5d, 0xf4, 0x5e, 0x9e, 0xf7, + 0xfa, 0xdd, 0xab, 0x6e, 0xbf, 0xdd, 0xeb, 0x5d, 0xb8, 0x97, 0x2f, 0xae, 0xae, 0xce, 0xcf, 0xbb, + 0xbd, 0xee, 0xe5, 0x65, 0xef, 0xfc, 0x05, 0x80, 0x41, 0xc2, 0x78, 0x18, 0x1d, 0xea, 0xb9, 0x57, + 0xa8, 0xdf, 0xbd, 0xb8, 0xbc, 0x7c, 0x89, 0x9f, 0xab, 0xb3, 0xb6, 0xdb, 0xeb, 0x9d, 0xf7, 0x01, + 0xf9, 0xe2, 0xec, 0x05, 0x40, 0x5c, 0x02, 0x76, 0x02, 0xd8, 0xe0, 0x97, 0xce, 0x79, 0x17, 0x53, + 0x38, 0xbb, 0xbc, 0x3c, 0xeb, 0x5e, 0x9d, 0x9d, 0xf5, 0x7b, 0x7d, 0x82, 0x43, 0x7a, 0xdc, 0x3f, + 0xeb, 0x5f, 0x60, 0x32, 0x57, 0xe8, 0xf0, 0xe2, 0x45, 0xaf, 0x77, 0xf5, 0xe2, 0xbc, 0xed, 0x76, + 0x5f, 0x5c, 0x9d, 0x9f, 0x9d, 0x5f, 0x5c, 0x52, 0xfd, 0xcb, 0xfe, 0xd9, 0x79, 0x5f, 0xce, 0xed, + 0x82, 0x86, 0xf8, 0xd2, 0xed, 0xbe, 0xbc, 0xe8, 0x5f, 0x5d, 0x9e, 0x5d, 0xa0, 0xd6, 0xc5, 0x0b, + 0x39, 0x35, 0xc1, 0xa2, 0x82, 0xa9, 0x85, 0xf0, 0x30, 0x98, 0xf8, 0x67, 0x2a, 0x36, 0x78, 0xf8, + 0x67, 0x0c, 0x7e, 0x87, 0x85, 0xff, 0x2e, 0x25, 0xf1, 0x5e, 0xaf, 0x4e, 0x50, 0x49, 0x26, 0xb9, + 0x17, 0x0b, 0xd5, 0xe7, 0x67, 0xa0, 0xcd, 0x73, 0x7e, 0x3e, 0x70, 0xcf, 0x5f, 0x95, 0x1d, 0x3e, + 0x90, 0xeb, 0x98, 0xfa, 0x8d, 0x75, 0xc6, 0xac, 0x5d, 0xcc, 0xbd, 0x6c, 0x93, 0x67, 0x94, 0x82, + 0xed, 0x53, 0x37, 0xfb, 0x2f, 0x3f, 0x3b, 0x8d, 0x86, 0x90, 0x6b, 0x1e, 0x14, 0x78, 0x15, 0x27, + 0xd9, 0xb0, 0x6d, 0x31, 0x0f, 0xcf, 0x07, 0x0e, 0x72, 0xad, 0xba, 0x85, 0xb5, 0x23, 0xf2, 0xc8, + 0xe8, 0xb1, 0x63, 0x36, 0x7a, 0xa0, 0x33, 0x83, 0xb0, 0x02, 0x57, 0x16, 0x83, 0x79, 0xee, 0x19, + 0xa2, 0x58, 0xea, 0x7d, 0x69, 0x05, 0x6c, 0x13, 0x2b, 0x68, 0x1d, 0x14, 0x0a, 0xb6, 0xc3, 0xbc, + 0x73, 0xb3, 0x59, 0xcd, 0x9c, 0xb2, 0x1d, 0x9d, 0x43, 0x92, 0xea, 0x25, 0x61, 0xdb, 0xa6, 0x87, + 0xb6, 0xb2, 0x00, 0x15, 0x4e, 0xda, 0x8a, 0x7e, 0x72, 0xfa, 0x09, 0xf0, 0xb3, 0xa9, 0xfc, 0xd9, + 0x4c, 0x86, 0xe7, 0xa2, 0x51, 0x36, 0xf6, 0xe9, 0x88, 0xc5, 0x11, 0xdc, 0xdd, 0x6c, 0x3c, 0x50, + 0xc6, 0x4e, 0xc1, 0xec, 0xc8, 0x5d, 0xac, 0xd7, 0x91, 0x9b, 0xd3, 0xcf, 0x8a, 0x7e, 0x28, 0x86, + 0xd8, 0xe8, 0x48, 0x3a, 0xb1, 0x91, 0x8a, 0x52, 0xeb, 0x96, 0x18, 0x7a, 0x35, 0xf9, 0x60, 0x67, + 0xf6, 0x0a, 0xa7, 0x0d, 0x4b, 0x68, 0x67, 0xaa, 0xb5, 0xe6, 0xae, 0x81, 0xc1, 0x62, 0x38, 0x00, + 0x4b, 0x82, 0x71, 0x17, 0x8f, 0xc0, 0x21, 0x04, 0x6f, 0x2a, 0x58, 0x79, 0x80, 0x76, 0x81, 0xf0, + 0x16, 0x1f, 0x07, 0x99, 0x3f, 0x06, 0x32, 0x37, 0x41, 0x4a, 0x93, 0xe6, 0x71, 0x68, 0xab, 0xc7, + 0xa0, 0xad, 0x4c, 0x68, 0x41, 0xbc, 0x5c, 0x04, 0x4f, 0x40, 0x0b, 0x1e, 0x81, 0x56, 0x63, 0x17, + 0x6f, 0xec, 0x21, 0x00, 0x6c, 0xa7, 0x02, 0xae, 0xac, 0x9c, 0x03, 0xd0, 0x1b, 0xdc, 0x66, 0x6b, + 0x3b, 0xeb, 0xd0, 0x1a, 0x91, 0x09, 0xd6, 0x64, 0xaa, 0xa2, 0xee, 0x28, 0xe3, 0xd3, 0x47, 0xe7, + 0x80, 0xc6, 0x6e, 0xf6, 0x08, 0xe0, 0x7a, 0x16, 0x78, 0x23, 0xc9, 0x61, 0xce, 0x62, 0x9e, 0x71, + 0x9e, 0x3c, 0x09, 0x7e, 0xfe, 0x7c, 0xf0, 0xf3, 0x06, 0xf8, 0xc9, 0x53, 0xcb, 0x49, 0xd0, 0x27, + 0xcf, 0x87, 0x3e, 0x69, 0x40, 0x27, 0xff, 0x72, 0xbf, 0x58, 0xa8, 0x2d, 0x32, 0x5b, 0x71, 0x07, + 0x13, 0x76, 0x8a, 0xb4, 0x51, 0xa4, 0x84, 0xab, 0xed, 0xa3, 0x6f, 0x48, 0x90, 0xce, 0xf1, 0x88, + 0x48, 0x70, 0x7e, 0x63, 0x53, 0x6c, 0xec, 0x09, 0xaa, 0xa8, 0x0d, 0x22, 0x72, 0x59, 0x40, 0x1a, + 0xd5, 0xd9, 0xeb, 0xc2, 0x0d, 0x86, 0x3d, 0xc8, 0x9e, 0xa0, 0x39, 0xd0, 0x67, 0xb0, 0xb0, 0x98, + 0x21, 0x88, 0x8c, 0xd2, 0x82, 0x74, 0x37, 0x8f, 0xd1, 0x8b, 0x29, 0x20, 0xe2, 0xc7, 0xd1, 0x00, + 0xfb, 0xa4, 0x81, 0x86, 0x85, 0x34, 0x07, 0x04, 0x1a, 0xe2, 0x5a, 0x76, 0x0a, 0x34, 0xe4, 0x12, + 0x0d, 0xf1, 0x73, 0xd1, 0xa0, 0x2d, 0x8f, 0xe7, 0xa0, 0x41, 0x38, 0xf3, 0x4f, 0xa2, 0x01, 0xb5, + 0x9e, 0x89, 0x06, 0xe1, 0x6f, 0x98, 0x48, 0x96, 0x5e, 0xd5, 0x63, 0xaa, 0x02, 0x95, 0xf4, 0x61, + 0x6a, 0x24, 0x29, 0x7e, 0xe1, 0x66, 0x6d, 0x8b, 0xb5, 0x28, 0x31, 0xd7, 0x89, 0x49, 0xdb, 0x72, + 0xac, 0x03, 0x9a, 0x43, 0xef, 0x14, 0x50, 0xd4, 0x73, 0x06, 0x13, 0xc8, 0xff, 0xcd, 0xe5, 0x77, + 0x3c, 0xa4, 0x18, 0xd1, 0x10, 0x9a, 0xfb, 0xef, 0x36, 0x1d, 0x57, 0x92, 0x3a, 0x38, 0x13, 0x6f, + 0x7d, 0xf5, 0xc6, 0xc5, 0xdb, 0x99, 0x7c, 0x13, 0x36, 0xc5, 0xb7, 0x55, 0x4b, 0x71, 0x14, 0xf0, + 0x99, 0x4d, 0xd1, 0x35, 0xbd, 0x9d, 0x8f, 0x59, 0xcf, 0x71, 0xd8, 0x51, 0x72, 0xd8, 0x4b, 0xc4, + 0x04, 0x5b, 0xca, 0xdd, 0x75, 0x06, 0x15, 0xdd, 0x80, 0xde, 0x23, 0x10, 0x7b, 0x06, 0x4a, 0xe7, + 0xd0, 0x65, 0xe5, 0xa6, 0x41, 0xa5, 0xcf, 0xc2, 0x60, 0x60, 0xa0, 0x30, 0x38, 0x8c, 0x43, 0x99, + 0x08, 0xf6, 0x23, 0x73, 0xdb, 0x59, 0xae, 0x89, 0x44, 0xbb, 0xd8, 0xcf, 0x5c, 0xc6, 0x4f, 0xac, + 0xf6, 0x3f, 0x60, 0x43, 0x64, 0x8e, 0x78, 0xcc, 0xe5, 0x63, 0x9f, 0xa4, 0x6d, 0x2e, 0x1f, 0x50, + 0xa9, 0x16, 0xf0, 0xcb, 0xc6, 0x02, 0xf6, 0x5e, 0x9c, 0xfc, 0xb7, 0x58, 0x08, 0x5a, 0x04, 0xf5, + 0xd2, 0x1f, 0xd3, 0x1a, 0xa8, 0x97, 0xb3, 0xb1, 0x5c, 0xbd, 0xaf, 0xff, 0x74, 0x33, 0x22, 0x1a, + 0xf9, 0x72, 0xae, 0x60, 0x7c, 0xd7, 0x80, 0x51, 0x03, 0xa8, 0x5b, 0x9b, 0x3d, 0x7e, 0xd1, 0xa4, + 0x97, 0x47, 0xaa, 0xa3, 0x27, 0xdd, 0xcd, 0xa3, 0x14, 0x02, 0x5c, 0x3f, 0x83, 0x42, 0xc8, 0x91, + 0xdb, 0x98, 0xeb, 0xf3, 0xf2, 0xcf, 0x51, 0xc9, 0x81, 0x15, 0xc2, 0xe3, 0x67, 0xe1, 0x67, 0x82, + 0x46, 0x9c, 0x83, 0x2c, 0x5e, 0x07, 0x5c, 0x4c, 0x31, 0xf2, 0xf4, 0x00, 0x50, 0x49, 0xf7, 0x8f, + 0xa4, 0x20, 0xd2, 0x85, 0x26, 0xc9, 0xbc, 0x6d, 0x7d, 0x26, 0x53, 0x31, 0x52, 0xcf, 0x67, 0xf5, + 0x1f, 0xf6, 0xb1, 0x3a, 0x7c, 0x45, 0x83, 0x5f, 0x85, 0x73, 0x54, 0xf1, 0x2b, 0xbd, 0x89, 0xa5, + 0xfb, 0x74, 0x2f, 0xab, 0x3f, 0xd5, 0xf4, 0xf9, 0xac, 0x8e, 0x29, 0x6e, 0x2d, 0x24, 0xc9, 0x46, + 0xc8, 0xf4, 0x08, 0x02, 0x1d, 0x7e, 0xff, 0x36, 0xab, 0x93, 0x24, 0x7e, 0x16, 0x0e, 0x03, 0x03, + 0x89, 0xc1, 0x63, 0x58, 0x7c, 0x9a, 0xd9, 0x8d, 0xa8, 0xd7, 0x66, 0x03, 0x1a, 0x8d, 0x37, 0xb6, + 0x53, 0xc7, 0x42, 0xff, 0xc7, 0x40, 0x3b, 0x94, 0xc7, 0x6d, 0x34, 0x2d, 0x16, 0x42, 0x41, 0xe5, + 0x98, 0x1f, 0xcf, 0xde, 0x47, 0x7f, 0x70, 0xf2, 0x1d, 0xdd, 0x49, 0x9a, 0xe1, 0xf5, 0x9f, 0xa2, + 0x98, 0x0e, 0xb6, 0xc2, 0x82, 0x9e, 0xc6, 0xfc, 0xc7, 0x60, 0x1a, 0x95, 0x39, 0x4b, 0xc5, 0x85, + 0xaf, 0xe9, 0x94, 0xf6, 0x17, 0xe2, 0xba, 0xed, 0x22, 0x80, 0x6b, 0x91, 0xfb, 0xd6, 0x22, 0xcd, + 0xa2, 0x3f, 0xd2, 0xa4, 0x08, 0x62, 0x4b, 0x68, 0xa8, 0x38, 0xb8, 0x4f, 0xcb, 0xe2, 0xcb, 0x28, + 0xe3, 0x62, 0x0c, 0xb5, 0x4f, 0xaa, 0x76, 0x25, 0x13, 0x3f, 0x73, 0xc8, 0x57, 0x3e, 0x49, 0xdb, + 0x7d, 0xf8, 0xdf, 0x56, 0x18, 0x65, 0xa1, 0x3c, 0x57, 0x1b, 0x0f, 0x1f, 0x64, 0xc7, 0x22, 0x04, + 0xea, 0x55, 0xbd, 0xb6, 0xb7, 0x06, 0xa4, 0x5f, 0x60, 0xe5, 0x7b, 0x51, 0x07, 0x80, 0x3a, 0x04, + 0x48, 0x4c, 0x0e, 0x4b, 0xb3, 0xe0, 0xd1, 0x7c, 0x51, 0x20, 0x11, 0xde, 0x79, 0x11, 0x1c, 0xe9, + 0xf0, 0x5e, 0x3c, 0x32, 0xd1, 0x96, 0x92, 0x1d, 0x7e, 0xda, 0xdf, 0x78, 0x8d, 0xae, 0x32, 0x54, + 0x68, 0x42, 0xcd, 0x74, 0x03, 0x2a, 0xba, 0xf3, 0xba, 0xec, 0x1e, 0xff, 0x64, 0x17, 0xf9, 0x30, + 0xab, 0xbb, 0xc9, 0x87, 0x91, 0x97, 0x19, 0x9b, 0x7e, 0xff, 0x32, 0x43, 0xc9, 0x84, 0x7e, 0xa0, + 0x3b, 0x52, 0x88, 0xa7, 0x4b, 0x19, 0xb2, 0x19, 0x90, 0x1c, 0xb9, 0x46, 0x87, 0xc0, 0xb1, 0x7e, + 0x17, 0x03, 0xa2, 0x9d, 0x87, 0x27, 0xf0, 0x0a, 0xd4, 0xc7, 0x43, 0x0c, 0xce, 0xe3, 0x34, 0x47, + 0x3f, 0x6d, 0xef, 0xdb, 0x8a, 0x4b, 0x44, 0xb8, 0x8e, 0x82, 0x11, 0x42, 0xb8, 0xab, 0xb8, 0x8f, + 0x5e, 0x42, 0x71, 0x10, 0x52, 0x84, 0x80, 0x84, 0x39, 0xec, 0xe9, 0x95, 0xa2, 0xc8, 0x93, 0x0b, + 0x49, 0x2e, 0x4a, 0xa4, 0x2d, 0x5b, 0x97, 0xcd, 0x8d, 0x32, 0x61, 0x88, 0xd6, 0x45, 0x13, 0xa3, + 0x48, 0x7a, 0x09, 0xba, 0x8c, 0xa2, 0x1c, 0xf0, 0xd1, 0x64, 0x91, 0xb2, 0xf1, 0x3d, 0x79, 0x9d, + 0xad, 0xa0, 0xa0, 0xd5, 0x07, 0x7e, 0xb3, 0xe4, 0x19, 0x79, 0x3e, 0x5c, 0xd0, 0xdf, 0x4d, 0x70, + 0x67, 0x64, 0x89, 0x38, 0x9e, 0x9d, 0x28, 0x53, 0xbe, 0xc3, 0x9d, 0xd3, 0xf2, 0xc4, 0x88, 0x76, + 0x54, 0xb1, 0xb0, 0x2e, 0xab, 0x82, 0x60, 0xa9, 0x94, 0x12, 0xb2, 0xc3, 0x85, 0x31, 0x4a, 0xa0, + 0xf9, 0xd4, 0x3f, 0x73, 0x2f, 0x65, 0x89, 0xe1, 0x6c, 0xd5, 0x15, 0x72, 0x59, 0x26, 0xbd, 0x26, + 0x4f, 0xdd, 0xf3, 0xab, 0x8b, 0xe1, 0xfe, 0x0a, 0x14, 0x8b, 0xd8, 0x7f, 0xa9, 0x07, 0x11, 0x43, + 0x04, 0x85, 0x7e, 0xa7, 0x77, 0x12, 0xb6, 0x4b, 0xd0, 0x75, 0x0a, 0xbb, 0xed, 0xce, 0x8b, 0x87, + 0xb9, 0x17, 0x82, 0x72, 0xe2, 0x61, 0xe8, 0xe5, 0x1b, 0x19, 0x54, 0x47, 0xd3, 0xc2, 0xef, 0x9f, + 0x88, 0x91, 0xfe, 0xf0, 0x86, 0x45, 0x85, 0xbf, 0xef, 0x4a, 0x94, 0x5d, 0x7c, 0x96, 0xb4, 0x13, + 0xe7, 0x33, 0xb8, 0xf7, 0xd9, 0xfe, 0x1a, 0x72, 0xda, 0xf9, 0xef, 0x59, 0x61, 0x17, 0x27, 0x45, + 0x3b, 0x39, 0x49, 0x9c, 0x4d, 0xcd, 0xf8, 0x22, 0xe8, 0x5f, 0xef, 0x31, 0x08, 0xfa, 0x03, 0xe5, + 0x57, 0x2c, 0xd5, 0x69, 0xb2, 0x54, 0xa7, 0x21, 0x02, 0x8c, 0x0b, 0x5d, 0x85, 0x19, 0x79, 0x53, + 0x50, 0xb4, 0x6d, 0x2b, 0xb9, 0xa1, 0x92, 0x2a, 0x92, 0x5d, 0x92, 0x26, 0x28, 0x62, 0xc2, 0x84, + 0x58, 0x30, 0x31, 0x78, 0x24, 0x2d, 0x1a, 0x11, 0x69, 0x82, 0xbc, 0xe0, 0x3c, 0xfe, 0x3c, 0x99, + 0xc7, 0x52, 0x1c, 0x89, 0xd7, 0x1d, 0x09, 0x42, 0xa7, 0xb6, 0xc3, 0x38, 0x0d, 0xaf, 0x6f, 0xa3, + 0x5c, 0xc8, 0x0b, 0x3e, 0x84, 0x2c, 0x69, 0x8b, 0xd3, 0xfc, 0x5b, 0xb9, 0xd0, 0x01, 0x1d, 0x2a, + 0xa1, 0x26, 0x41, 0x52, 0x44, 0x7b, 0x9a, 0xf5, 0x5e, 0x76, 0x3b, 0x68, 0xba, 0x5b, 0x4a, 0x87, + 0xa0, 0x7c, 0x8a, 0x6d, 0x62, 0x6d, 0xec, 0x44, 0xa8, 0x93, 0x7a, 0xe8, 0xf1, 0xd6, 0xd0, 0x05, + 0x82, 0x28, 0xf0, 0xe8, 0x86, 0x77, 0xa0, 0x5c, 0x3c, 0xee, 0x29, 0xf2, 0x2a, 0xb6, 0x89, 0x28, + 0x70, 0x45, 0x72, 0xb7, 0xec, 0x44, 0x62, 0xf3, 0x31, 0xf6, 0xc5, 0xc4, 0xc5, 0xba, 0x05, 0x45, + 0x90, 0xf4, 0xed, 0x4e, 0xc4, 0x28, 0x72, 0x69, 0xa3, 0x8f, 0xd3, 0x04, 0x6a, 0x0c, 0x3c, 0x5d, + 0x51, 0x70, 0x56, 0x88, 0x73, 0xd9, 0x2c, 0x35, 0x82, 0x86, 0xf5, 0xde, 0x97, 0x1d, 0x93, 0x53, + 0x61, 0xbc, 0x83, 0x18, 0x4f, 0xd3, 0x93, 0xdc, 0x3c, 0x7f, 0x90, 0xef, 0x2e, 0x9e, 0xd0, 0x02, + 0x93, 0xf4, 0xee, 0x5b, 0x21, 0x85, 0x9a, 0x0b, 0x99, 0x68, 0x91, 0xa6, 0x6e, 0x1e, 0x42, 0xb0, + 0x25, 0x7a, 0x55, 0x0f, 0x89, 0x62, 0xa3, 0xb7, 0x70, 0x0b, 0x33, 0x79, 0x85, 0x99, 0x5b, 0xa5, + 0x56, 0x32, 0x2d, 0xfc, 0x52, 0x24, 0x25, 0x60, 0x26, 0xa2, 0xc5, 0xa9, 0x73, 0x6a, 0x73, 0x92, + 0xe3, 0x0e, 0xb1, 0x35, 0x90, 0x60, 0x47, 0x22, 0xaf, 0xac, 0xf2, 0xf4, 0x48, 0xd5, 0x94, 0xb7, + 0x98, 0x3d, 0x96, 0xcc, 0xce, 0x56, 0x7b, 0x4b, 0x51, 0xd6, 0xc9, 0x65, 0x0d, 0x63, 0xb8, 0x41, + 0x51, 0xdf, 0xab, 0xad, 0x4e, 0x51, 0xf8, 0xdd, 0x01, 0xaf, 0x4f, 0xb0, 0x70, 0x7d, 0xd8, 0xa2, + 0x14, 0xe7, 0x24, 0xdc, 0x3b, 0xac, 0x66, 0x2a, 0x93, 0xf7, 0x1d, 0x71, 0xfb, 0xb3, 0x66, 0xc1, + 0xf2, 0xa4, 0x6c, 0xa7, 0x18, 0xec, 0xab, 0x26, 0x7e, 0xf4, 0x39, 0x41, 0x6e, 0x1e, 0xd3, 0x33, + 0xee, 0xcd, 0x18, 0x5c, 0xfa, 0x80, 0x65, 0x81, 0x3e, 0x06, 0x8e, 0xe1, 0xe0, 0x0b, 0x06, 0xea, + 0x20, 0xc7, 0x62, 0x32, 0xed, 0x35, 0xb8, 0x8a, 0xee, 0xc7, 0xb4, 0xf2, 0x94, 0x6c, 0x14, 0xe1, + 0x15, 0x88, 0x92, 0xd7, 0xb4, 0x7d, 0x65, 0xcc, 0x70, 0x51, 0x6c, 0x1f, 0xaa, 0x69, 0x5b, 0x9d, + 0x39, 0xe1, 0x9d, 0x4e, 0x11, 0x5a, 0xed, 0x44, 0x9a, 0x1c, 0x11, 0xf0, 0xb5, 0xb4, 0xf7, 0xc4, + 0xf2, 0x46, 0xdd, 0xfa, 0x1a, 0x38, 0xec, 0xaa, 0xb6, 0xd5, 0x12, 0x6d, 0x3e, 0xb3, 0x36, 0x8e, + 0xfb, 0x31, 0x05, 0x62, 0x2d, 0x66, 0x39, 0xc2, 0x32, 0xa9, 0x6f, 0x0e, 0x18, 0xf3, 0xd9, 0xdd, + 0x13, 0x1c, 0x42, 0xa5, 0x8b, 0x9b, 0x3d, 0x42, 0x10, 0x4e, 0x0b, 0x7f, 0x64, 0xdd, 0xa4, 0x65, + 0x2e, 0xae, 0x27, 0x00, 0x56, 0x91, 0x96, 0xe1, 0x42, 0xa5, 0x45, 0x7e, 0xb9, 0xd4, 0xb9, 0x3c, + 0x99, 0x5a, 0x63, 0x36, 0x37, 0x04, 0x61, 0x62, 0xec, 0x84, 0x8a, 0x4e, 0x13, 0x75, 0x4a, 0x91, + 0x82, 0x7e, 0xd5, 0x11, 0x8d, 0x68, 0xea, 0xcb, 0x35, 0xca, 0xb0, 0x26, 0xe9, 0x8d, 0x4d, 0xf7, + 0x87, 0xeb, 0xfd, 0xd6, 0xb3, 0x4b, 0x87, 0x8e, 0xd8, 0xcb, 0x71, 0xda, 0x17, 0xf5, 0xd1, 0x33, + 0x71, 0xb2, 0xed, 0xb7, 0xdf, 0x44, 0x00, 0xf5, 0xb7, 0xdf, 0x7c, 0x48, 0x02, 0xdb, 0x8c, 0xa7, + 0xea, 0xb3, 0x3c, 0xf2, 0x58, 0xa6, 0x2d, 0x2e, 0xc0, 0x57, 0x85, 0x8e, 0xd3, 0x38, 0xd5, 0x5f, + 0x38, 0xbb, 0xe7, 0x42, 0x0e, 0xc4, 0x4d, 0x25, 0xdd, 0x88, 0x7b, 0x30, 0xf2, 0xfc, 0x87, 0xd8, + 0x31, 0x03, 0xc7, 0x3e, 0xa4, 0xc9, 0x5b, 0xc2, 0xc7, 0x97, 0xe9, 0x6d, 0x02, 0x96, 0x84, 0x49, + 0x40, 0x38, 0xc9, 0x85, 0xe1, 0x42, 0xfa, 0xe1, 0x69, 0x53, 0x41, 0x1f, 0xff, 0x13, 0x9a, 0x35, + 0x9b, 0x47, 0xc9, 0x50, 0x9b, 0x00, 0x6f, 0xc5, 0xab, 0xa7, 0xf3, 0xc1, 0xa5, 0x0f, 0x58, 0x81, + 0x6c, 0x16, 0xa7, 0xb7, 0x9e, 0xb5, 0x8a, 0xf2, 0x68, 0x02, 0xd3, 0x8c, 0x4d, 0xa3, 0x7c, 0x09, + 0x98, 0x5e, 0x36, 0xb4, 0xa2, 0x24, 0x8e, 0x12, 0xde, 0x99, 0x90, 0xc8, 0xb4, 0x28, 0x14, 0x45, + 0xcf, 0xea, 0xf6, 0x78, 0x57, 0x6f, 0x9d, 0x8b, 0x13, 0xa7, 0xa8, 0x2e, 0xc1, 0x7e, 0xc7, 0x67, + 0x05, 0x2a, 0xcb, 0x97, 0x0f, 0xe9, 0xd2, 0x1a, 0xfb, 0xdc, 0x61, 0x0b, 0xfb, 0x9d, 0x3c, 0xee, + 0x56, 0x5f, 0x24, 0xb7, 0xf5, 0xba, 0x81, 0x31, 0x4b, 0xb9, 0x0b, 0x69, 0x1e, 0x66, 0xaa, 0x10, + 0x74, 0x00, 0x87, 0x4c, 0x1d, 0x3e, 0x92, 0x87, 0x2d, 0xd3, 0xe4, 0x4d, 0xb2, 0x2c, 0x0b, 0xb2, + 0x77, 0x29, 0x93, 0x8e, 0xae, 0xd1, 0x69, 0x94, 0x2f, 0x48, 0x5e, 0x62, 0xc5, 0x5f, 0xc7, 0xc4, + 0x08, 0x3f, 0x02, 0x45, 0xb0, 0x98, 0xa9, 0x1f, 0x71, 0xa5, 0xe5, 0x4b, 0xa9, 0xee, 0x91, 0xa5, + 0xcd, 0x13, 0x49, 0x81, 0xf9, 0x90, 0x46, 0x49, 0xb6, 0xda, 0xf4, 0x83, 0xcc, 0xa0, 0x6b, 0xba, + 0x64, 0xc8, 0x71, 0x37, 0x14, 0x90, 0xfe, 0xa7, 0x43, 0x92, 0x63, 0x46, 0xe2, 0x4d, 0x67, 0xfd, + 0x0b, 0x59, 0x45, 0xba, 0xac, 0xed, 0xae, 0xa2, 0xb6, 0xb8, 0x04, 0x89, 0x4f, 0xb1, 0xa4, 0x96, + 0x27, 0xde, 0xeb, 0x45, 0xb5, 0xbc, 0xa3, 0x1e, 0x9d, 0xc6, 0x15, 0xb7, 0xbd, 0xe8, 0xf2, 0xf6, + 0xb4, 0xa0, 0x53, 0x4a, 0x5f, 0x05, 0x00, 0x61, 0xce, 0xbb, 0x3a, 0x1f, 0xbc, 0x73, 0xe3, 0x8a, + 0xf8, 0xfe, 0x61, 0x49, 0x67, 0xcd, 0x56, 0x9c, 0xe2, 0xea, 0xce, 0xc6, 0xdc, 0xbd, 0x33, 0xb8, + 0xce, 0xe8, 0x5a, 0xbe, 0xcb, 0x4e, 0x7b, 0xbb, 0xd5, 0xc1, 0x8c, 0x46, 0x65, 0x62, 0x48, 0x55, + 0xb7, 0xef, 0xb0, 0xa7, 0xc6, 0xb7, 0xef, 0x86, 0xd7, 0x9e, 0x21, 0xd2, 0xf6, 0xf5, 0xc6, 0x7e, + 0x63, 0xb8, 0x2f, 0x93, 0x86, 0x06, 0x93, 0x71, 0xc6, 0x32, 0x8b, 0xc9, 0x9e, 0x25, 0x9a, 0xd6, + 0x54, 0xb7, 0xb0, 0xc5, 0x09, 0x72, 0xf6, 0x50, 0xdd, 0xe4, 0xf3, 0xac, 0x37, 0x59, 0xfa, 0xad, + 0xa0, 0x97, 0x56, 0x95, 0xea, 0x74, 0x48, 0x58, 0x0a, 0xf2, 0x14, 0xc2, 0x8c, 0x4e, 0x18, 0xe7, + 0x9f, 0xa3, 0xa7, 0x15, 0x1f, 0x5a, 0x46, 0x2d, 0x9d, 0x09, 0x8a, 0xb5, 0xa0, 0x65, 0xe9, 0x8e, + 0x9f, 0xf7, 0x60, 0x75, 0x6e, 0xf9, 0xe4, 0x3a, 0x2a, 0x3a, 0x45, 0xb0, 0xec, 0x2c, 0xa0, 0xc6, + 0x62, 0x52, 0x65, 0x1d, 0x71, 0x66, 0xc0, 0x92, 0x51, 0x1e, 0x28, 0x9d, 0x96, 0xfa, 0xdf, 0x19, + 0x58, 0xac, 0x80, 0xd0, 0xc9, 0xe9, 0x92, 0x9a, 0x67, 0x89, 0x64, 0x4c, 0xc2, 0xc2, 0x6a, 0x93, + 0x8c, 0x74, 0xef, 0x1c, 0x29, 0x7c, 0xc5, 0xcb, 0xbd, 0x90, 0xa2, 0x70, 0x2e, 0xe2, 0x58, 0x6e, + 0x5d, 0xa8, 0x06, 0xe2, 0x82, 0x1b, 0x03, 0xfd, 0x78, 0xa8, 0x46, 0xf6, 0x08, 0x51, 0x97, 0x4e, + 0x4b, 0x9d, 0x8d, 0x97, 0x3e, 0x0c, 0x3f, 0xad, 0xb9, 0xf5, 0xeb, 0x32, 0xcd, 0x23, 0x42, 0x20, + 0xcc, 0x9b, 0x09, 0xd4, 0x44, 0x59, 0x80, 0x87, 0x77, 0xd9, 0x1a, 0xf8, 0x86, 0x7d, 0x04, 0xe4, + 0x61, 0x79, 0x2d, 0xd6, 0x38, 0xa0, 0x68, 0x3f, 0x88, 0x1b, 0x66, 0xdf, 0xd2, 0x57, 0x25, 0x1a, + 0x2e, 0x27, 0x7c, 0xd8, 0x62, 0xbd, 0xae, 0x56, 0x16, 0xec, 0xa4, 0x8e, 0xa5, 0xe7, 0x5f, 0xdc, + 0x7f, 0x90, 0xf7, 0xf3, 0x6c, 0x8b, 0x38, 0xcd, 0x72, 0xd4, 0x1d, 0xd3, 0x5b, 0x20, 0x3c, 0xbd, + 0x75, 0x93, 0x60, 0x15, 0xcd, 0x03, 0x3a, 0x10, 0x86, 0xde, 0xb2, 0xcf, 0xe7, 0x74, 0xe3, 0x3e, + 0xf2, 0x4f, 0x7f, 0xb5, 0xed, 0xe1, 0x51, 0x08, 0x07, 0xfc, 0x86, 0xaf, 0x81, 0xfe, 0x2c, 0x8d, + 0xa6, 0x8e, 0xeb, 0x9c, 0xe4, 0xc1, 0x2c, 0xc8, 0xa2, 0xd3, 0x48, 0x5f, 0x09, 0xa5, 0x8f, 0x9f, + 0x44, 0x3f, 0x2c, 0xd2, 0x84, 0xaf, 0xa3, 0x1f, 0xd2, 0x29, 0x7e, 0x82, 0xa9, 0x51, 0xca, 0x75, + 0x2f, 0x74, 0xb8, 0xdf, 0x30, 0x19, 0xed, 0x68, 0xbd, 0xce, 0xc0, 0x40, 0x10, 0x4b, 0xfa, 0xde, + 0xee, 0x50, 0x9d, 0xb5, 0xc3, 0xc2, 0xb5, 0xad, 0xd3, 0x53, 0xab, 0x0d, 0xb9, 0x92, 0xe6, 0x05, + 0x1e, 0x4b, 0x28, 0x8a, 0x04, 0xe3, 0x6f, 0xd3, 0xf1, 0xc6, 0x20, 0x0b, 0x17, 0x6d, 0xe8, 0xab, + 0x8d, 0x1d, 0x69, 0x29, 0x44, 0x27, 0x0f, 0xd9, 0x91, 0xc4, 0x98, 0xf2, 0x59, 0x41, 0x71, 0x77, + 0x5e, 0x46, 0xe6, 0x2d, 0x67, 0x19, 0x44, 0xf3, 0x0c, 0x4b, 0xe8, 0x59, 0x09, 0x86, 0x69, 0x89, + 0x5b, 0xa1, 0xe9, 0x35, 0xef, 0x88, 0x85, 0xb2, 0xbc, 0x3e, 0x93, 0xef, 0x9e, 0xf5, 0x49, 0xb7, + 0xdb, 0x85, 0x0e, 0x7d, 0x1c, 0x54, 0xa7, 0x2f, 0x81, 0x81, 0xb3, 0xf0, 0x78, 0x04, 0xd8, 0x6c, + 0x36, 0x03, 0x30, 0xc3, 0x42, 0x5d, 0x16, 0x36, 0xd7, 0xf1, 0x06, 0xee, 0x06, 0x82, 0x92, 0xdf, + 0x10, 0xcd, 0x43, 0x54, 0x55, 0x67, 0xbb, 0xe9, 0x70, 0xd0, 0x2b, 0x48, 0x29, 0xa2, 0xde, 0xbc, + 0xc6, 0x8c, 0x7c, 0x1f, 0x15, 0x63, 0x4f, 0xa5, 0x59, 0x02, 0x1f, 0x96, 0xbe, 0x7d, 0x00, 0x07, + 0x52, 0x9a, 0x71, 0xf4, 0xc1, 0x05, 0x65, 0xc6, 0xd1, 0x57, 0x17, 0x94, 0x19, 0x97, 0xfb, 0xff, + 0xb2, 0xe1, 0xb9, 0xd1, 0x49, 0x8a, 0x83, 0x6e, 0xe8, 0x9f, 0xf3, 0x42, 0x47, 0xa3, 0x2e, 0x13, + 0xc1, 0x64, 0x3c, 0xda, 0x99, 0x88, 0x80, 0xd2, 0x53, 0x04, 0x91, 0xc7, 0x6c, 0x44, 0x76, 0xa2, + 0x28, 0xa6, 0x0d, 0xee, 0xdd, 0x0a, 0xe3, 0x3d, 0xbe, 0x6b, 0x05, 0x91, 0xaa, 0x51, 0x68, 0xb5, + 0xbb, 0x1f, 0xa2, 0x2e, 0xd6, 0x90, 0x1b, 0x10, 0x4d, 0x8f, 0x77, 0x07, 0xa0, 0x1e, 0x47, 0x77, + 0x3f, 0x40, 0x5d, 0x4c, 0xf1, 0xe6, 0x0a, 0x60, 0xc3, 0x4f, 0xd6, 0x10, 0x83, 0x9d, 0x36, 0x6a, + 0x20, 0x4f, 0x81, 0xde, 0x19, 0xaf, 0xf6, 0xb5, 0x6b, 0x13, 0x77, 0x34, 0x96, 0x1e, 0x76, 0xd3, + 0xe9, 0x4e, 0x77, 0x9c, 0x6e, 0x2c, 0x6f, 0xda, 0x29, 0xb1, 0xb2, 0x25, 0x1d, 0x1e, 0x19, 0xe4, + 0xaf, 0xd2, 0x41, 0xde, 0xf6, 0xe3, 0xd3, 0x97, 0x2c, 0x6c, 0xfb, 0x3d, 0x75, 0xa9, 0xd3, 0xff, + 0xa9, 0xb1, 0xbf, 0x06, 0x73, 0x65, 0xe6, 0x07, 0x2e, 0x5d, 0x40, 0x0c, 0xdc, 0x39, 0x5b, 0xe1, + 0x77, 0x32, 0xe0, 0xf2, 0xdc, 0xec, 0xa8, 0xd7, 0x77, 0x2f, 0x4e, 0x42, 0x3d, 0xee, 0x99, 0x18, + 0xed, 0x42, 0xfc, 0xae, 0xc4, 0x88, 0xeb, 0xb3, 0x9c, 0x3b, 0x5e, 0xbb, 0xc0, 0xcb, 0x27, 0x33, + 0xf0, 0x0d, 0x4d, 0xfe, 0xd2, 0xbd, 0xbc, 0xbc, 0xa4, 0xf7, 0x99, 0x78, 0x3f, 0x3b, 0x73, 0xcf, + 0xce, 0xce, 0xf0, 0xde, 0x95, 0xef, 0x17, 0x5d, 0x91, 0x9e, 0x51, 0xfa, 0x52, 0xd7, 0xed, 0x76, + 0xc5, 0xfb, 0x4b, 0x5d, 0x77, 0x26, 0xdf, 0x05, 0x22, 0x25, 0xe0, 0xf1, 0x6e, 0x44, 0x40, 0x98, + 0xa9, 0x98, 0x62, 0xb5, 0xdd, 0x43, 0xa1, 0x3f, 0xb1, 0x15, 0x4e, 0xe7, 0xec, 0x28, 0x12, 0xe0, + 0xb0, 0xf9, 0xde, 0x72, 0x82, 0xab, 0x6a, 0x0c, 0x8c, 0x29, 0xc8, 0x98, 0xe9, 0xd4, 0x95, 0xd3, + 0x9e, 0xca, 0x60, 0x1f, 0x25, 0x64, 0xc4, 0x54, 0x0f, 0x48, 0x56, 0x9b, 0xab, 0x6a, 0x73, 0x5d, + 0x6d, 0xae, 0xab, 0x8d, 0xf7, 0x06, 0x28, 0x68, 0xb4, 0x93, 0xbd, 0xa3, 0x89, 0xc4, 0xc6, 0x3d, + 0x40, 0x37, 0x47, 0x23, 0x04, 0x51, 0xb3, 0xcf, 0x89, 0xea, 0x73, 0xa2, 0xfb, 0x9c, 0x54, 0x7d, + 0x6e, 0x36, 0x82, 0xc3, 0x6b, 0x9d, 0x3b, 0x2f, 0xb6, 0x75, 0xc6, 0x86, 0x71, 0xf6, 0xa0, 0xac, + 0x2e, 0x6f, 0xef, 0xe1, 0xb3, 0xfd, 0x47, 0xd2, 0x20, 0xbf, 0x45, 0xa4, 0x8c, 0x6c, 0xa9, 0x66, + 0x50, 0x8c, 0x9b, 0x41, 0xb1, 0xc1, 0xd3, 0x96, 0xee, 0xb0, 0xd3, 0x3b, 0x89, 0xda, 0x70, 0xc5, + 0x20, 0x41, 0x53, 0x08, 0x9c, 0xca, 0x1f, 0xac, 0xdd, 0x6a, 0x92, 0x52, 0x5d, 0x47, 0x39, 0xe4, + 0x5b, 0x0e, 0x74, 0x79, 0x92, 0x39, 0x8f, 0x88, 0x27, 0x33, 0x70, 0x95, 0xef, 0xf0, 0x90, 0xc6, + 0x4c, 0xde, 0x8e, 0x45, 0x68, 0xc8, 0xde, 0x09, 0x63, 0xe5, 0xce, 0xbe, 0xd8, 0x98, 0xa8, 0xbd, + 0x1b, 0xa8, 0x3a, 0x73, 0x2f, 0x4f, 0xe2, 0x41, 0x2d, 0x16, 0x0d, 0xf1, 0xb3, 0x27, 0x2e, 0x47, + 0x81, 0x37, 0x54, 0xdf, 0x0a, 0x56, 0xc5, 0x62, 0xd1, 0x48, 0x10, 0x13, 0x17, 0x8a, 0x2f, 0x96, + 0xc0, 0xfe, 0xc1, 0xea, 0x48, 0x03, 0x87, 0x4e, 0xb0, 0x97, 0x23, 0x6e, 0x4c, 0x74, 0xec, 0x63, + 0x31, 0xb4, 0xe1, 0x4c, 0x9f, 0xbd, 0x71, 0xa1, 0x8f, 0x37, 0x1b, 0x67, 0xe7, 0x8c, 0x49, 0x6d, + 0x79, 0x4d, 0xa3, 0x95, 0xb5, 0x4b, 0x08, 0xc9, 0xb6, 0x31, 0xf6, 0x5e, 0xf4, 0x61, 0x29, 0x73, + 0x6a, 0xa7, 0xfe, 0x43, 0x6d, 0xb2, 0x64, 0x1c, 0x96, 0x12, 0x99, 0x5f, 0xb5, 0x8d, 0x93, 0x99, + 0x16, 0x4e, 0xea, 0x28, 0x9f, 0x58, 0xba, 0xd8, 0x94, 0x15, 0x23, 0x2b, 0x08, 0xaf, 0xe7, 0x62, + 0x25, 0x3d, 0x0b, 0x0e, 0x58, 0x14, 0xd6, 0xae, 0xee, 0x27, 0x61, 0x18, 0xb6, 0xfa, 0x17, 0x9f, + 0xb1, 0x16, 0x69, 0xcb, 0x56, 0xb7, 0x75, 0xd1, 0xa5, 0x34, 0xe5, 0x76, 0x5b, 0x2f, 0xea, 0x7c, + 0xd8, 0x60, 0x35, 0x14, 0x0a, 0x8d, 0x7b, 0xd6, 0x4b, 0xf8, 0xd9, 0x2f, 0xc9, 0x63, 0xa5, 0x83, + 0x24, 0xe4, 0xb1, 0xc8, 0xe9, 0xee, 0x9d, 0xdb, 0x37, 0xaa, 0xbf, 0xe7, 0xcc, 0xb1, 0x36, 0xcb, + 0xc8, 0xbc, 0xeb, 0x4a, 0xcb, 0x4e, 0xc7, 0x97, 0x2d, 0xd0, 0xc3, 0x67, 0x96, 0x9e, 0xb1, 0x7a, + 0x7b, 0x7c, 0xca, 0xf0, 0xf5, 0x2d, 0xf2, 0xcb, 0x02, 0x60, 0x78, 0x8b, 0x47, 0xf8, 0x0e, 0x8f, + 0xc0, 0x84, 0x6f, 0xa1, 0x5b, 0x58, 0xad, 0x48, 0x64, 0xd4, 0x89, 0xc5, 0x42, 0x58, 0x39, 0x33, + 0xb2, 0x1c, 0xe4, 0x34, 0x27, 0x05, 0x7b, 0xd0, 0x66, 0xb0, 0x07, 0x2a, 0x11, 0x46, 0xb3, 0xa7, + 0x6e, 0x10, 0xc2, 0x3e, 0xe1, 0xcd, 0x80, 0x3c, 0x6c, 0xf2, 0x2a, 0xeb, 0xfd, 0x6a, 0xae, 0x3e, + 0x4d, 0xa6, 0x73, 0xc4, 0xad, 0x68, 0x76, 0xe7, 0xe5, 0xee, 0x1d, 0xbb, 0xc7, 0xef, 0x3d, 0xd9, + 0x2b, 0x86, 0xc5, 0x72, 0x6f, 0x58, 0x2c, 0x79, 0x21, 0xcd, 0x8d, 0xa2, 0x8a, 0x1a, 0x15, 0x75, + 0xd4, 0xa8, 0xa8, 0xa3, 0x46, 0xda, 0x56, 0xa1, 0xc3, 0x8d, 0x8a, 0xb2, 0x61, 0xab, 0x34, 0xed, + 0x9d, 0xb0, 0xb6, 0x77, 0xc8, 0x47, 0x3f, 0x6c, 0xef, 0x24, 0xb5, 0xbd, 0x13, 0xec, 0x8d, 0xb5, + 0x8e, 0x94, 0x02, 0x9a, 0xcd, 0xb6, 0xe4, 0x25, 0x0c, 0x9b, 0x92, 0x43, 0x62, 0xd2, 0x1a, 0x31, + 0x90, 0x15, 0x09, 0x4a, 0x56, 0x6b, 0xf1, 0x2e, 0x13, 0xff, 0xd5, 0x92, 0x5d, 0x8a, 0x5c, 0x08, + 0x53, 0x14, 0xd1, 0x5d, 0x5e, 0x3f, 0xde, 0x89, 0xbe, 0x28, 0xce, 0xda, 0x67, 0x46, 0xc9, 0x90, + 0xda, 0xc1, 0xfd, 0x84, 0x4c, 0x6f, 0xd7, 0x48, 0x73, 0x2b, 0xf6, 0x49, 0x7b, 0x67, 0x62, 0x6f, + 0x24, 0xa4, 0x83, 0x7d, 0x55, 0xb0, 0x9a, 0x22, 0xd3, 0xed, 0x54, 0x1e, 0xa4, 0x3c, 0xc9, 0x29, + 0x3e, 0xdd, 0xb6, 0xc3, 0x4e, 0x2a, 0xcf, 0x53, 0x9e, 0x80, 0x14, 0x84, 0xd4, 0x70, 0x36, 0x7f, + 0x5d, 0xd8, 0x47, 0x33, 0x9b, 0x8e, 0x2b, 0x54, 0x62, 0x3f, 0x28, 0xa4, 0x1c, 0x62, 0x33, 0x7d, + 0x05, 0xcc, 0xcf, 0x86, 0xe2, 0x6c, 0xba, 0xa4, 0x31, 0x11, 0xbb, 0x02, 0x9b, 0x7b, 0x76, 0xbe, + 0x2d, 0xa0, 0x42, 0x71, 0x2e, 0x22, 0x54, 0x00, 0x9c, 0x5d, 0xf1, 0xa4, 0x4e, 0x78, 0x89, 0xcf, + 0x5d, 0x1c, 0x1f, 0x3f, 0x09, 0x80, 0x44, 0xe1, 0xbf, 0x43, 0xc0, 0x7d, 0x41, 0xc1, 0xba, 0x43, + 0x9c, 0xbf, 0x5f, 0x92, 0x95, 0xa6, 0xa7, 0x56, 0x89, 0xbd, 0xc7, 0x65, 0xcd, 0x33, 0xba, 0x79, + 0x96, 0xfc, 0x48, 0x35, 0xc3, 0xb3, 0x87, 0x43, 0x72, 0x04, 0xd2, 0x61, 0x92, 0x16, 0x45, 0x0a, + 0x27, 0x34, 0xa0, 0x3d, 0x71, 0x69, 0xb1, 0x6d, 0xd5, 0x50, 0xf2, 0x23, 0x18, 0x75, 0xc7, 0xc4, + 0xd4, 0x70, 0x52, 0xc9, 0x63, 0x29, 0xe8, 0x56, 0xfe, 0x2e, 0x51, 0x17, 0x58, 0xf7, 0x70, 0xe3, + 0x1c, 0xa2, 0xf9, 0x6d, 0xb9, 0xd3, 0x53, 0x72, 0x47, 0x39, 0xed, 0xda, 0x23, 0xaa, 0xb6, 0x3c, + 0xff, 0xaa, 0x20, 0x9a, 0x8d, 0x14, 0xc4, 0xb1, 0x90, 0x48, 0xc6, 0xeb, 0xfd, 0xe6, 0x31, 0xf9, + 0x17, 0x9a, 0xe3, 0x08, 0xb7, 0xc6, 0x21, 0x25, 0xcf, 0xb7, 0xc6, 0x68, 0xd6, 0xeb, 0xbf, 0x38, + 0xba, 0xb0, 0x39, 0xba, 0xd0, 0x18, 0x1d, 0xe1, 0x78, 0xd2, 0xfc, 0xf8, 0xa4, 0xff, 0x60, 0xba, + 0x9d, 0x7a, 0xab, 0x32, 0xf3, 0x5e, 0x8a, 0xfe, 0x44, 0x80, 0x4d, 0x76, 0xf4, 0xa0, 0xca, 0xe0, + 0xf7, 0x2f, 0xb7, 0x40, 0xec, 0x92, 0x92, 0xb1, 0xc9, 0xeb, 0xc1, 0xa1, 0x27, 0xcd, 0x5d, 0x59, + 0x09, 0xd5, 0x89, 0xc2, 0xa6, 0x01, 0xe4, 0xf5, 0xfb, 0x90, 0x71, 0x4d, 0x9b, 0xc7, 0xeb, 0xf5, + 0xf8, 0xd9, 0xc6, 0x08, 0xe9, 0xdc, 0x56, 0xc2, 0x5e, 0x6c, 0x9e, 0x70, 0xa7, 0x12, 0xf4, 0x95, + 0x48, 0x4f, 0x7d, 0x9b, 0x37, 0xf6, 0x90, 0xb4, 0x84, 0x77, 0x6a, 0xc1, 0x4f, 0xbb, 0x9f, 0x42, + 0xca, 0xd1, 0x77, 0xc8, 0xfe, 0x84, 0x08, 0xd5, 0x9e, 0xe8, 0x23, 0xfb, 0x36, 0xb4, 0x27, 0xd1, + 0x7b, 0xd9, 0x6d, 0x8b, 0xcd, 0x1a, 0x32, 0xa0, 0x8f, 0xba, 0x8e, 0x73, 0x62, 0x27, 0xc5, 0xa9, + 0x38, 0x59, 0x90, 0x43, 0xd2, 0x4a, 0xc9, 0x99, 0xa2, 0xf3, 0xe6, 0xd6, 0xd3, 0xf6, 0xce, 0x15, + 0xcc, 0x51, 0xaf, 0x57, 0x0b, 0x5d, 0xde, 0xce, 0xe5, 0xbe, 0x5f, 0x98, 0xe6, 0x50, 0xe0, 0x27, + 0xb4, 0x35, 0x58, 0xea, 0xbc, 0x9c, 0x36, 0x32, 0x90, 0x57, 0x09, 0x5f, 0xe8, 0xa2, 0xff, 0x3d, + 0x8b, 0xc1, 0xba, 0x90, 0x59, 0xfb, 0xb6, 0x18, 0x36, 0xff, 0x0b, 0x62, 0x1e, 0xa9, 0xa3, 0xfd, + 0x16, 0xff, 0x7e, 0x4c, 0x6f, 0x6f, 0x26, 0xb6, 0x32, 0xd0, 0x42, 0x27, 0x61, 0x65, 0x27, 0x72, + 0x5e, 0xa5, 0xda, 0x6a, 0x55, 0xdb, 0x27, 0x47, 0xbd, 0xc1, 0x96, 0x1a, 0x09, 0x0d, 0x35, 0x92, + 0xee, 0x53, 0x23, 0xe9, 0xb6, 0x16, 0x10, 0xd4, 0xe2, 0xc7, 0xcf, 0x56, 0x23, 0x4f, 0x01, 0xf8, + 0x77, 0xa9, 0x91, 0x7f, 0x12, 0x01, 0xfd, 0x59, 0x45, 0x92, 0xfd, 0x05, 0x45, 0x22, 0x3a, 0xfa, + 0x96, 0xb8, 0xf8, 0x40, 0x5f, 0x01, 0x7b, 0x30, 0x42, 0x98, 0x59, 0x4a, 0xb7, 0x37, 0x7f, 0x86, + 0xcd, 0x03, 0xe6, 0xac, 0xb7, 0x6b, 0xdb, 0x57, 0x5d, 0xe8, 0x85, 0x29, 0x9f, 0x37, 0x8c, 0xe7, + 0xed, 0x1d, 0xd9, 0x6d, 0xb6, 0xd8, 0xb6, 0xd0, 0xe1, 0xe0, 0xb0, 0xd6, 0x3d, 0x8f, 0xe3, 0xf4, + 0x96, 0xb5, 0xe2, 0xe8, 0x86, 0xb3, 0x56, 0xf0, 0x7b, 0x19, 0xb0, 0x16, 0xf9, 0x3b, 0xac, 0x75, + 0x13, 0x50, 0xb8, 0x10, 0xaf, 0xa8, 0xe7, 0x58, 0xde, 0xde, 0xd6, 0x55, 0x1d, 0xd9, 0x44, 0x36, + 0x97, 0xa0, 0x34, 0x60, 0xd1, 0x7a, 0xf3, 0x14, 0x4a, 0xde, 0x1b, 0xe7, 0xaf, 0x0f, 0x63, 0xc6, + 0x9c, 0x2b, 0x8d, 0x23, 0x88, 0xeb, 0xf1, 0xc8, 0x88, 0x5e, 0x0b, 0x28, 0xc8, 0x79, 0x5e, 0x74, + 0x72, 0x88, 0x4d, 0xe9, 0x62, 0xb0, 0x96, 0x40, 0xa8, 0x12, 0x66, 0x72, 0x28, 0x0a, 0x37, 0xdf, + 0xd1, 0x52, 0x26, 0x3c, 0xcf, 0x45, 0x4c, 0xf0, 0xe0, 0xe0, 0xaa, 0x6a, 0xcf, 0x1d, 0x9b, 0x30, + 0x38, 0x59, 0xba, 0xa4, 0x2f, 0x47, 0xdf, 0x7b, 0xbd, 0x4e, 0x2e, 0x8d, 0xbc, 0x27, 0xb1, 0xf0, + 0x85, 0x10, 0x0f, 0x8f, 0xf4, 0x52, 0x39, 0x0b, 0xe5, 0x13, 0xfa, 0x3e, 0xfe, 0xff, 0xaf, 0xef, + 0xc3, 0xa6, 0xbe, 0x0f, 0x9f, 0xab, 0xef, 0x63, 0x73, 0x1c, 0xf1, 0xbf, 0x4b, 0xdf, 0x87, 0xa3, + 0xb8, 0x39, 0xba, 0x78, 0x4b, 0xdf, 0x93, 0xec, 0xbb, 0x36, 0xb6, 0xbc, 0xa2, 0xed, 0xbd, 0xcf, + 0x7a, 0x03, 0x6c, 0x10, 0xed, 0xd9, 0x06, 0x95, 0x6a, 0x95, 0x02, 0x7f, 0xea, 0xda, 0x9e, 0x21, + 0xd7, 0x7a, 0x32, 0x4f, 0x6c, 0x7d, 0xe5, 0xbe, 0x3e, 0x2c, 0x2c, 0xe7, 0xf6, 0x55, 0x33, 0x13, + 0x06, 0x03, 0xcf, 0xc0, 0x43, 0x5b, 0xd9, 0xd1, 0x94, 0xbe, 0x7d, 0x3c, 0x65, 0x36, 0xed, 0xfd, + 0x35, 0x1d, 0x2c, 0xfd, 0xee, 0x8d, 0x54, 0x6a, 0xec, 0xec, 0xdd, 0x28, 0xaa, 0x3e, 0xf9, 0x11, + 0x4c, 0xa7, 0x52, 0x84, 0x0b, 0xfd, 0xa7, 0xaf, 0xaf, 0x9a, 0xb2, 0xbd, 0xeb, 0x18, 0x1f, 0x0d, + 0xd8, 0x35, 0x5b, 0xb0, 0x8c, 0xa2, 0x23, 0xaf, 0x9e, 0x38, 0x53, 0x63, 0x30, 0x50, 0xc1, 0xa4, + 0x47, 0xec, 0xe9, 0xf0, 0xd1, 0xa6, 0x0a, 0x3d, 0x46, 0x5b, 0x5b, 0xbf, 0xd1, 0xe3, 0x5b, 0xbf, + 0x91, 0xf8, 0x8c, 0xe8, 0x73, 0xb6, 0x7e, 0xf5, 0xcc, 0xb6, 0xc3, 0xe3, 0xd5, 0xb7, 0xc5, 0xc5, + 0xa9, 0x13, 0x63, 0x8c, 0xfa, 0x03, 0xe8, 0x03, 0x65, 0xb7, 0xf0, 0xdb, 0xd6, 0x4f, 0x76, 0x51, + 0xdd, 0xa5, 0xa1, 0x3a, 0x72, 0xb3, 0xc8, 0xd8, 0x2b, 0x56, 0x87, 0x14, 0x15, 0x80, 0x7c, 0x29, + 0xbe, 0xcc, 0x98, 0xc0, 0x03, 0x8d, 0x1a, 0x94, 0xb0, 0x67, 0x0d, 0x8c, 0x63, 0x45, 0x8a, 0x37, + 0xfc, 0x64, 0x63, 0xa2, 0xba, 0xbe, 0x4c, 0x2c, 0xef, 0xf8, 0x3f, 0xec, 0x62, 0x55, 0xd7, 0xaf, + 0xe8, 0xe4, 0x26, 0x82, 0xaf, 0x20, 0x97, 0x83, 0xae, 0x8a, 0x5a, 0x2c, 0xda, 0xda, 0xd0, 0x55, + 0x5f, 0x8d, 0xdf, 0xc2, 0x4b, 0x63, 0x53, 0xbc, 0x39, 0x97, 0x82, 0xf5, 0x1c, 0x3a, 0x96, 0x90, + 0xa8, 0x4b, 0xa4, 0xf6, 0xff, 0xa3, 0x79, 0xa9, 0x4f, 0x90, 0xfb, 0xbe, 0x31, 0x28, 0xbd, 0xcf, + 0xfd, 0x18, 0x95, 0x72, 0x03, 0x05, 0x99, 0x3a, 0xe7, 0x90, 0x6c, 0x21, 0xa1, 0xd9, 0x74, 0xf7, + 0x13, 0x14, 0xa2, 0xb5, 0x89, 0x0c, 0xba, 0xae, 0xfb, 0xd4, 0x04, 0x8c, 0xf1, 0x6f, 0xf6, 0x8c, + 0xa5, 0xea, 0xd3, 0x32, 0xf0, 0xb7, 0x3b, 0xae, 0xd7, 0x52, 0x70, 0x1c, 0xfa, 0xc6, 0xc7, 0x60, + 0x8b, 0x7a, 0xbb, 0x4f, 0xad, 0x86, 0xb1, 0x16, 0x7a, 0xfd, 0x36, 0xbb, 0x32, 0xea, 0xb1, 0x86, + 0xd1, 0x33, 0x44, 0x44, 0x72, 0x60, 0xc2, 0x71, 0x6c, 0xce, 0x36, 0xdf, 0x9a, 0x6e, 0x9a, 0x1c, + 0x9c, 0xa7, 0x3e, 0x40, 0x20, 0x85, 0xe4, 0x60, 0xe7, 0x0b, 0xc1, 0xc3, 0x02, 0x02, 0xee, 0x80, + 0x68, 0xb3, 0x33, 0x14, 0xad, 0xd7, 0xe2, 0x41, 0x5f, 0x62, 0x74, 0xaa, 0xaf, 0x91, 0x44, 0x5b, + 0x02, 0x15, 0x15, 0xc4, 0xa7, 0x99, 0x77, 0x72, 0xf7, 0xc2, 0x4d, 0xd4, 0x97, 0x5a, 0xe4, 0xf9, + 0x09, 0x67, 0xb3, 0x17, 0x1e, 0x75, 0xb8, 0xd9, 0x9e, 0xe7, 0x6c, 0x66, 0x4e, 0xb4, 0x3e, 0x83, + 0x47, 0x0b, 0xfa, 0xa7, 0xa6, 0x26, 0xb9, 0x34, 0x53, 0x68, 0x41, 0x35, 0xf9, 0xa7, 0x20, 0xb4, + 0xd8, 0xa9, 0x3e, 0xa1, 0x00, 0xe1, 0xd3, 0xdb, 0x19, 0x06, 0x2d, 0xce, 0xfe, 0xeb, 0x92, 0xfa, + 0x20, 0x87, 0xf8, 0x80, 0xfb, 0xf6, 0x07, 0x70, 0x3a, 0xbd, 0x41, 0xf7, 0x55, 0x46, 0x9f, 0x93, + 0x16, 0x57, 0x03, 0xab, 0xe2, 0x51, 0xd6, 0xee, 0x8d, 0xf5, 0x89, 0x8d, 0x6d, 0x0d, 0x36, 0x38, + 0x3a, 0xda, 0xfe, 0x12, 0x8e, 0xf8, 0x6c, 0x00, 0x97, 0x4b, 0x43, 0x0f, 0x32, 0xf0, 0x6d, 0x63, + 0x91, 0x45, 0xc9, 0xe8, 0x71, 0x75, 0x55, 0xe8, 0x25, 0x90, 0x66, 0xb6, 0x04, 0xd3, 0xdb, 0x9a, + 0xa7, 0x29, 0x0c, 0x0f, 0x5e, 0x0f, 0xfd, 0xbf, 0x9b, 0x6a, 0x73, 0xd9, 0x07, 0x4a, 0x8d, 0x38, + 0x02, 0xc5, 0xd5, 0x10, 0x89, 0x8e, 0xa0, 0x9c, 0x42, 0x78, 0x50, 0x91, 0x03, 0xa5, 0x66, 0xce, + 0xbd, 0x26, 0xcb, 0x68, 0x57, 0x06, 0xbc, 0x13, 0x5f, 0x1d, 0xcf, 0x77, 0xe5, 0x52, 0x25, 0x71, + 0x8a, 0x1d, 0xa9, 0x9e, 0x47, 0x7f, 0xf0, 0xbd, 0x0d, 0x14, 0x30, 0x5b, 0x1f, 0x1d, 0xdd, 0xec, + 0x36, 0xdd, 0xf3, 0xc5, 0x81, 0xe7, 0xc8, 0x13, 0xd1, 0xd2, 0x36, 0x45, 0xc2, 0x23, 0xf2, 0x7c, + 0x9b, 0xf3, 0xc5, 0x07, 0xc3, 0x76, 0x07, 0xcc, 0x63, 0xfd, 0xc7, 0x2b, 0x9a, 0x2a, 0x4d, 0x7c, + 0x1f, 0x4b, 0xca, 0x92, 0x1d, 0x50, 0x86, 0x62, 0xde, 0x92, 0x27, 0xcf, 0x14, 0xd3, 0x86, 0x75, + 0xa6, 0x3f, 0xf3, 0xb0, 0xd7, 0x60, 0x13, 0x23, 0x11, 0x25, 0x9e, 0x3c, 0x96, 0x64, 0x49, 0xb7, + 0x74, 0x57, 0xf6, 0x99, 0xc5, 0xbb, 0x4c, 0x28, 0x3c, 0xd8, 0xed, 0x33, 0x55, 0x34, 0x5e, 0x71, + 0xdd, 0x6c, 0xb8, 0xd3, 0x99, 0x3c, 0xa1, 0x64, 0x08, 0x52, 0xd4, 0xf5, 0x7a, 0xfb, 0xeb, 0x4a, + 0x85, 0xd7, 0xac, 0xda, 0xf7, 0xfd, 0xea, 0x83, 0x4d, 0x66, 0x5d, 0x3a, 0x51, 0xd4, 0xac, 0xba, + 0x4d, 0x1a, 0x5b, 0x67, 0xe7, 0xaa, 0xaf, 0xfe, 0x49, 0x81, 0x91, 0x54, 0x1b, 0x1d, 0x3a, 0xa2, + 0xd0, 0x94, 0x65, 0xf4, 0x65, 0x6f, 0xb0, 0xc6, 0xe8, 0xa1, 0xfa, 0x8a, 0x90, 0x77, 0x5b, 0x6c, + 0x98, 0xf1, 0xba, 0x2c, 0x36, 0x63, 0xb8, 0x63, 0xb5, 0x8b, 0x16, 0xde, 0x8b, 0x93, 0x7d, 0xc4, + 0x18, 0x8d, 0x6a, 0x4c, 0x7e, 0x89, 0x3f, 0xf7, 0x1e, 0xcc, 0x90, 0x98, 0xdc, 0xd4, 0xdb, 0x6c, + 0x9c, 0x6d, 0xc7, 0x4a, 0x38, 0x55, 0x82, 0x2e, 0x7e, 0x88, 0xc2, 0x6b, 0x72, 0xaa, 0xa2, 0xa9, + 0x47, 0x01, 0x03, 0x7d, 0xb4, 0x48, 0x9f, 0xae, 0x83, 0xb4, 0x90, 0xa9, 0xcd, 0x06, 0x16, 0x47, + 0xd3, 0x67, 0x32, 0x34, 0x52, 0xfd, 0x1d, 0x24, 0xb1, 0x4f, 0xa2, 0x06, 0x53, 0xc7, 0x70, 0xa2, + 0x7d, 0x21, 0x9c, 0x8c, 0x3d, 0xd0, 0x59, 0x1e, 0xf5, 0x57, 0x34, 0x74, 0x3c, 0xa7, 0xdc, 0x22, + 0x01, 0x69, 0x4b, 0x52, 0x58, 0x5a, 0xb8, 0xa8, 0x5e, 0xa9, 0x1c, 0xa1, 0x44, 0x38, 0x22, 0x8e, + 0x3e, 0x9f, 0x75, 0xfd, 0x64, 0xf4, 0x50, 0xf2, 0xf7, 0x59, 0xb7, 0xab, 0x83, 0x14, 0x94, 0x94, + 0xe4, 0x28, 0xb7, 0x51, 0xb4, 0x55, 0x0e, 0xd1, 0xa7, 0x4e, 0x16, 0x7b, 0x97, 0x6c, 0x6b, 0xa3, + 0xca, 0xb3, 0x56, 0x90, 0xd5, 0x11, 0x1c, 0x1a, 0x1d, 0xcc, 0x7a, 0x6d, 0x42, 0x30, 0x22, 0x84, + 0x5e, 0x97, 0x99, 0x8e, 0x97, 0xf7, 0x92, 0xed, 0x7a, 0x67, 0x32, 0x12, 0x5a, 0x39, 0x63, 0xe6, + 0xeb, 0x0f, 0xcd, 0xf0, 0x28, 0x6b, 0x7a, 0xe4, 0xe4, 0x0f, 0xd6, 0x01, 0x0f, 0x4f, 0xbd, 0x18, + 0xa3, 0x6c, 0x1e, 0x21, 0x67, 0xf5, 0xd5, 0x1a, 0xd9, 0x47, 0xe3, 0xa0, 0x65, 0xaf, 0xcf, 0xaa, + 0xe3, 0xd7, 0xf2, 0x4f, 0xcb, 0xb0, 0x87, 0x1a, 0x15, 0x9a, 0x12, 0xd4, 0xb1, 0x4a, 0x22, 0x14, + 0xf9, 0xd7, 0x30, 0xa4, 0xbf, 0x62, 0x29, 0xac, 0x5b, 0x4c, 0x9e, 0xa4, 0x94, 0x00, 0xa4, 0x8b, + 0x70, 0x53, 0xb0, 0xbb, 0x82, 0x7d, 0x2c, 0xd8, 0xdb, 0x82, 0xbd, 0x2b, 0x7c, 0xfb, 0x8d, 0xe9, + 0xb5, 0xd8, 0x37, 0x85, 0x7f, 0x6d, 0xfa, 0x23, 0x5b, 0x24, 0xf2, 0xa6, 0x60, 0x37, 0xf0, 0x16, + 0xd1, 0xe6, 0x8d, 0x0f, 0x40, 0x6f, 0x0a, 0x23, 0x60, 0xfb, 0x86, 0xce, 0xa1, 0x2b, 0x75, 0x1f, + 0x81, 0xe2, 0x0e, 0x7c, 0x95, 0x54, 0x50, 0xbc, 0xd1, 0x8c, 0x57, 0x97, 0x9e, 0x12, 0xf3, 0x43, + 0x3a, 0xaa, 0xfa, 0x30, 0xa9, 0xbf, 0x6e, 0xfa, 0x7b, 0xc9, 0xb3, 0xfb, 0xf7, 0x3c, 0xe6, 0xe2, + 0x43, 0x7f, 0xf4, 0x65, 0xef, 0xc6, 0xdf, 0xd2, 0x88, 0xc4, 0xc1, 0x4d, 0xf1, 0xd7, 0x29, 0xa4, + 0xa0, 0x36, 0xbe, 0xfa, 0x75, 0xe0, 0x34, 0x01, 0x2b, 0x07, 0x37, 0x6e, 0x44, 0xdf, 0x59, 0x8c, + 0xe4, 0xb7, 0x9e, 0xb8, 0x2f, 0xfe, 0x86, 0x8c, 0xef, 0xa7, 0xf2, 0xbb, 0xed, 0x9e, 0x70, 0xdc, + 0x92, 0xf5, 0x3a, 0x71, 0x31, 0x37, 0xbf, 0x3a, 0x88, 0x4a, 0xb6, 0x0e, 0x2b, 0x49, 0x1f, 0xd3, + 0x5f, 0x26, 0xa1, 0x8f, 0x78, 0x43, 0x99, 0x7a, 0x74, 0x8a, 0x2c, 0x71, 0x28, 0xc9, 0x38, 0xfd, + 0xa1, 0x92, 0x3b, 0x66, 0x7c, 0xac, 0x7c, 0xfb, 0xaf, 0xc8, 0xd0, 0x5f, 0x33, 0xc8, 0x86, 0xa3, + 0x68, 0xec, 0x71, 0xd9, 0x55, 0xe3, 0x23, 0xaa, 0x89, 0xf9, 0x11, 0x55, 0x56, 0x32, 0x0a, 0x79, + 0x88, 0xbf, 0x7d, 0x42, 0x7f, 0x65, 0xa6, 0x24, 0x43, 0xce, 0x5e, 0x60, 0xb1, 0x76, 0x39, 0xf8, + 0x61, 0xfb, 0x24, 0x9e, 0x36, 0x88, 0xc5, 0x0d, 0x3e, 0xba, 0x3a, 0x00, 0x10, 0x56, 0x9c, 0x06, + 0xc4, 0x4a, 0xf4, 0x05, 0x51, 0xe3, 0xf0, 0x65, 0x30, 0xbd, 0x17, 0x4a, 0x67, 0x88, 0x35, 0xf1, + 0x0e, 0x1f, 0x1a, 0xb5, 0xbe, 0x7c, 0xf7, 0x56, 0x7c, 0xc4, 0x18, 0x79, 0x80, 0x43, 0xf7, 0xe3, + 0x09, 0xed, 0x0a, 0xd7, 0xf6, 0xc7, 0xc2, 0xbf, 0xa3, 0xff, 0xd7, 0xeb, 0x87, 0x8d, 0xe3, 0x82, + 0x33, 0x73, 0x8c, 0xc4, 0xb7, 0xc4, 0x5f, 0x71, 0xb3, 0x40, 0x78, 0xae, 0xf4, 0x5f, 0x7e, 0xaa, + 0x92, 0x52, 0xda, 0xf9, 0xef, 0x0a, 0x66, 0xbf, 0x2d, 0x7c, 0xe4, 0x96, 0x98, 0xaa, 0x2d, 0x9e, + 0xe2, 0x0f, 0x00, 0xb8, 0x0b, 0x7f, 0x01, 0x6a, 0x75, 0xab, 0x2f, 0x5f, 0x7f, 0x41, 0xdf, 0x96, + 0x9c, 0x13, 0x05, 0xbb, 0x92, 0x71, 0xfd, 0x89, 0x78, 0x91, 0xdb, 0xeb, 0xfe, 0x52, 0xbc, 0x88, + 0xe8, 0x94, 0x7f, 0x2b, 0xd2, 0x5f, 0xa4, 0x77, 0xfe, 0x3d, 0xd1, 0x3d, 0x78, 0xe0, 0xff, 0x00, + 0x6f, 0x95, 0xe5, 0xa4, 0x5e, 0x6e, 0x00, 0x00 +}; + + +// Autogenerated from wled00/data/rangetouch.js, do not edit!! +const uint16_t rangetouchJs_length = 1833; +const uint8_t rangetouchJs[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xb5, 0x58, 0xdf, 0x8f, 0xdb, 0xb8, + 0x11, 0x7e, 0x3f, 0xe0, 0xfe, 0x07, 0x59, 0x6d, 0x7d, 0xe4, 0x2e, 0x57, 0xb6, 0x17, 0xc8, 0x8b, + 0x1c, 0xc6, 0x48, 0x73, 0x39, 0xa0, 0x68, 0xb6, 0x29, 0xb2, 0x39, 0xb4, 0x80, 0xcf, 0x0f, 0xb2, + 0x44, 0xdb, 0xbc, 0xc8, 0xa4, 0x8e, 0xa4, 0xbc, 0x31, 0x76, 0xf5, 0xbf, 0x77, 0x86, 0x94, 0x6c, + 0x39, 0x6b, 0x27, 0x79, 0xb8, 0x2e, 0x16, 0xb6, 0x44, 0x0d, 0x87, 0x33, 0xdf, 0x7c, 0xf3, 0x43, + 0x1e, 0x8d, 0x22, 0xfe, 0xa7, 0xfd, 0xfd, 0xf8, 0xc3, 0x68, 0x14, 0x99, 0x4c, 0xad, 0x85, 0xd3, + 0x75, 0xbe, 0x49, 0x7e, 0xb7, 0xd1, 0xee, 0x36, 0x19, 0x27, 0x13, 0xff, 0xe0, 0x2e, 0xfb, 0x24, + 0xd5, 0x3a, 0x7a, 0x29, 0x55, 0x55, 0xbb, 0xc8, 0xed, 0x2b, 0xc1, 0x63, 0x2f, 0x1c, 0xbf, 0x8a, + 0x1e, 0xb4, 0xf9, 0x14, 0x69, 0x15, 0xf9, 0x7d, 0x51, 0x21, 0x76, 0x32, 0x17, 0xd6, 0xef, 0xda, + 0x38, 0x57, 0xd9, 0x74, 0x34, 0x5a, 0x4b, 0xb7, 0xa9, 0x97, 0x49, 0xae, 0xb7, 0x23, 0x9b, 0x6d, + 0x2b, 0xed, 0x9c, 0x1d, 0x1d, 0x8f, 0xf2, 0xa2, 0xef, 0x60, 0x93, 0xb2, 0x22, 0x8d, 0x3e, 0x6e, + 0x44, 0x74, 0xf7, 0x8f, 0x8f, 0xdd, 0x42, 0x44, 0xe0, 0x86, 0x7a, 0x91, 0x3f, 0xd3, 0xd7, 0xc1, + 0xaa, 0x56, 0xb9, 0x93, 0x5a, 0x11, 0xc1, 0x1c, 0x7d, 0x8c, 0xf5, 0xf2, 0x77, 0x91, 0xbb, 0x98, + 0x73, 0x74, 0x4d, 0xaf, 0x22, 0xf1, 0xb9, 0xd2, 0xc6, 0xd9, 0xe1, 0x30, 0xae, 0x55, 0x21, 0x56, + 0x52, 0x89, 0x22, 0x1e, 0x74, 0x0f, 0xb7, 0xba, 0xa8, 0x4b, 0x31, 0x0b, 0x5f, 0x49, 0x2b, 0xca, + 0x1d, 0xa1, 0x69, 0xdc, 0xa9, 0x3d, 0x6a, 0x0a, 0xbb, 0x87, 0xc3, 0xf0, 0x9d, 0x64, 0xdb, 0x62, + 0x16, 0x2e, 0x49, 0xfc, 0x01, 0x21, 0xf8, 0x88, 0x10, 0xc4, 0x60, 0x44, 0x4a, 0x04, 0x17, 0x4f, + 0x4f, 0x56, 0x94, 0x2b, 0x9a, 0x1c, 0x1f, 0xa1, 0xde, 0x86, 0xb8, 0x8d, 0xb4, 0x8c, 0x1c, 0x8c, + 0x06, 0x8b, 0x6b, 0xc0, 0xc6, 0x3a, 0x23, 0xc1, 0xea, 0x69, 0xb7, 0x1e, 0x89, 0xe0, 0xce, 0x4a, + 0x1b, 0xb2, 0xcb, 0x4c, 0xa4, 0xf8, 0x78, 0xaa, 0x5e, 0xba, 0xa4, 0x14, 0x6a, 0xed, 0x36, 0x53, + 0x75, 0x7d, 0x4d, 0x1f, 0x71, 0xdd, 0x70, 0x37, 0x57, 0x8b, 0xa9, 0x49, 0x84, 0xaa, 0xb7, 0xc2, + 0x64, 0xcb, 0x52, 0xf0, 0xfe, 0xcd, 0xd3, 0xd3, 0x60, 0xc2, 0x0c, 0xc4, 0x4b, 0xad, 0xe4, 0xba, + 0x0e, 0xcf, 0x07, 0x63, 0x16, 0xef, 0xb2, 0xb2, 0x16, 0xb1, 0x54, 0x91, 0x19, 0x0e, 0x89, 0x49, + 0x1e, 0x8c, 0x74, 0xed, 0x33, 0xca, 0xde, 0x7b, 0x04, 0x93, 0xe0, 0xdb, 0xbf, 0x8d, 0xae, 0x84, + 0x71, 0x7b, 0x30, 0xc7, 0x24, 0x9f, 0xc4, 0x9e, 0x19, 0xda, 0x34, 0x07, 0x2b, 0x1d, 0x5a, 0xc9, + 0x14, 0x7d, 0x34, 0xc2, 0xd5, 0x06, 0xee, 0x23, 0x50, 0x29, 0x66, 0x97, 0x34, 0x38, 0xf6, 0xe8, + 0x0f, 0x4e, 0x15, 0x3b, 0x9a, 0x98, 0x82, 0x3d, 0x7d, 0xfb, 0xf0, 0xbe, 0xb3, 0x07, 0xae, 0x1b, + 0x9a, 0x8a, 0xb9, 0x5b, 0x70, 0xd8, 0x72, 0x3c, 0xb7, 0x0d, 0x76, 0x40, 0xa6, 0x3d, 0x0d, 0x8c, + 0xb3, 0x44, 0xd0, 0xa9, 0x5c, 0x91, 0x76, 0x05, 0x58, 0xf9, 0xfe, 0x41, 0x75, 0xe7, 0xdf, 0xef, + 0xb7, 0x4b, 0x5d, 0xda, 0x0e, 0xb7, 0xaf, 0xc9, 0xa0, 0x1a, 0x87, 0xc0, 0x00, 0x96, 0x2b, 0x59, + 0x3a, 0x61, 0xc8, 0x31, 0x62, 0xee, 0xe0, 0xed, 0x59, 0x15, 0x3f, 0x0b, 0x9b, 0x1b, 0x59, 0x39, + 0x08, 0x1c, 0xda, 0xd8, 0x8b, 0x45, 0x43, 0x29, 0x65, 0x2a, 0xa9, 0x6a, 0xbb, 0x49, 0xb2, 0xaa, + 0x2a, 0xf7, 0x44, 0x21, 0x9a, 0xad, 0x32, 0x75, 0xf4, 0x0e, 0x76, 0x1e, 0x23, 0x6f, 0xf8, 0x64, + 0x6a, 0x5e, 0x66, 0x66, 0x0d, 0x6a, 0x94, 0xb3, 0x1d, 0x03, 0x4c, 0xc7, 0x00, 0xc9, 0x55, 0x5d, + 0x96, 0x03, 0x7e, 0x90, 0x98, 0x9b, 0xc5, 0xac, 0x7f, 0x93, 0x3e, 0x36, 0x53, 0xf3, 0xb7, 0xdb, + 0x99, 0x6a, 0x61, 0x21, 0x92, 0x32, 0x08, 0x73, 0x02, 0x07, 0xbc, 0xcd, 0xf2, 0x4d, 0xcf, 0x33, + 0x88, 0x23, 0x06, 0x54, 0x31, 0x09, 0x9c, 0xa2, 0x60, 0x6e, 0xfa, 0x0d, 0x0f, 0xed, 0xd9, 0x48, + 0x4b, 0x01, 0x00, 0xb2, 0x6f, 0x6d, 0x05, 0x33, 0x68, 0xda, 0xb3, 0xe9, 0x9c, 0x41, 0x00, 0xf5, + 0x65, 0x2a, 0x7d, 0x0b, 0x7d, 0x09, 0xe8, 0xa3, 0x13, 0x1d, 0xc0, 0xa2, 0x09, 0x70, 0x3d, 0x66, + 0x45, 0xf1, 0xe6, 0xfe, 0x1e, 0x69, 0x06, 0xb5, 0x6c, 0xbb, 0xfc, 0x8f, 0x2c, 0xdc, 0x26, 0x9d, + 0xbc, 0x60, 0x0f, 0x99, 0xcb, 0x37, 0xc8, 0xb8, 0x63, 0x16, 0xd6, 0x81, 0x67, 0xad, 0x86, 0x5e, + 0xd2, 0xb6, 0x2b, 0xaf, 0x8d, 0xc9, 0xf6, 0xc9, 0xca, 0xe8, 0x2d, 0x29, 0x74, 0xee, 0x21, 0x4f, + 0xfe, 0xa8, 0x85, 0xd9, 0xdf, 0x8b, 0x12, 0x8c, 0xd3, 0xe6, 0x75, 0x59, 0x82, 0x17, 0x34, 0x91, + 0x2a, 0x2f, 0xeb, 0x02, 0x80, 0xc1, 0xf4, 0xa7, 0x4d, 0x92, 0x67, 0xf0, 0x00, 0x75, 0x7b, 0xa3, + 0x34, 0x3f, 0x16, 0xb1, 0x83, 0xee, 0x10, 0x56, 0x31, 0x13, 0x98, 0xbd, 0x50, 0x1f, 0x6a, 0xd4, + 0x97, 0xe2, 0x6a, 0xc3, 0x72, 0x7e, 0x5a, 0xf5, 0xc2, 0x96, 0xc1, 0x80, 0x40, 0x79, 0x02, 0xe2, + 0x0a, 0x48, 0x43, 0xeb, 0x32, 0x95, 0x63, 0xd9, 0x82, 0x33, 0x58, 0x79, 0xe9, 0x00, 0xce, 0x45, + 0xc3, 0xb2, 0x73, 0x4f, 0x35, 0x5c, 0x43, 0x81, 0x0d, 0x28, 0x37, 0xcc, 0x7e, 0x45, 0xe6, 0x1e, + 0x8a, 0x97, 0x5a, 0x37, 0x6c, 0x75, 0x4e, 0x26, 0x40, 0x24, 0xad, 0xff, 0x86, 0xf5, 0x86, 0x6d, + 0xce, 0x89, 0xe5, 0xe0, 0xc8, 0xbf, 0x74, 0x21, 0xde, 0x49, 0x8b, 0x06, 0x17, 0xdc, 0xb2, 0x3d, + 0x5f, 0xb1, 0x25, 0xdf, 0xb0, 0xed, 0x25, 0xf9, 0xb7, 0xa5, 0x40, 0xc4, 0x41, 0x7c, 0x7d, 0x51, + 0x64, 0x17, 0x04, 0xaa, 0x73, 0x02, 0x10, 0x01, 0xfa, 0xf4, 0x44, 0xac, 0xff, 0x5a, 0xf9, 0xcf, + 0x0d, 0x7c, 0xd2, 0xe1, 0x70, 0x20, 0xda, 0x3c, 0x7b, 0x7a, 0xca, 0x60, 0x05, 0x16, 0x4e, 0x8b, + 0x4c, 0xfb, 0xb4, 0xc7, 0x94, 0x5d, 0x08, 0x04, 0x14, 0x9f, 0xc9, 0xab, 0x43, 0x65, 0xea, 0x9f, + 0x89, 0x2b, 0x8e, 0xc7, 0x31, 0x46, 0x33, 0xcf, 0x1c, 0x2a, 0xd9, 0x22, 0xe1, 0xc8, 0x88, 0xcc, + 0xd2, 0xdf, 0x12, 0xf2, 0x5b, 0x71, 0x4d, 0xe9, 0x0c, 0xae, 0xe7, 0xe2, 0xed, 0x82, 0xcc, 0xaf, + 0x6f, 0x16, 0xb3, 0xb0, 0xf4, 0xd7, 0x11, 0x9d, 0x76, 0xe5, 0x75, 0x76, 0x97, 0xb9, 0x0d, 0xec, + 0xfb, 0x4c, 0xc6, 0x8c, 0xb8, 0xf9, 0x64, 0x31, 0xc3, 0x8f, 0xd6, 0x9a, 0x74, 0x4c, 0x6f, 0x60, + 0xf1, 0x76, 0x31, 0xbb, 0xc6, 0x4f, 0xb8, 0xa5, 0xe9, 0x18, 0x1a, 0xce, 0x61, 0x7b, 0x95, 0x19, + 0x2b, 0x7e, 0x29, 0x35, 0x9e, 0x9e, 0x38, 0xfd, 0x8b, 0xfc, 0x2c, 0x0a, 0x48, 0xfa, 0x43, 0x86, + 0x78, 0xe5, 0x46, 0x43, 0x9f, 0x24, 0x62, 0xe4, 0xe8, 0x95, 0x6b, 0x9e, 0x13, 0xff, 0xa4, 0xf4, + 0x43, 0xc1, 0x20, 0xa7, 0x44, 0x04, 0xff, 0x81, 0x84, 0xa7, 0xf4, 0xa3, 0x6e, 0x63, 0xf4, 0x43, + 0xa4, 0xc4, 0x43, 0xf4, 0x11, 0x3a, 0xe9, 0x5b, 0x63, 0x20, 0x3b, 0xe3, 0x37, 0x99, 0x52, 0xda, + 0x45, 0x98, 0x08, 0x51, 0x16, 0xe5, 0x65, 0x66, 0x6d, 0x94, 0xc1, 0xff, 0xe1, 0xb0, 0x18, 0x92, + 0x37, 0x74, 0x4b, 0x47, 0xd9, 0x16, 0xf0, 0x9a, 0xe1, 0x4d, 0x22, 0x42, 0xd0, 0xb9, 0x48, 0x0b, + 0x1f, 0x19, 0x72, 0xb2, 0x7a, 0x3e, 0x0b, 0x31, 0xa8, 0xa0, 0xa2, 0x2f, 0x09, 0x3b, 0xab, 0x93, + 0x85, 0xc4, 0x1c, 0x1a, 0xf5, 0x41, 0x6b, 0x68, 0x4b, 0xdc, 0x90, 0xc7, 0x86, 0x49, 0x06, 0x1f, + 0x8a, 0x32, 0xff, 0x40, 0x2a, 0x09, 0xcd, 0xfc, 0x58, 0xbb, 0xb9, 0x83, 0x94, 0x9c, 0x3f, 0x02, + 0x3b, 0xd2, 0xd8, 0xc2, 0x5a, 0x15, 0xb3, 0xd0, 0xe8, 0xbe, 0x64, 0x80, 0xe2, 0x93, 0x67, 0xb5, + 0x7c, 0x38, 0xdc, 0x69, 0x59, 0x44, 0xe3, 0x01, 0xef, 0x15, 0xf1, 0x49, 0xbf, 0x88, 0x4f, 0xb0, + 0x88, 0x33, 0xed, 0x2b, 0x3d, 0xf6, 0xb7, 0xca, 0x73, 0x15, 0xfd, 0x9f, 0x69, 0xfe, 0x5d, 0x35, + 0xc8, 0xcb, 0x8a, 0xf4, 0x27, 0x3f, 0xf5, 0xcd, 0xfb, 0x53, 0xdf, 0xe2, 0x27, 0xa0, 0xc9, 0x36, + 0xa8, 0x9a, 0x8b, 0x45, 0xba, 0x7c, 0xa6, 0x55, 0xd0, 0x74, 0x1f, 0xa0, 0xd6, 0x5c, 0x74, 0xfd, + 0x70, 0x0b, 0x88, 0x56, 0x44, 0x53, 0xda, 0x2b, 0x26, 0x53, 0xf4, 0x30, 0x3f, 0x81, 0x0b, 0xad, + 0x0d, 0x71, 0xca, 0x13, 0x5f, 0x5f, 0x03, 0x0c, 0x25, 0x47, 0x36, 0xdc, 0xd5, 0x2e, 0x43, 0x70, + 0xde, 0x2f, 0xad, 0x30, 0xbb, 0x93, 0x26, 0x0b, 0xcc, 0xea, 0x59, 0xa0, 0x2e, 0x74, 0xab, 0xbe, + 0x48, 0x02, 0x65, 0x5d, 0x14, 0x58, 0x42, 0xec, 0x05, 0x69, 0xd4, 0x33, 0x1c, 0xd6, 0xd0, 0x75, + 0xd1, 0x1e, 0x34, 0xc0, 0xc1, 0x75, 0xee, 0x7b, 0x84, 0xff, 0x9f, 0x96, 0x89, 0x0e, 0xa6, 0x1c, + 0x91, 0x5c, 0xea, 0x62, 0xcf, 0x1e, 0xf3, 0x8d, 0x2c, 0x0b, 0x2c, 0x4d, 0xd8, 0x35, 0x6c, 0xbd, + 0x74, 0x46, 0x84, 0xd9, 0xa4, 0x63, 0x80, 0x86, 0xa4, 0xac, 0x7a, 0xc7, 0xf5, 0xaa, 0xac, 0x3f, + 0x07, 0x73, 0x05, 0x0f, 0x69, 0x58, 0x20, 0x89, 0x50, 0x38, 0x11, 0x14, 0x31, 0x83, 0xf6, 0x95, + 0x3e, 0xeb, 0x2b, 0xb1, 0x56, 0x7e, 0x9e, 0x86, 0x24, 0x32, 0x0e, 0xa7, 0xb4, 0x83, 0x39, 0xdd, + 0x45, 0x5b, 0xfa, 0x9a, 0x66, 0xc1, 0x20, 0x2a, 0x2d, 0xf3, 0x90, 0x95, 0xcf, 0x88, 0x07, 0x3d, + 0x3d, 0x69, 0x4f, 0x3b, 0xa5, 0x75, 0x12, 0xfa, 0xe0, 0x17, 0x19, 0x94, 0x58, 0xb7, 0x87, 0x51, + 0x18, 0x66, 0x52, 0x13, 0xf8, 0xc3, 0x63, 0xa5, 0x95, 0x88, 0xd9, 0x19, 0xa1, 0x07, 0xb1, 0xfc, + 0xa7, 0x74, 0xbf, 0x7e, 0x8f, 0xa8, 0x77, 0xe7, 0xb5, 0xb7, 0x88, 0xc7, 0xdb, 0x4c, 0xc9, 0xaa, + 0x2e, 0xb3, 0x90, 0xe5, 0x41, 0xbc, 0x04, 0x70, 0x85, 0x12, 0x30, 0x17, 0xe0, 0x08, 0x7a, 0x21, + 0x31, 0x79, 0xe8, 0x9e, 0x1d, 0x88, 0x10, 0x6a, 0x67, 0xf4, 0xfe, 0xff, 0xe0, 0xf2, 0x77, 0xba, + 0xfb, 0x6d, 0x57, 0xcf, 0xb8, 0x37, 0xb9, 0xec, 0x1e, 0x26, 0xd1, 0xd1, 0xbd, 0xc3, 0x9e, 0x4b, + 0xc5, 0xc4, 0x79, 0x3c, 0x98, 0x82, 0x21, 0x21, 0x06, 0xcf, 0x7c, 0xaf, 0x7b, 0xd7, 0x6e, 0x8a, + 0xd3, 0xd8, 0x88, 0xad, 0xde, 0x89, 0xd3, 0xd5, 0xe9, 0x3c, 0xee, 0x31, 0x8b, 0x85, 0x1b, 0x14, + 0xeb, 0xae, 0x85, 0x2a, 0xe2, 0xc5, 0x99, 0xfc, 0x11, 0x1e, 0xd4, 0x60, 0x32, 0x4c, 0x87, 0x40, + 0xe8, 0x73, 0x64, 0x07, 0x0c, 0x04, 0xb6, 0xb6, 0x06, 0x46, 0xcc, 0xc9, 0x09, 0xe1, 0x81, 0xe7, + 0xe7, 0xdc, 0xc0, 0x7e, 0x71, 0x08, 0x16, 0xbc, 0xac, 0xac, 0xb1, 0x54, 0x7f, 0x59, 0x52, 0x60, + 0x4c, 0x86, 0xc2, 0x03, 0x16, 0x83, 0x12, 0x26, 0xe1, 0x32, 0xdf, 0x20, 0x66, 0x85, 0x07, 0x4d, + 0xd8, 0xf9, 0x78, 0xc1, 0x6a, 0xde, 0x6b, 0x6e, 0x06, 0x87, 0xc2, 0xd7, 0x0e, 0xa6, 0x92, 0x65, + 0xed, 0xe0, 0xed, 0x6c, 0x2b, 0x81, 0x66, 0x50, 0x30, 0xc7, 0x50, 0x42, 0xbf, 0x26, 0x96, 0x7d, + 0xf6, 0x62, 0x93, 0x31, 0xbc, 0x90, 0x7c, 0x4d, 0x10, 0xb0, 0xac, 0x82, 0x24, 0x0c, 0x56, 0xfe, + 0xe1, 0xdf, 0xb1, 0x69, 0xc2, 0x0c, 0xf4, 0xa6, 0x94, 0x80, 0xcf, 0x07, 0x9c, 0x68, 0x29, 0x8c, + 0x55, 0xa0, 0x69, 0x54, 0x26, 0x0f, 0x38, 0x60, 0x5e, 0x9d, 0xf0, 0xf0, 0x38, 0x78, 0x8e, 0x6e, + 0xe9, 0x08, 0xc4, 0xba, 0x16, 0x3d, 0x7e, 0x45, 0xd4, 0xe9, 0x36, 0x99, 0xe4, 0x5e, 0xe9, 0x7f, + 0x6f, 0x4a, 0xe8, 0x17, 0x2b, 0x68, 0xa7, 0x33, 0x78, 0x19, 0x4c, 0x41, 0xe6, 0xa5, 0x02, 0x22, + 0x7b, 0x69, 0xca, 0x5e, 0x8c, 0x5f, 0xa9, 0x99, 0xba, 0xe1, 0x04, 0xee, 0x6e, 0x6e, 0xaf, 0x14, + 0xbd, 0xca, 0xd2, 0x17, 0xad, 0xc4, 0x35, 0xbf, 0xbd, 0x22, 0xea, 0xe6, 0xc5, 0x18, 0x16, 0x29, + 0xab, 0xaf, 0x77, 0x44, 0xe1, 0x91, 0x57, 0x44, 0xdf, 0xd4, 0x14, 0xeb, 0x5f, 0x17, 0x23, 0x7b, + 0x3e, 0x46, 0xbd, 0x64, 0x5a, 0x87, 0x61, 0xa8, 0x0b, 0x46, 0x52, 0x48, 0xdb, 0xa5, 0x99, 0x48, + 0x2a, 0x23, 0x90, 0x6b, 0x3f, 0x8b, 0x55, 0x56, 0x97, 0x08, 0xc0, 0x41, 0xcc, 0xeb, 0xf4, 0x64, + 0x45, 0xb0, 0x40, 0x07, 0x7b, 0x36, 0x35, 0xe0, 0xe0, 0xda, 0xb5, 0x48, 0x2c, 0x99, 0x9e, 0xb6, + 0x04, 0x5e, 0x18, 0x97, 0xf5, 0x12, 0x4e, 0xb0, 0xbe, 0xda, 0x4e, 0x05, 0x9e, 0x58, 0x61, 0x1f, + 0x09, 0xcf, 0xa1, 0xa8, 0x36, 0xe4, 0x40, 0x8d, 0x23, 0x83, 0x61, 0x28, 0x85, 0x55, 0xe8, 0x72, + 0xb3, 0x38, 0x70, 0x25, 0xc6, 0xea, 0x08, 0xad, 0x2f, 0x46, 0x4a, 0x2e, 0xc0, 0x07, 0x01, 0x1d, + 0xa3, 0x32, 0xda, 0x69, 0x94, 0x62, 0x1a, 0x70, 0xf0, 0x6b, 0x00, 0x07, 0x73, 0x2d, 0xe9, 0x34, + 0xcb, 0x1b, 0xe2, 0xfb, 0x02, 0xfe, 0x68, 0xf1, 0x97, 0xc8, 0xea, 0xda, 0xe4, 0xe2, 0x0e, 0x5e, + 0xdd, 0x20, 0xd2, 0xbf, 0x7e, 0x78, 0xc7, 0x4f, 0x7e, 0x63, 0xc1, 0x06, 0xf0, 0xe3, 0x0f, 0xff, + 0x03, 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 00000000..790d8ea4 --- /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 00000000..6f6ba41c --- /dev/null +++ b/wled00/html_pxmagic.h @@ -0,0 +1,549 @@ +/* + * 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 = 8581; +const uint8_t PAGE_pxmagic[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xbd, 0x7d, 0xdb, 0x76, 0xdb, 0x46, + 0xb2, 0xe8, 0x3b, 0xbf, 0x02, 0x86, 0x3d, 0x0e, 0x61, 0x81, 0x20, 0xa9, 0x5b, 0x14, 0x50, 0x90, + 0x26, 0xb1, 0x9d, 0x89, 0xf7, 0xf2, 0x24, 0x39, 0xb1, 0x66, 0xf6, 0x64, 0x69, 0x6b, 0xc5, 0x4d, + 0xa2, 0x49, 0x22, 0x06, 0xd1, 0x1c, 0x00, 0xd4, 0x25, 0x14, 0x3e, 0xe8, 0x3c, 0x9f, 0x4f, 0xd8, + 0x3f, 0x76, 0xaa, 0xaa, 0xbb, 0x81, 0xc6, 0x85, 0xb2, 0x14, 0xef, 0xb5, 0x3d, 0x33, 0x02, 0xd0, + 0xe8, 0x4b, 0x75, 0xdd, 0xab, 0xba, 0xc0, 0x39, 0x7d, 0xf6, 0xe6, 0xa7, 0xd7, 0x17, 0xbf, 0xfe, + 0xfc, 0xd6, 0x5a, 0xe6, 0xab, 0xf8, 0xcc, 0x3a, 0xc5, 0x8b, 0x15, 0xb3, 0x64, 0x11, 0xd8, 0x3c, + 0xb1, 0xb1, 0x81, 0xb3, 0x10, 0x2e, 0x2b, 0x9e, 0x33, 0x6b, 0xb6, 0x64, 0x69, 0xc6, 0xf3, 0xc0, + 0xfe, 0xc7, 0xc5, 0xf7, 0x83, 0x13, 0x5b, 0x37, 0xf7, 0x12, 0xb6, 0xe2, 0x81, 0x7d, 0x1d, 0xf1, + 0x9b, 0xb5, 0x48, 0x73, 0xdb, 0x9a, 0x89, 0x24, 0xe7, 0x09, 0xf4, 0xbb, 0x89, 0xc2, 0x7c, 0x19, + 0x84, 0xfc, 0x3a, 0x9a, 0xf1, 0x01, 0x3d, 0xb8, 0x51, 0x12, 0xe5, 0x11, 0x8b, 0x07, 0xd9, 0x8c, + 0xc5, 0x3c, 0x18, 0x37, 0x27, 0x61, 0x9b, 0x7c, 0x29, 0x52, 0x63, 0x8a, 0xbf, 0xb2, 0xdf, 0x45, + 0xce, 0x92, 0x19, 0x76, 0xcc, 0xa3, 0x3c, 0xe6, 0x67, 0x3f, 0x47, 0xb7, 0x3c, 0xb6, 0xfe, 0xce, + 0x16, 0xd1, 0xcc, 0xba, 0x10, 0x22, 0x3e, 0x1d, 0xca, 0x76, 0xeb, 0x34, 0xcb, 0xef, 0xe0, 0xda, + 0xf3, 0x53, 0x21, 0xf2, 0xed, 0x60, 0x20, 0xae, 0x79, 0x1a, 0xb3, 0x3b, 0x3f, 0x5d, 0x4c, 0x59, + 0x7f, 0xe4, 0x5a, 0xea, 0xbf, 0xde, 0x91, 0x33, 0x19, 0x0c, 0xa6, 0x6c, 0xf6, 0x69, 0x91, 0x8a, + 0x4d, 0x12, 0xfa, 0xcf, 0xc7, 0xe3, 0x31, 0xb4, 0xe4, 0xfc, 0x36, 0xf7, 0x9f, 0x4f, 0xa7, 0x53, + 0xb8, 0x5f, 0xa4, 0xec, 0x6e, 0x10, 0xb2, 0xf4, 0x93, 0xff, 0x7c, 0x7f, 0x7f, 0x5f, 0x37, 0xac, + 0x78, 0x18, 0x6d, 0x56, 0xfe, 0xf3, 0x83, 0x83, 0x03, 0xdd, 0x14, 0x47, 0x8b, 0x25, 0x8c, 0xe2, + 0xf4, 0x0f, 0xa7, 0x8d, 0x37, 0x5c, 0x0f, 0x3c, 0x39, 0x3c, 0x38, 0xda, 0xd7, 0x6d, 0xe5, 0xd8, + 0xe9, 0xd7, 0xa3, 0x93, 0xa9, 0x6e, 0x55, 0xc3, 0x0f, 0x4f, 0x18, 0xb4, 0x64, 0x9b, 0xd9, 0x8c, + 0x67, 0x99, 0x1a, 0x3e, 0x3a, 0x38, 0x3c, 0x1c, 0xcd, 0x8c, 0x66, 0x3d, 0xc3, 0xd1, 0xe1, 0xc9, + 0x6c, 0x7f, 0x6e, 0xbc, 0x50, 0x93, 0x9c, 0xb0, 0xd9, 0x37, 0xfb, 0xc7, 0xd0, 0xce, 0xd3, 0x54, + 0xa4, 0x6a, 0x16, 0x76, 0x32, 0x62, 0x23, 0x56, 0x36, 0xea, 0x39, 0xb0, 0xe7, 0xfe, 0xb4, 0x6c, + 0x56, 0x33, 0xcc, 0xe7, 0x47, 0xdf, 0x1c, 0xe1, 0x2e, 0x6e, 0x58, 0x9a, 0x44, 0xc9, 0x42, 0xcd, + 0x11, 0xc2, 0x72, 0xa3, 0x7d, 0xa3, 0x59, 0xcf, 0xc2, 0x4f, 0x8e, 0xc2, 0xd1, 0xa1, 0xf1, 0x42, + 0xcf, 0x03, 0x10, 0x8e, 0x8e, 0x0b, 0xdf, 0xcf, 0x78, 0xcc, 0x67, 0x79, 0x24, 0x92, 0xad, 0x81, + 0xee, 0x6b, 0x96, 0xf6, 0xcd, 0xed, 0x3b, 0xc5, 0xab, 0xed, 0x1c, 0xc8, 0x3d, 0x98, 0xb3, 0x55, + 0x14, 0xdf, 0xf9, 0x3f, 0xf0, 0xf8, 0x9a, 0xe7, 0xd1, 0x8c, 0xb9, 0xff, 0xe4, 0x69, 0xc8, 0x12, + 0xe6, 0x66, 0x2c, 0xc9, 0x06, 0x19, 0x4f, 0xa3, 0xf9, 0x64, 0x2a, 0x6e, 0x07, 0x59, 0xf4, 0x07, + 0x2c, 0xe6, 0x4f, 0x45, 0x1a, 0xf2, 0x74, 0x00, 0x2d, 0x93, 0x15, 0x4b, 0x17, 0x51, 0xe2, 0x8f, + 0x26, 0x6b, 0x16, 0x86, 0xf8, 0x6e, 0x54, 0x4c, 0x45, 0x78, 0xb7, 0x0d, 0xa3, 0x6c, 0x8d, 0x1c, + 0x30, 0x8f, 0xf9, 0xed, 0xe4, 0xf7, 0x4d, 0x96, 0x47, 0xf3, 0xbb, 0x81, 0x62, 0x2d, 0x7f, 0x06, + 0x7f, 0x78, 0x3a, 0x61, 0x00, 0x44, 0x32, 0x88, 0x72, 0xbe, 0xca, 0x74, 0xd3, 0x2a, 0x4a, 0x06, + 0x4b, 0x4e, 0x7b, 0x19, 0x8f, 0x46, 0xd7, 0xcb, 0x49, 0x1b, 0xfa, 0xb2, 0xc1, 0x29, 0x66, 0x2c, + 0xb9, 0x66, 0xd9, 0x96, 0xd8, 0x1b, 0xfb, 0xff, 0xa5, 0x98, 0x8b, 0x74, 0xb5, 0x95, 0x30, 0x01, + 0x78, 0x79, 0x2e, 0x56, 0xfe, 0xfe, 0x68, 0x7d, 0x5b, 0x64, 0x2b, 0x16, 0xc7, 0x25, 0x50, 0xd3, + 0x58, 0xcc, 0x3e, 0x4d, 0x68, 0xe7, 0x37, 0x72, 0xb1, 0xc3, 0xd1, 0x48, 0x6f, 0x65, 0x7f, 0x7d, + 0x6b, 0x8d, 0xac, 0xa3, 0xf5, 0xed, 0x64, 0x26, 0x62, 0x91, 0xaa, 0x65, 0x91, 0x45, 0x1d, 0x39, + 0x04, 0x90, 0xc0, 0xfd, 0x31, 0x74, 0x83, 0xd5, 0x04, 0x00, 0x6d, 0xac, 0x5f, 0xa2, 0xc3, 0x02, + 0x69, 0x12, 0x16, 0x2e, 0x3d, 0xa9, 0x2f, 0x8a, 0xf3, 0x0c, 0x68, 0xe3, 0x6a, 0xcb, 0x05, 0xdb, + 0x52, 0x5b, 0xc8, 0x67, 0x22, 0x65, 0x48, 0x31, 0x3f, 0x11, 0x09, 0xaf, 0x2d, 0x6e, 0x50, 0xac, + 0x01, 0x42, 0x6d, 0x13, 0xc7, 0xa3, 0x51, 0xe1, 0x47, 0x59, 0x9f, 0xf9, 0x4b, 0x14, 0x40, 0x97, + 0xf9, 0x73, 0x31, 0xdb, 0x64, 0x70, 0x65, 0xc0, 0x0a, 0xd7, 0xdc, 0xd9, 0xb6, 0x26, 0x95, 0xfc, + 0xe4, 0x14, 0xde, 0x6a, 0xf0, 0x07, 0x4f, 0xc5, 0x56, 0xc3, 0xff, 0x2c, 0x5a, 0xa1, 0x42, 0x61, + 0x49, 0x8e, 0xaf, 0x24, 0x26, 0x1b, 0x78, 0x1d, 0xc3, 0xe6, 0xea, 0xdd, 0x72, 0xb1, 0xd6, 0x7d, + 0xe0, 0xb6, 0xd5, 0x01, 0xa9, 0xcf, 0xa2, 0xa4, 0x8e, 0xb0, 0x1a, 0x9f, 0x74, 0x30, 0xc4, 0x0e, + 0xd6, 0xc1, 0xee, 0x83, 0x30, 0x4a, 0x25, 0x8f, 0xfb, 0xb0, 0xaf, 0xcd, 0x2a, 0x91, 0x4b, 0x40, + 0x07, 0xb5, 0x00, 0xe8, 0xb9, 0x59, 0x1f, 0x57, 0xb1, 0x06, 0xd6, 0x21, 0x00, 0xe3, 0x00, 0x79, + 0x6e, 0xa5, 0x32, 0xf4, 0xbf, 0x3e, 0x3e, 0x59, 0x97, 0xdc, 0x4b, 0x2c, 0xe2, 0xa5, 0xe2, 0xa6, + 0xce, 0xb6, 0xb4, 0xc8, 0x4d, 0xca, 0xd6, 0x40, 0x11, 0xbc, 0xb4, 0x80, 0xc9, 0xd6, 0x0c, 0xb4, + 0xeb, 0x94, 0xe7, 0x37, 0x9c, 0x27, 0xe6, 0x64, 0xc0, 0x40, 0x23, 0x84, 0x06, 0xa1, 0xda, 0xd2, + 0x34, 0x53, 0x96, 0x45, 0x99, 0x84, 0xe8, 0x88, 0x00, 0x1a, 0x13, 0x40, 0x6b, 0x91, 0x45, 0xb4, + 0x83, 0x94, 0xc7, 0x0c, 0x69, 0x54, 0xc9, 0x11, 0xb2, 0xa0, 0x9e, 0x63, 0x30, 0xdf, 0x00, 0xfb, + 0x1a, 0x13, 0x11, 0xee, 0x3e, 0x37, 0x38, 0x66, 0x53, 0xde, 0x64, 0xfa, 0x3a, 0x0d, 0x8f, 0x1a, + 0x1c, 0xf4, 0x35, 0x88, 0x41, 0x8b, 0xeb, 0x8b, 0x28, 0x59, 0x6f, 0xf2, 0xcb, 0xfc, 0x6e, 0xcd, + 0x83, 0x64, 0xb3, 0x9a, 0xf2, 0xf4, 0xca, 0x35, 0x9a, 0xb0, 0xcf, 0x95, 0x2b, 0xf5, 0x8d, 0x8b, + 0x0f, 0x2c, 0xe5, 0xcc, 0x24, 0xb1, 0x06, 0x0a, 0x77, 0x3c, 0x51, 0xaa, 0x23, 0x65, 0xc0, 0x77, + 0x99, 0x7f, 0x44, 0x4d, 0xa5, 0x44, 0x0f, 0xcc, 0xb5, 0x0d, 0xbd, 0xef, 0xa8, 0x61, 0xfe, 0x18, + 0x50, 0x9b, 0x89, 0x38, 0x0a, 0xad, 0x8e, 0x3e, 0x62, 0x93, 0xc7, 0xc0, 0x5e, 0x7e, 0x7d, 0x0b, + 0x95, 0xad, 0xa8, 0xc9, 0xce, 0x21, 0xe0, 0xc7, 0xd8, 0x04, 0x8d, 0xb8, 0x52, 0x50, 0x1f, 0xa0, + 0x60, 0x29, 0x2d, 0x44, 0xf7, 0xb3, 0x4d, 0x9a, 0xc1, 0x84, 0x6b, 0x11, 0x11, 0xf3, 0x55, 0x58, + 0x06, 0x78, 0xaa, 0x65, 0x0b, 0x8f, 0x26, 0x1c, 0xe0, 0x5e, 0xd6, 0x7f, 0x52, 0x01, 0xd6, 0xe6, + 0xb0, 0xe8, 0x1e, 0x98, 0x2f, 0xef, 0x4b, 0x28, 0x53, 0xf0, 0x0e, 0xf8, 0x95, 0xb3, 0xad, 0x23, + 0xf1, 0x44, 0xf2, 0x9b, 0x75, 0x82, 0xfc, 0x62, 0x0e, 0x57, 0x0f, 0x21, 0xcf, 0x66, 0x69, 0xb4, + 0x26, 0x63, 0xa0, 0x36, 0x78, 0x62, 0x6c, 0x10, 0xef, 0x4d, 0x12, 0x59, 0x1d, 0xf8, 0x43, 0x6b, + 0xe4, 0xb4, 0x75, 0xb1, 0x89, 0xdb, 0x3a, 0x4c, 0xc4, 0x80, 0xf4, 0xbf, 0xd1, 0x83, 0xb4, 0xab, + 0x0f, 0x8e, 0xf9, 0x3c, 0x07, 0xf2, 0xb5, 0x74, 0x64, 0x83, 0x70, 0x13, 0xc4, 0x77, 0x69, 0x26, + 0x8e, 0x3b, 0xb4, 0x60, 0x1d, 0x0d, 0xd9, 0xbf, 0x37, 0xc0, 0x93, 0x6d, 0xac, 0x55, 0xca, 0x49, + 0x8b, 0x05, 0x01, 0x80, 0x48, 0x28, 0x4a, 0x46, 0x4e, 0x39, 0xad, 0x0b, 0xfa, 0x14, 0x0d, 0x63, + 0x6c, 0x5a, 0xa8, 0xfd, 0x51, 0x9b, 0xa3, 0x89, 0x08, 0xa0, 0x74, 0x41, 0xba, 0x06, 0x52, 0x26, + 0xb6, 0x2d, 0x31, 0x6d, 0x74, 0xb0, 0x54, 0x3f, 0xb6, 0x5e, 0x73, 0x06, 0x24, 0x9e, 0x71, 0x69, + 0x01, 0x60, 0x43, 0xd3, 0x4f, 0x11, 0x60, 0xa2, 0xd9, 0xbe, 0x12, 0x7f, 0xb4, 0x1a, 0x0d, 0x21, + 0x8a, 0x56, 0x6c, 0xa1, 0x5a, 0x15, 0x5d, 0x07, 0xa9, 0x82, 0xb7, 0xc5, 0xcb, 0x4d, 0x50, 0x48, + 0x65, 0xf8, 0x3e, 0x9b, 0xa3, 0x5d, 0xd3, 0xdc, 0x6a, 0xdb, 0x95, 0xaa, 0x61, 0x53, 0xa0, 0xe1, + 0x26, 0xe7, 0x13, 0xd4, 0xf0, 0xa5, 0x3a, 0xdb, 0xb3, 0x8e, 0x51, 0x9b, 0xa5, 0x15, 0x45, 0x72, + 0x80, 0x2d, 0x43, 0x6b, 0x0c, 0x2e, 0x61, 0xce, 0x72, 0xde, 0x1f, 0x1f, 0x1c, 0x85, 0x7c, 0xe1, + 0x4c, 0x24, 0x07, 0x1e, 0x57, 0x0c, 0x78, 0x5c, 0xe1, 0x10, 0xe7, 0xdc, 0xff, 0x3c, 0x9b, 0xa8, + 0xdd, 0x3c, 0xd0, 0x51, 0xed, 0x6e, 0xc0, 0xaf, 0x01, 0xfe, 0x8c, 0x70, 0x51, 0x78, 0x61, 0x2a, + 0xd6, 0x7f, 0xc0, 0x9d, 0xa9, 0x9b, 0x0c, 0xde, 0x0c, 0x59, 0xb6, 0xe4, 0x9d, 0xab, 0x3e, 0xa0, + 0x9f, 0xa4, 0x60, 0xec, 0xd2, 0x37, 0x6d, 0x46, 0xd6, 0x82, 0x86, 0xe6, 0xc8, 0x1a, 0x77, 0xb2, + 0x4f, 0xd3, 0x8e, 0x48, 0x54, 0x2a, 0xec, 0xc7, 0xb1, 0xe5, 0x1d, 0x65, 0x16, 0x67, 0x19, 0x1f, + 0x00, 0x23, 0x82, 0xee, 0xa9, 0xf6, 0x25, 0xad, 0xfe, 0xb6, 0xa1, 0xac, 0x3e, 0x2b, 0xcb, 0x83, + 0x5d, 0xd0, 0x2b, 0xc8, 0xba, 0x27, 0xa8, 0x96, 0x85, 0x1b, 0xb6, 0xa0, 0x95, 0x1f, 0xa1, 0xc8, + 0xc1, 0xca, 0xa2, 0x0e, 0x1b, 0x64, 0x40, 0x37, 0x18, 0xf1, 0x00, 0x17, 0xef, 0x82, 0x4a, 0x71, + 0x0d, 0x22, 0xaa, 0x45, 0x47, 0x8d, 0x45, 0x42, 0x6c, 0x65, 0x0e, 0x14, 0x42, 0x91, 0x2f, 0xd1, + 0x2c, 0x9b, 0x10, 0xf8, 0xbe, 0x96, 0x32, 0xf9, 0x3c, 0xc8, 0x97, 0x60, 0xe1, 0x5a, 0x70, 0x99, + 0xba, 0x46, 0xad, 0x7a, 0xfc, 0x19, 0xcb, 0xa5, 0xd0, 0xdc, 0x30, 0x77, 0x7f, 0x69, 0xda, 0x12, + 0xc5, 0x80, 0x00, 0x56, 0x76, 0x13, 0xe5, 0xb3, 0x65, 0x5b, 0x5b, 0x94, 0x2e, 0x52, 0x94, 0x90, + 0xde, 0x93, 0x76, 0xbc, 0xad, 0xc8, 0xa5, 0x13, 0x23, 0x67, 0x91, 0x96, 0x63, 0x5b, 0xa1, 0x40, + 0xcf, 0x21, 0x25, 0x41, 0x76, 0xd2, 0x44, 0x68, 0xcb, 0x76, 0x03, 0x46, 0x14, 0xcb, 0xd1, 0x44, + 0x29, 0x67, 0x29, 0x7c, 0xa8, 0xd5, 0xc9, 0x83, 0x18, 0x3d, 0xc1, 0x7c, 0x6b, 0x34, 0x1c, 0x1c, + 0x6a, 0x05, 0x21, 0xd7, 0xf5, 0x0e, 0xb3, 0x06, 0x4c, 0xfe, 0x94, 0x83, 0xee, 0xe0, 0x5d, 0xa0, + 0x55, 0x3a, 0x49, 0x13, 0xe5, 0xb0, 0x22, 0x0a, 0x19, 0x07, 0x84, 0xf3, 0x80, 0x24, 0x8b, 0x20, + 0x3c, 0xe8, 0x22, 0x14, 0x04, 0x5c, 0xf3, 0x0e, 0xe2, 0x34, 0x80, 0x92, 0xf6, 0x77, 0xb6, 0xe4, + 0xb3, 0x4f, 0x3c, 0xdc, 0x6b, 0xa0, 0x6d, 0xc7, 0xbe, 0xcd, 0xe8, 0x4a, 0x8e, 0x27, 0x27, 0xbc, + 0x35, 0x1a, 0xa3, 0xa9, 0x25, 0x0b, 0xc5, 0x8d, 0x8f, 0xb6, 0x1b, 0x15, 0xd0, 0xae, 0xf1, 0xdd, + 0xeb, 0x6b, 0x14, 0x55, 0x8a, 0x96, 0xee, 0x80, 0x67, 0xf8, 0xbf, 0xfa, 0x63, 0xe0, 0x0b, 0xa7, + 0x78, 0x9e, 0x0b, 0x96, 0xe5, 0x83, 0xca, 0xef, 0x2e, 0xd1, 0x39, 0x87, 0x40, 0x3e, 0x9c, 0x18, + 0x01, 0xd2, 0xc4, 0xb0, 0x10, 0x7f, 0x80, 0x66, 0x09, 0xf9, 0xad, 0xff, 0x0d, 0xfc, 0x2b, 0x3c, + 0x9a, 0x63, 0xfb, 0x39, 0x37, 0x5d, 0xe2, 0x1f, 0x83, 0x9e, 0x52, 0xc5, 0xa1, 0xb4, 0x51, 0x88, + 0xa2, 0xc5, 0x4f, 0xeb, 0xb3, 0xb6, 0xc6, 0xeb, 0xd8, 0xc3, 0xaf, 0xfd, 0x03, 0x72, 0x90, 0x05, + 0xb8, 0xd8, 0x51, 0x7e, 0x07, 0x7c, 0x76, 0x1d, 0x65, 0xd1, 0x34, 0x8a, 0xf1, 0x61, 0x19, 0x85, + 0x21, 0x4f, 0x14, 0x6c, 0x96, 0xbc, 0x0c, 0x28, 0xec, 0xd4, 0x8b, 0x93, 0x4f, 0xd4, 0x74, 0x0b, + 0x3a, 0x02, 0xba, 0x98, 0xe7, 0x68, 0x27, 0xd0, 0x91, 0xc7, 0x61, 0x1e, 0xf9, 0xdc, 0x34, 0x9f, + 0xa7, 0x62, 0xfc, 0x5d, 0x74, 0xae, 0xe7, 0x06, 0x1c, 0x3d, 0x8a, 0xa2, 0xfa, 0x5d, 0x63, 0x8c, + 0x90, 0xbf, 0x1c, 0xa0, 0x02, 0xf8, 0x5d, 0x43, 0x6a, 0xf1, 0xbd, 0x1e, 0x34, 0x58, 0xa7, 0x62, + 0x91, 0x22, 0x70, 0x6d, 0x01, 0x21, 0xf6, 0x3f, 0xac, 0xd8, 0xbf, 0x92, 0x0e, 0x33, 0x1c, 0x42, + 0xfe, 0x28, 0x3d, 0xc0, 0x1a, 0x05, 0x28, 0x3b, 0xf4, 0xaf, 0xfe, 0xc8, 0xa9, 0xda, 0x06, 0x02, + 0xb8, 0x03, 0x28, 0x88, 0x53, 0x37, 0xa8, 0x17, 0x25, 0x4b, 0x9e, 0x46, 0x79, 0x03, 0x67, 0x56, + 0x13, 0x4c, 0xc3, 0x73, 0x44, 0x85, 0xc4, 0x52, 0x54, 0x0f, 0x61, 0x04, 0xbc, 0xd3, 0x87, 0x30, + 0x99, 0x78, 0xcf, 0xad, 0xa3, 0x55, 0xee, 0xd7, 0xed, 0xc4, 0x75, 0x1d, 0xd9, 0x5f, 0xb0, 0x98, + 0x49, 0x8f, 0x5a, 0x53, 0x73, 0x21, 0x45, 0x85, 0x2f, 0x58, 0xaa, 0x4e, 0xc7, 0x46, 0x63, 0xb5, + 0x1c, 0xe6, 0xfc, 0x40, 0x50, 0xdb, 0x61, 0x68, 0x33, 0xd6, 0xed, 0x12, 0xc2, 0x2a, 0x2e, 0x19, + 0x59, 0xd2, 0x1a, 0xc8, 0xe9, 0x2c, 0x6f, 0x0a, 0xa4, 0x0c, 0xeb, 0x79, 0x0a, 0x1d, 0x08, 0x4b, + 0x27, 0xb6, 0xca, 0xb9, 0xfc, 0xa5, 0x91, 0xb0, 0xa8, 0x6c, 0x48, 0x65, 0xaa, 0x66, 0x0c, 0xf6, + 0x9c, 0x19, 0xb1, 0x25, 0xc1, 0x69, 0xce, 0x61, 0x2c, 0xd5, 0x72, 0x38, 0xd5, 0x60, 0x2b, 0x5a, + 0x2d, 0x1a, 0xc1, 0xa9, 0x31, 0xca, 0x9c, 0x4c, 0x39, 0xe7, 0x86, 0x8a, 0x6a, 0x83, 0x82, 0xb3, + 0xf9, 0x31, 0xa9, 0xbb, 0x65, 0x14, 0x87, 0xdb, 0xda, 0x18, 0xe8, 0x37, 0xdd, 0x80, 0x34, 0x24, + 0x1d, 0xae, 0xdf, 0xa8, 0x1e, 0xfc, 0xa0, 0xe6, 0xec, 0x0a, 0x52, 0xdb, 0xaa, 0xa3, 0x61, 0x24, + 0xcb, 0xcc, 0xcf, 0x48, 0xf9, 0x75, 0x9d, 0xc1, 0xd2, 0x63, 0xe2, 0x59, 0xe9, 0x39, 0x3c, 0xec, + 0xf5, 0x35, 0x23, 0xa2, 0x56, 0x00, 0x24, 0xf7, 0xab, 0x5c, 0xc2, 0x6e, 0x58, 0x4c, 0x0f, 0xe5, + 0xc1, 0xc8, 0xba, 0x9c, 0xad, 0x8d, 0x5f, 0xed, 0x05, 0xe8, 0x2e, 0xd9, 0x63, 0x58, 0xd7, 0xc8, + 0xd7, 0x3d, 0x57, 0x99, 0xe2, 0xa6, 0x69, 0xaa, 0xb9, 0x1c, 0x3b, 0xd8, 0x62, 0x87, 0xc6, 0x54, + 0x33, 0x3a, 0x75, 0x9f, 0x47, 0x2f, 0xe4, 0xc5, 0x82, 0x11, 0xb5, 0xdb, 0x31, 0x4e, 0x9d, 0x17, + 0x3f, 0x13, 0xf1, 0x0c, 0xac, 0xf1, 0x37, 0xa8, 0x43, 0x09, 0xc4, 0x56, 0xab, 0x12, 0x2d, 0x23, + 0xca, 0xd9, 0xaf, 0xc2, 0x1c, 0xb2, 0x8c, 0x9f, 0x09, 0x71, 0x60, 0xad, 0xc7, 0xbb, 0x94, 0x2c, + 0x81, 0x90, 0x8f, 0x60, 0xcd, 0xd6, 0x51, 0x62, 0x8d, 0x33, 0x4b, 0x2a, 0x23, 0xf0, 0x02, 0xe7, + 0x98, 0xf4, 0x87, 0xdd, 0x03, 0x05, 0x20, 0x98, 0xcd, 0x79, 0xf8, 0x0e, 0x83, 0xc3, 0x6d, 0x2d, + 0xc4, 0xc0, 0x68, 0xf9, 0x1a, 0xd4, 0x49, 0xb8, 0xdd, 0xc1, 0x0b, 0x55, 0x56, 0xdb, 0x31, 0xf3, + 0x79, 0x5a, 0x55, 0x66, 0x19, 0x4e, 0x59, 0xc7, 0x5e, 0xdb, 0xea, 0x49, 0xe0, 0xcb, 0xf8, 0xa7, + 0xd3, 0x3e, 0x37, 0xd2, 0xab, 0x7f, 0x45, 0xfe, 0x63, 0x56, 0xdf, 0xcc, 0xda, 0x7d, 0x0d, 0xe8, + 0xdd, 0x6a, 0x35, 0xa9, 0xc3, 0xdb, 0x8a, 0x0b, 0x31, 0x87, 0x57, 0xa5, 0xed, 0x28, 0x69, 0xd7, + 0xcd, 0x83, 0x5a, 0x5c, 0x75, 0x86, 0xcd, 0x7d, 0x30, 0xd3, 0xd6, 0x0c, 0xc9, 0xaa, 0x64, 0x77, + 0xf1, 0xd7, 0x4f, 0xfc, 0x6e, 0x9e, 0x32, 0x40, 0x83, 0x85, 0xd8, 0xdf, 0xe6, 0x62, 0xdb, 0x8a, + 0x7d, 0x0f, 0x8e, 0x47, 0x18, 0xfb, 0xd6, 0x3a, 0x97, 0x36, 0xa4, 0x36, 0x40, 0x59, 0xdf, 0x71, + 0xbd, 0xef, 0x1c, 0x76, 0xfb, 0x2e, 0xd9, 0x1e, 0xfd, 0x65, 0xab, 0xdd, 0xa1, 0xb1, 0xe9, 0x0e, + 0xd1, 0x6d, 0xcc, 0xbb, 0xdd, 0xa8, 0x91, 0x53, 0x7c, 0x53, 0x1b, 0xb8, 0xab, 0x57, 0xd1, 0x3b, + 0x1d, 0xca, 0x63, 0x1c, 0xeb, 0x74, 0xa8, 0xce, 0x9e, 0xd0, 0xa7, 0x82, 0x4b, 0x18, 0x5d, 0x5b, + 0x33, 0x10, 0xfe, 0x2c, 0xb0, 0x4b, 0x67, 0xd2, 0x6e, 0xb7, 0x83, 0x1c, 0x61, 0x2b, 0xce, 0x6d, + 0xf5, 0xa2, 0x30, 0xb0, 0xf1, 0xee, 0x6f, 0x1c, 0x3a, 0xc3, 0x1a, 0xb6, 0x95, 0x08, 0xe2, 0x32, + 0xb8, 0xaf, 0x0f, 0x95, 0xc4, 0xc4, 0x91, 0xd9, 0xf5, 0x42, 0x37, 0x92, 0xc5, 0xb2, 0xad, 0xde, + 0xed, 0x2a, 0x4e, 0xb0, 0x4f, 0x9e, 0xaf, 0xfd, 0xe1, 0xf0, 0xe6, 0xe6, 0xc6, 0xbb, 0x39, 0xf0, + 0x44, 0xba, 0x18, 0x82, 0xdd, 0x1a, 0x0d, 0x61, 0x80, 0x6d, 0xc9, 0xa3, 0x2e, 0x1b, 0x1a, 0x6c, + 0x4b, 0x4a, 0x5b, 0x60, 0x8f, 0x0f, 0xe0, 0xa1, 0x87, 0xe7, 0x62, 0xdf, 0x89, 0xdb, 0xc0, 0x96, + 0x16, 0x11, 0x34, 0x33, 0x34, 0x9f, 0x9d, 0x52, 0x82, 0x64, 0xe7, 0xb0, 0xe1, 0xd9, 0x29, 0xce, + 0x8b, 0x58, 0x00, 0x20, 0x15, 0xa8, 0x3d, 0x05, 0x16, 0xf0, 0x57, 0x6b, 0xe3, 0xc8, 0x36, 0xb6, + 0x65, 0xec, 0x8d, 0x92, 0x28, 0x16, 0x6c, 0x1e, 0xe0, 0x16, 0x59, 0x8e, 0xc7, 0x6b, 0xf6, 0xd9, + 0x0f, 0xea, 0x0e, 0xb0, 0x4c, 0xef, 0xa1, 0x1f, 0xb9, 0xf7, 0x16, 0x65, 0xf6, 0x6c, 0xb4, 0x29, + 0x80, 0x22, 0x3a, 0x89, 0x2b, 0x07, 0x59, 0x88, 0xc4, 0xea, 0x29, 0xe5, 0xff, 0xde, 0x00, 0x1f, + 0x87, 0xdd, 0xa0, 0x3d, 0x0c, 0x88, 0x04, 0xe2, 0x67, 0xe0, 0x38, 0x9e, 0x5b, 0x3f, 0xc2, 0x43, + 0x13, 0x8c, 0x5e, 0x1b, 0x8e, 0x0a, 0x06, 0x79, 0x07, 0x33, 0x6f, 0xa0, 0xf9, 0x47, 0x7e, 0x63, + 0xc9, 0x89, 0xba, 0x40, 0x92, 0x97, 0x9e, 0x89, 0xa2, 0x47, 0x21, 0xcd, 0x7c, 0x6b, 0xa6, 0xa4, + 0x6c, 0x9c, 0xcc, 0xd8, 0xc8, 0x9a, 0xa1, 0xc7, 0x9e, 0xc0, 0x5e, 0xe4, 0x4d, 0xb5, 0x0f, 0x95, + 0xc2, 0x92, 0xb0, 0xeb, 0x6e, 0x04, 0x7e, 0xf9, 0xd0, 0x33, 0xe0, 0x15, 0x94, 0x11, 0x95, 0x9b, + 0x3a, 0xfb, 0x20, 0xc7, 0xe2, 0x41, 0xab, 0x88, 0x66, 0x80, 0x1c, 0xf9, 0xb6, 0xd1, 0x0d, 0x18, + 0x04, 0xe6, 0xa0, 0x33, 0xcf, 0xc0, 0xbe, 0xfc, 0x6a, 0x4e, 0xff, 0xbe, 0xba, 0xb2, 0x55, 0x06, + 0x8f, 0xc3, 0x52, 0xfa, 0x0e, 0xc0, 0x7e, 0x97, 0xc0, 0x9e, 0xa2, 0x70, 0xc3, 0x62, 0x6b, 0xe7, + 0x7c, 0xfb, 0xc6, 0x7c, 0x23, 0xd7, 0xaa, 0xa6, 0x3c, 0x7b, 0x87, 0x31, 0xd8, 0xce, 0x71, 0x07, + 0xb6, 0x65, 0x0c, 0x3b, 0xaa, 0x8d, 0xec, 0xfd, 0x82, 0x09, 0x0e, 0x63, 0xe8, 0x50, 0x02, 0xd5, + 0xa4, 0xd1, 0x23, 0x08, 0xd2, 0xdb, 0x41, 0x11, 0x93, 0x20, 0xe0, 0x8d, 0x00, 0xff, 0xd8, 0x67, + 0x3f, 0xd1, 0x75, 0x07, 0x39, 0x54, 0x27, 0xa9, 0x16, 0xf4, 0xc3, 0x9f, 0x26, 0x46, 0x4f, 0x61, + 0xe1, 0xf7, 0x4c, 0x24, 0xdd, 0xc8, 0xff, 0xcf, 0xf7, 0x6f, 0xdf, 0x58, 0xff, 0xf1, 0xe1, 0xa7, + 0x1f, 0x77, 0x62, 0x70, 0xc9, 0x00, 0x53, 0x3f, 0x88, 0x15, 0xb7, 0xbe, 0xcd, 0x40, 0xcf, 0xa3, + 0x49, 0xdb, 0xd9, 0x17, 0x5c, 0xbd, 0xd8, 0x3e, 0x7b, 0xfd, 0x8f, 0x5f, 0xde, 0x3f, 0x80, 0xd5, + 0xde, 0x6e, 0xec, 0x82, 0x00, 0x58, 0x7a, 0xd3, 0xa4, 0x62, 0x03, 0xdb, 0x74, 0x4e, 0x76, 0x08, + 0x47, 0xaf, 0x5b, 0x92, 0xe5, 0x99, 0xbe, 0x7d, 0xf6, 0x86, 0xae, 0x9f, 0xd7, 0x26, 0xaa, 0xbf, + 0x44, 0xbd, 0x7e, 0xe8, 0x56, 0x25, 0x9f, 0xd3, 0x24, 0x3d, 0x02, 0x60, 0x93, 0x44, 0xff, 0xde, + 0xf0, 0x77, 0x80, 0xe5, 0x7f, 0xd0, 0x9d, 0xf5, 0x2e, 0xfc, 0x3c, 0x14, 0xe5, 0x20, 0x09, 0x47, + 0xf5, 0xf8, 0x25, 0x90, 0xcc, 0x53, 0x08, 0xb5, 0xc2, 0xf8, 0xee, 0x47, 0x52, 0x6e, 0xdf, 0xab, + 0xa7, 0x4e, 0xf5, 0xd6, 0x86, 0xa8, 0x36, 0x58, 0xd9, 0xab, 0x5a, 0xd3, 0x2e, 0xdd, 0xf6, 0x38, + 0xd5, 0xd6, 0x7b, 0x9c, 0x6e, 0x33, 0x29, 0x9b, 0xf1, 0xc5, 0x0a, 0xf3, 0xd5, 0x36, 0x48, 0x00, + 0xdd, 0x99, 0x98, 0xed, 0xd5, 0x05, 0xaa, 0xec, 0x4b, 0x0a, 0xae, 0x1a, 0xd9, 0x64, 0x5d, 0x30, + 0x6e, 0x00, 0x04, 0x1b, 0x28, 0x6b, 0x37, 0x3e, 0x06, 0xc8, 0xa8, 0xa1, 0x34, 0x79, 0xc7, 0x30, + 0x48, 0xaf, 0x37, 0xb2, 0x1e, 0xa9, 0x36, 0x1e, 0x67, 0x74, 0xa6, 0x14, 0x7e, 0x25, 0xe0, 0xe5, + 0xd8, 0x67, 0xdf, 0x95, 0xf7, 0x15, 0x69, 0xcc, 0x99, 0x8c, 0x13, 0x1b, 0xbb, 0x41, 0x35, 0x4a, + 0xd8, 0x6a, 0xb2, 0x19, 0x73, 0xd2, 0xd6, 0xcd, 0xe7, 0xde, 0x2a, 0x4a, 0x68, 0xcb, 0xe0, 0x31, + 0x82, 0x6e, 0x3d, 0x3a, 0x2a, 0x6d, 0xd6, 0x78, 0xff, 0xc4, 0x2e, 0x69, 0x66, 0xe4, 0x7f, 0xed, + 0x4e, 0x06, 0xe9, 0xd5, 0x27, 0xfe, 0x27, 0xce, 0x51, 0x0e, 0x6f, 0x1d, 0xaa, 0x59, 0xf2, 0x7c, + 0xa9, 0xb6, 0x56, 0x03, 0x69, 0xbd, 0xa7, 0xf1, 0x4e, 0x37, 0x36, 0x4b, 0x37, 0x1f, 0x14, 0xd7, + 0xb7, 0xfa, 0xbe, 0x42, 0xa6, 0xec, 0xa9, 0x66, 0x92, 0xa9, 0xc2, 0xe6, 0xee, 0x28, 0x93, 0x38, + 0x15, 0xb7, 0x76, 0x59, 0xf3, 0x53, 0x4e, 0x49, 0xb8, 0x34, 0x56, 0x00, 0xfd, 0xbd, 0x66, 0x49, + 0x7d, 0xba, 0x12, 0x67, 0xc0, 0x1a, 0xf0, 0x12, 0x37, 0x57, 0x32, 0xe7, 0x13, 0x64, 0x97, 0x36, + 0x43, 0xbe, 0xe7, 0x1a, 0xd0, 0x96, 0xe4, 0x14, 0x92, 0xc0, 0x9e, 0x2e, 0xaa, 0x26, 0x8b, 0xda, + 0xbe, 0x6c, 0x6f, 0xad, 0x15, 0x68, 0x8b, 0xed, 0x75, 0x1f, 0xdc, 0x69, 0xaf, 0xb5, 0xd5, 0x27, + 0xef, 0x54, 0x1e, 0x20, 0xea, 0x4d, 0xfe, 0x42, 0x4f, 0xff, 0x13, 0xfb, 0x33, 0xe7, 0xa5, 0xad, + 0xd5, 0x16, 0x7a, 0x14, 0xfd, 0x7a, 0xad, 0x5d, 0x75, 0xdb, 0xae, 0xda, 0x52, 0x0f, 0x18, 0xb0, + 0xc7, 0x69, 0x05, 0x52, 0x45, 0x60, 0xa2, 0xf1, 0xd2, 0xad, 0xa6, 0x65, 0x95, 0x41, 0xb9, 0x53, + 0x39, 0x80, 0xf6, 0xa8, 0x6e, 0xb5, 0xa4, 0x1d, 0xdb, 0x7f, 0xc6, 0x6c, 0x48, 0xd5, 0x07, 0x2e, + 0x39, 0x5d, 0x1f, 0x06, 0x41, 0x79, 0xe4, 0x72, 0x84, 0xf4, 0xc7, 0xd5, 0x7d, 0xaf, 0x0b, 0x88, + 0x6e, 0xfc, 0x19, 0x62, 0xf6, 0xc5, 0xd8, 0x93, 0x21, 0x21, 0x5a, 0x3b, 0xbc, 0xee, 0xf0, 0xe2, + 0xeb, 0xd0, 0xab, 0x21, 0x96, 0x34, 0x71, 0xf2, 0x5e, 0x01, 0x7f, 0xf8, 0x30, 0x02, 0x77, 0xb9, + 0x20, 0x9b, 0x54, 0xa9, 0x89, 0x37, 0xea, 0xae, 0xae, 0xd3, 0x3b, 0x55, 0x7a, 0xef, 0x01, 0xfc, + 0x96, 0x13, 0x12, 0x8c, 0xd5, 0x93, 0x36, 0x62, 0xde, 0x91, 0xe1, 0xbb, 0x23, 0xa7, 0xcb, 0x3b, + 0x50, 0xf4, 0x4a, 0xe1, 0x67, 0x39, 0x5f, 0x63, 0x3f, 0xf0, 0xcf, 0x69, 0x95, 0x95, 0x08, 0xe5, + 0x22, 0x3c, 0x8d, 0x66, 0x76, 0xa7, 0xa9, 0x31, 0x34, 0xb8, 0x7d, 0x96, 0xf1, 0x59, 0x83, 0x84, + 0x8f, 0xe1, 0xaa, 0x5e, 0x4b, 0xa3, 0x45, 0x72, 0xba, 0x8b, 0xf2, 0xfe, 0x11, 0x88, 0x79, 0x88, + 0x74, 0xc6, 0xa4, 0x95, 0xfa, 0x8a, 0x1a, 0xc8, 0xd9, 0xaf, 0x7c, 0x15, 0x13, 0x37, 0xbd, 0x27, + 0x21, 0xe7, 0x11, 0xb8, 0xe9, 0x59, 0x9f, 0xc5, 0x11, 0x72, 0x7b, 0x5b, 0xf1, 0x76, 0x33, 0x7d, + 0xaf, 0x8d, 0x5c, 0xca, 0xbe, 0xec, 0xe2, 0x7c, 0xca, 0x29, 0x81, 0x1b, 0xbe, 0x14, 0x22, 0xe3, + 0x18, 0x1d, 0xe0, 0xb3, 0xe9, 0x1f, 0x61, 0x8d, 0xdf, 0x99, 0xf5, 0x1a, 0x9b, 0xad, 0x7c, 0xc9, + 0x72, 0x08, 0xf1, 0xe3, 0x18, 0x50, 0x03, 0x8b, 0xce, 0x38, 0xb4, 0x70, 0xac, 0x53, 0x4d, 0x45, + 0xb2, 0x38, 0x33, 0x40, 0xb4, 0xd6, 0x58, 0xd4, 0x9a, 0x61, 0xee, 0x83, 0x5e, 0xf5, 0x00, 0x47, + 0xd4, 0x57, 0x66, 0x09, 0xa0, 0x5d, 0x4e, 0x5b, 0x57, 0xc6, 0x04, 0x8a, 0xa2, 0x91, 0x7a, 0x40, + 0xf2, 0xa8, 0x5b, 0xad, 0x1a, 0x54, 0x71, 0xaa, 0xfd, 0x67, 0x3c, 0x48, 0x85, 0x8a, 0x3f, 0xe1, + 0x46, 0x12, 0xe0, 0xa0, 0x21, 0x08, 0xf9, 0x59, 0x33, 0x20, 0xeb, 0x29, 0xa8, 0x55, 0x2f, 0x02, + 0x5b, 0xdf, 0x3f, 0x39, 0x20, 0xeb, 0x35, 0xbc, 0xcd, 0xcd, 0x1a, 0xd3, 0xab, 0x10, 0x18, 0xd0, + 0xf5, 0x21, 0x5f, 0x52, 0x62, 0xb5, 0x27, 0x0d, 0x61, 0x66, 0xc9, 0x81, 0x3c, 0xb4, 0x72, 0x61, + 0x9d, 0x32, 0xa9, 0xea, 0x63, 0x1e, 0xbe, 0x0d, 0x23, 0xd0, 0xb4, 0xcb, 0x94, 0xcf, 0xcb, 0x14, + 0xd0, 0x25, 0xb6, 0x0f, 0xa2, 0xf5, 0xd5, 0x90, 0xd3, 0xcb, 0x5e, 0xce, 0xd2, 0x05, 0x56, 0x46, + 0xff, 0x36, 0x8d, 0x59, 0xf2, 0x89, 0x2c, 0x9f, 0xa4, 0x24, 0xc6, 0x80, 0x25, 0x59, 0x61, 0x61, + 0x76, 0x66, 0x01, 0x63, 0xc8, 0x95, 0x5a, 0xd4, 0x6d, 0xc4, 0x70, 0x88, 0x6a, 0x52, 0x45, 0xaa, + 0xc8, 0xa1, 0xf4, 0xfe, 0xaa, 0x06, 0x4d, 0x98, 0x5d, 0x2a, 0x7d, 0xad, 0x22, 0x2e, 0x35, 0xe0, + 0x3d, 0x52, 0x01, 0xda, 0xdf, 0xa4, 0x6c, 0x01, 0x06, 0x21, 0xb4, 0xf0, 0x05, 0xa0, 0x74, 0x1e, + 0xc5, 0xdc, 0x5a, 0xf2, 0x94, 0x23, 0x70, 0xb3, 0x38, 0x9a, 0x7d, 0x42, 0x1c, 0x64, 0x1a, 0xe3, + 0xf4, 0xfa, 0x74, 0xb8, 0x6e, 0xea, 0x4f, 0x6c, 0xd7, 0x0c, 0x98, 0x89, 0x4d, 0x3a, 0x53, 0x4e, + 0x80, 0xbe, 0xef, 0xb1, 0xd9, 0x8c, 0xaf, 0x73, 0x45, 0xdb, 0xe1, 0xef, 0xeb, 0x85, 0xab, 0xef, + 0xb8, 0xbe, 0x5d, 0x27, 0xfa, 0x6e, 0x11, 0xcd, 0x77, 0x9a, 0xa6, 0x27, 0x24, 0x74, 0x24, 0xcf, + 0x62, 0xde, 0x90, 0x0e, 0x03, 0x14, 0xa8, 0xf2, 0xc1, 0x2e, 0xd5, 0xaf, 0x7e, 0x26, 0x27, 0x3b, + 0x4f, 0xca, 0x04, 0xe1, 0x99, 0xa5, 0x6f, 0x61, 0x51, 0xd9, 0xa9, 0x25, 0x38, 0x43, 0x4c, 0x29, + 0x6a, 0x75, 0x4e, 0xa9, 0x9d, 0x94, 0x63, 0x96, 0xef, 0x41, 0xc3, 0x2a, 0x9d, 0x23, 0x33, 0x01, + 0x8e, 0x2e, 0x90, 0x9c, 0x50, 0xd7, 0x79, 0x19, 0x5e, 0xd5, 0x5a, 0x24, 0x59, 0xe5, 0x52, 0xa9, + 0x27, 0xe8, 0x12, 0x8a, 0x24, 0xbe, 0xc3, 0x36, 0x79, 0x87, 0x1a, 0xcc, 0x2a, 0xff, 0x9d, 0x0e, + 0xf5, 0x4c, 0x75, 0xc4, 0xa8, 0x63, 0x11, 0x7b, 0xa7, 0xd0, 0x37, 0xac, 0xfe, 0x2e, 0xe4, 0x75, + 0xe3, 0xee, 0xb5, 0x58, 0xdf, 0x5d, 0x88, 0xd7, 0x71, 0xb4, 0x9e, 0x0a, 0x96, 0x82, 0xe0, 0xf5, + 0x2c, 0x6c, 0x42, 0x0e, 0x2a, 0x1b, 0x3b, 0x90, 0xd9, 0x36, 0x6a, 0x4f, 0xa4, 0xd9, 0x07, 0x76, + 0x0d, 0x38, 0xc4, 0xbf, 0x4f, 0x9b, 0xbc, 0xf7, 0x98, 0x4d, 0xbd, 0x11, 0x37, 0x09, 0xca, 0xa8, + 0xca, 0x31, 0x82, 0xc8, 0xa8, 0x86, 0xf6, 0x4e, 0x7a, 0x3b, 0x14, 0xaa, 0x14, 0x85, 0x68, 0xb5, + 0x89, 0x29, 0xf3, 0xfc, 0x68, 0xa7, 0xeb, 0x61, 0x06, 0x6e, 0x80, 0xdb, 0xd3, 0xc8, 0x50, 0xeb, + 0x94, 0xf0, 0xea, 0x86, 0x07, 0xd8, 0xb8, 0xe3, 0xd2, 0x3b, 0x95, 0xe5, 0xe4, 0x67, 0xa8, 0x00, + 0x2b, 0x95, 0x97, 0x81, 0xce, 0x5b, 0x44, 0xf9, 0x72, 0x33, 0xf5, 0x66, 0x62, 0x35, 0x54, 0xdf, + 0x64, 0x0c, 0xe9, 0x53, 0x0c, 0xfa, 0x12, 0x03, 0x3f, 0xc4, 0xb0, 0xad, 0xa6, 0x16, 0xec, 0x59, + 0x7f, 0xa3, 0x41, 0xd6, 0xcb, 0x19, 0x30, 0xc4, 0xc4, 0x6a, 0x7e, 0xba, 0x21, 0xf5, 0x21, 0xca, + 0x93, 0x5a, 0xb3, 0xa1, 0xf8, 0x1a, 0xa5, 0x23, 0x14, 0x09, 0xd5, 0x7b, 0xa8, 0x03, 0xb5, 0x4a, + 0x94, 0x86, 0xea, 0xb4, 0x40, 0xba, 0x0e, 0x67, 0x3d, 0x18, 0x9c, 0xe5, 0x56, 0x18, 0x84, 0x62, + 0xb6, 0xc1, 0xec, 0x82, 0x0b, 0x06, 0x97, 0xad, 0xb2, 0x20, 0xe1, 0x37, 0xd6, 0x3f, 0x7e, 0x79, + 0xff, 0x81, 0xb3, 0x74, 0xb6, 0xfc, 0x99, 0xda, 0xfa, 0x37, 0x51, 0x12, 0x8a, 0x1b, 0x2f, 0x16, + 0x33, 0xf2, 0xfc, 0xbc, 0x8c, 0x5e, 0x3a, 0x2e, 0x26, 0xbf, 0x03, 0x39, 0xce, 0x83, 0xfd, 0xf5, + 0xed, 0x65, 0x62, 0x3b, 0xe7, 0xcd, 0x06, 0xbf, 0x39, 0x1c, 0x87, 0x9d, 0x77, 0x35, 0xfa, 0xe0, + 0x06, 0xd1, 0x7f, 0x6c, 0x77, 0x9d, 0x8a, 0x5c, 0x00, 0xd9, 0xa5, 0x2e, 0xf5, 0xed, 0x20, 0x08, + 0x9a, 0x23, 0x74, 0x97, 0x73, 0x69, 0x7c, 0x6c, 0x7f, 0x57, 0x07, 0xac, 0x23, 0xb1, 0xd0, 0xde, + 0xfc, 0x06, 0x1b, 0x0b, 0x3e, 0xbe, 0xd8, 0xea, 0x17, 0xc5, 0x70, 0xf8, 0x62, 0x8b, 0x0b, 0x17, + 0x1f, 0x27, 0x12, 0x1f, 0x3a, 0x9d, 0x1f, 0x80, 0x86, 0x47, 0xac, 0xf4, 0xab, 0x04, 0xbf, 0x33, + 0xd1, 0xb7, 0x9e, 0xb4, 0xa7, 0xf8, 0xe8, 0x96, 0x6d, 0x2c, 0x0c, 0xdf, 0x62, 0x3d, 0xe3, 0xfb, + 0x08, 0x1c, 0x3a, 0xa0, 0x49, 0x1f, 0x62, 0x42, 0xca, 0x8c, 0xb8, 0x2c, 0xbb, 0x4b, 0x66, 0x7d, + 0x27, 0x38, 0xdb, 0x3e, 0x08, 0x43, 0x35, 0x73, 0xf1, 0xd1, 0x65, 0x37, 0x2c, 0xc2, 0xd2, 0x53, + 0x99, 0x38, 0xea, 0x3b, 0xaa, 0x41, 0xba, 0x02, 0x7d, 0xa7, 0x5c, 0x96, 0xec, 0x56, 0xdf, 0x29, + 0xa8, 0x56, 0xc6, 0xc2, 0x8c, 0x2b, 0x8a, 0x3d, 0xae, 0x13, 0x5c, 0x5e, 0xb9, 0xf4, 0xcc, 0x93, + 0x90, 0x9e, 0xb7, 0xc5, 0x64, 0xbe, 0x49, 0xe8, 0x98, 0xcd, 0xd2, 0xbb, 0xe3, 0xce, 0x36, 0xe5, + 0xf9, 0x26, 0x4d, 0xac, 0x10, 0xe9, 0xf5, 0x56, 0x36, 0x7f, 0x77, 0xf7, 0x2e, 0x84, 0x57, 0x45, + 0xd9, 0xbd, 0xb1, 0xda, 0x56, 0xe2, 0xca, 0x40, 0x52, 0xe9, 0x0d, 0x38, 0x13, 0xee, 0x91, 0x74, + 0xc8, 0x8b, 0xa7, 0xdc, 0xbb, 0xbe, 0x5d, 0x3a, 0x06, 0xb6, 0x5b, 0xdf, 0xac, 0x53, 0x10, 0x7e, + 0xac, 0x72, 0x31, 0x54, 0x02, 0x31, 0x20, 0x51, 0xaf, 0xb3, 0xa5, 0x6e, 0x3e, 0x2f, 0xaa, 0xe5, + 0xca, 0x10, 0xc4, 0x71, 0xd5, 0xdb, 0xdc, 0x78, 0x6b, 0x78, 0xe1, 0xe5, 0xfb, 0xc4, 0x78, 0x2f, + 0xa9, 0xe9, 0xb2, 0x40, 0x93, 0x63, 0xcf, 0x1e, 0xae, 0x49, 0x3b, 0x64, 0x1e, 0xe5, 0xac, 0xdd, + 0xcc, 0x7c, 0x45, 0x4d, 0x93, 0x3c, 0xbd, 0x53, 0xdb, 0x16, 0x81, 0xa4, 0xc5, 0x9c, 0x43, 0xd0, + 0xde, 0x67, 0x8e, 0x1b, 0xab, 0x06, 0x41, 0xa3, 0x81, 0x38, 0x51, 0xf0, 0xd3, 0xf4, 0x77, 0x70, + 0x10, 0xbc, 0x4f, 0xfc, 0x2e, 0xeb, 0xc7, 0x8e, 0x9b, 0x06, 0x91, 0x07, 0x6c, 0x0c, 0xa2, 0xdc, + 0xe7, 0xc1, 0x99, 0x2d, 0xe8, 0x2d, 0xb0, 0x34, 0x2a, 0x30, 0x31, 0xb7, 0xe2, 0x4b, 0x7e, 0xf5, + 0xf2, 0xe5, 0x33, 0xbc, 0x78, 0x7a, 0xf7, 0x2f, 0x5f, 0xd2, 0x63, 0xa2, 0xaf, 0x5e, 0x06, 0x3a, + 0x24, 0xcf, 0xfe, 0x13, 0xd4, 0x46, 0x3f, 0x71, 0x1c, 0x77, 0x46, 0x53, 0x26, 0xe1, 0x03, 0x13, + 0x3e, 0x38, 0x1f, 0xc8, 0x53, 0xe2, 0xdc, 0xdf, 0xaf, 0xf1, 0xc3, 0xb5, 0x77, 0x80, 0x95, 0xc8, + 0x5b, 0x8b, 0x75, 0xdf, 0x71, 0xf6, 0xc6, 0x6e, 0x18, 0x6c, 0xd7, 0x19, 0x70, 0x91, 0x5f, 0xbe, + 0x9c, 0x39, 0x6e, 0xe2, 0x27, 0xae, 0x48, 0xfc, 0x67, 0x23, 0x57, 0xf8, 0xcf, 0xc6, 0xae, 0x9e, + 0xd6, 0x87, 0xae, 0x7e, 0xea, 0x02, 0x41, 0xfc, 0x6f, 0xd3, 0x94, 0xdd, 0x79, 0xf3, 0x54, 0xac, + 0xfa, 0xdb, 0x98, 0x27, 0x8b, 0x7c, 0xe9, 0xa7, 0x9e, 0xbc, 0x29, 0x5c, 0x64, 0xff, 0xf1, 0xe8, + 0x15, 0x77, 0x5c, 0xa3, 0x72, 0xe2, 0x31, 0x23, 0x72, 0xc0, 0x1e, 0x5f, 0x83, 0x6f, 0xe0, 0x8f, + 0x5c, 0xe0, 0x64, 0x3c, 0xd3, 0x75, 0x57, 0xc1, 0x76, 0xc5, 0xf3, 0xa5, 0x08, 0x7d, 0xfb, 0xe7, + 0x9f, 0x3e, 0x5c, 0xd8, 0x2e, 0xaa, 0x36, 0x1f, 0xcf, 0x13, 0x00, 0x4b, 0x69, 0x94, 0x80, 0xbf, + 0x74, 0xd7, 0x0f, 0x9d, 0xc2, 0x20, 0x1a, 0xaf, 0x11, 0x2d, 0x73, 0x57, 0xc0, 0x19, 0xaa, 0x6e, + 0x09, 0x79, 0x47, 0xbe, 0xe4, 0x8a, 0x80, 0x13, 0xc0, 0x16, 0x69, 0xd8, 0xfe, 0xc7, 0x9f, 0xd5, + 0x3e, 0x2d, 0xfb, 0xc5, 0x36, 0x29, 0xc0, 0x56, 0x01, 0x5e, 0x2c, 0x35, 0x10, 0xad, 0xd1, 0xdd, + 0x47, 0xfc, 0xce, 0x09, 0xe7, 0x04, 0x51, 0x92, 0x63, 0xec, 0xb7, 0x54, 0xf7, 0x04, 0x3d, 0xb1, + 0x26, 0x49, 0x32, 0x96, 0x6f, 0xd9, 0x7b, 0xdc, 0xb5, 0xe9, 0xac, 0xdd, 0x76, 0x8a, 0xe6, 0x10, + 0xf3, 0x55, 0x43, 0x1a, 0x22, 0xf0, 0x6f, 0x52, 0xec, 0x91, 0x07, 0x80, 0xf6, 0x24, 0x38, 0xe2, + 0x07, 0x5a, 0xfe, 0x9e, 0xc6, 0xc0, 0xa8, 0x1f, 0x44, 0x30, 0x9a, 0x64, 0x4b, 0x71, 0xd3, 0x77, + 0x94, 0xba, 0x8b, 0x41, 0x4e, 0x57, 0x6c, 0xdd, 0x97, 0x8b, 0x02, 0x2b, 0x25, 0x74, 0x7a, 0x28, + 0x56, 0x51, 0xc6, 0xfb, 0xfd, 0xdc, 0x8d, 0x51, 0x6b, 0xc1, 0xbc, 0x17, 0xd1, 0x8a, 0x8b, 0x4d, + 0xde, 0xaf, 0x54, 0x59, 0x85, 0xda, 0xa4, 0x29, 0x0f, 0x5a, 0x40, 0x12, 0x2d, 0x0f, 0x71, 0x4d, + 0x1e, 0x04, 0x0a, 0x48, 0x2c, 0x99, 0x37, 0xef, 0x60, 0x5e, 0x71, 0x99, 0xa3, 0x34, 0xe0, 0xc5, + 0xe0, 0x5e, 0x7a, 0x44, 0xae, 0xe5, 0x5e, 0x8d, 0x6f, 0xe3, 0x8a, 0x6f, 0x53, 0xbd, 0x0c, 0xf8, + 0x01, 0xd1, 0x22, 0x01, 0x9c, 0x35, 0xf9, 0x38, 0x02, 0x45, 0x09, 0x92, 0xf3, 0x08, 0xee, 0x49, + 0x6b, 0xdc, 0x93, 0x37, 0xb8, 0x67, 0x66, 0x70, 0x4f, 0xa2, 0xb9, 0x27, 0xd7, 0xdc, 0x93, 0x54, + 0xdc, 0x23, 0x0f, 0x74, 0x81, 0x77, 0x00, 0xea, 0xff, 0x0d, 0xee, 0x01, 0xa4, 0x42, 0xc0, 0x74, + 0xb7, 0x05, 0xe5, 0x09, 0x72, 0x92, 0xbc, 0x12, 0x7b, 0x7b, 0xb0, 0x65, 0x67, 0x22, 0x21, 0x54, + 0x84, 0xf5, 0xa0, 0x0b, 0x6a, 0xa5, 0xfc, 0xbc, 0x93, 0xb2, 0xb2, 0x6f, 0xa5, 0x85, 0x3d, 0x08, + 0xb7, 0x93, 0x3e, 0xbd, 0x5a, 0x46, 0x21, 0x47, 0x63, 0xe3, 0xa9, 0x75, 0xea, 0xad, 0xb0, 0xa0, + 0xe3, 0xab, 0x87, 0x26, 0x17, 0x2b, 0xbb, 0x55, 0x4a, 0xa3, 0xc1, 0x9e, 0x18, 0x28, 0x9e, 0xe3, + 0x52, 0xc1, 0xd0, 0x06, 0x2e, 0x2f, 0xd5, 0xb4, 0x0a, 0x7b, 0x1d, 0xcd, 0xb1, 0xbb, 0x38, 0x8e, + 0xa3, 0x2a, 0x6f, 0x70, 0x5c, 0x16, 0x5c, 0xda, 0x18, 0x4a, 0xd9, 0x2e, 0x5c, 0xf0, 0x2f, 0x04, + 0x53, 0xf0, 0x17, 0x03, 0xa9, 0x2b, 0xe4, 0x4f, 0x43, 0x1b, 0x2b, 0x23, 0x83, 0x46, 0x81, 0xac, + 0x88, 0x7b, 0x99, 0xb8, 0xec, 0x2a, 0xc8, 0x3d, 0xf0, 0x46, 0x23, 0x00, 0xc3, 0x03, 0x08, 0x94, + 0x8d, 0x04, 0x22, 0xa2, 0x6b, 0x90, 0x57, 0x56, 0x0d, 0x20, 0xb6, 0xc1, 0x94, 0x64, 0x5e, 0x94, + 0xcc, 0xe2, 0x4d, 0x08, 0x5b, 0x64, 0xc8, 0x62, 0x71, 0x70, 0x49, 0x9f, 0x0d, 0xfa, 0x76, 0x23, + 0x36, 0xb7, 0x5d, 0x69, 0x8e, 0x6c, 0xbb, 0x70, 0x55, 0x0f, 0x19, 0x8c, 0x97, 0x2f, 0x54, 0x8c, + 0x5e, 0x5c, 0x4d, 0xc4, 0xcb, 0x97, 0xc8, 0xdd, 0x9b, 0x6c, 0xd9, 0xf7, 0x3c, 0x4f, 0x90, 0x9c, + 0xf6, 0x25, 0x9c, 0xbc, 0x00, 0xbc, 0xf7, 0xe5, 0x04, 0x5c, 0x8d, 0x04, 0x97, 0x42, 0x23, 0xb5, + 0x00, 0x8f, 0x02, 0x9c, 0x08, 0x20, 0x3c, 0x10, 0x19, 0x40, 0x03, 0x6f, 0xe4, 0x87, 0x8b, 0xbf, + 0xbf, 0x0f, 0x6c, 0xdb, 0x05, 0xb9, 0x13, 0xe9, 0x5b, 0x06, 0x68, 0x6b, 0x0c, 0x4f, 0x70, 0xca, + 0x52, 0xb1, 0xa0, 0x1a, 0xf8, 0x89, 0xb2, 0x03, 0xc0, 0x5d, 0x09, 0xe0, 0xde, 0xc3, 0x9a, 0xfb, + 0x24, 0x7c, 0x8d, 0xd5, 0x65, 0xb4, 0x47, 0xe7, 0x11, 0x5c, 0xa8, 0x78, 0xa1, 0xc9, 0x0c, 0x95, + 0x57, 0xd3, 0x76, 0x24, 0xca, 0xa3, 0x32, 0xc7, 0x64, 0x05, 0x5d, 0x21, 0x00, 0x66, 0xc8, 0xf0, + 0x39, 0x28, 0xd9, 0x8c, 0xc4, 0xaf, 0x7c, 0x35, 0x99, 0xfc, 0xed, 0x60, 0x99, 0x96, 0x42, 0x1c, + 0x66, 0x58, 0xfb, 0x63, 0xbb, 0x75, 0x6b, 0x9e, 0xa1, 0x60, 0xf3, 0x85, 0x1f, 0x17, 0x6d, 0x9b, + 0x5e, 0x91, 0x54, 0x9e, 0xc6, 0xbd, 0xe1, 0x73, 0xb6, 0x89, 0x73, 0x4d, 0xb9, 0x91, 0xab, 0x3f, + 0x3d, 0x70, 0xcb, 0xef, 0x11, 0x80, 0x8a, 0x31, 0x50, 0x31, 0x22, 0x5e, 0x02, 0x7e, 0x19, 0xc1, + 0x3c, 0x25, 0x45, 0x63, 0x45, 0xd1, 0x28, 0x04, 0x22, 0x24, 0x7e, 0xee, 0x92, 0x85, 0x07, 0x33, + 0x9b, 0x61, 0x29, 0x1b, 0x93, 0x8f, 0xbf, 0xfa, 0x19, 0x3d, 0xff, 0xea, 0x0b, 0x83, 0xe6, 0xf9, + 0xfd, 0x7d, 0x09, 0x06, 0xaa, 0x06, 0xe5, 0x21, 0x29, 0x08, 0xd8, 0x20, 0xd1, 0x20, 0x88, 0x41, + 0x86, 0x5c, 0xe0, 0xb8, 0xbc, 0xce, 0x06, 0x51, 0x93, 0x0d, 0x32, 0x35, 0x87, 0x50, 0x73, 0xc4, + 0x7a, 0x86, 0xa8, 0x70, 0xd3, 0x8a, 0x33, 0x66, 0x26, 0x67, 0x64, 0xae, 0x00, 0x3b, 0xe2, 0xe1, + 0x79, 0x25, 0x28, 0x12, 0x4f, 0x9e, 0x61, 0xc6, 0x6e, 0xd5, 0xa2, 0x0e, 0x31, 0x23, 0x77, 0x04, + 0x7a, 0x3b, 0x05, 0x44, 0xcc, 0xbc, 0xb2, 0x1a, 0x00, 0x1c, 0x89, 0x44, 0xf9, 0xd1, 0xb1, 0xcb, + 0xd4, 0x5d, 0x04, 0xec, 0xaa, 0xda, 0x5e, 0x45, 0x67, 0x47, 0xe3, 0xfd, 0xf3, 0x03, 0x7f, 0x8c, + 0xc0, 0x83, 0x73, 0x0d, 0x4e, 0x48, 0xf1, 0x14, 0xa6, 0x7b, 0x56, 0xe7, 0x3a, 0x60, 0xb6, 0xa7, + 0xfa, 0xd4, 0xa5, 0x95, 0xd4, 0x49, 0x22, 0xc3, 0x01, 0xd5, 0x79, 0x26, 0x90, 0x7d, 0x4a, 0xe9, + 0x18, 0x1c, 0x2c, 0x53, 0x3c, 0x4e, 0xe5, 0x6c, 0xa3, 0x40, 0x53, 0x5e, 0xc3, 0x70, 0xb7, 0x1b, + 0x76, 0x36, 0x69, 0x0a, 0x1f, 0xf5, 0x9f, 0xb0, 0x8e, 0xa8, 0x42, 0x6a, 0x8b, 0xbe, 0xb1, 0xaf, + 0x5c, 0x0a, 0xa4, 0xdb, 0xd5, 0x5b, 0x62, 0xa7, 0xd6, 0x3d, 0xe9, 0xe3, 0x02, 0x64, 0x66, 0xb4, + 0xb5, 0x51, 0x55, 0x97, 0x12, 0x15, 0xb6, 0xa3, 0x66, 0xcb, 0xd2, 0x59, 0x00, 0x9a, 0xa6, 0x29, + 0xbe, 0x0b, 0x95, 0x06, 0xea, 0x14, 0x5f, 0xb5, 0x79, 0xd4, 0xaf, 0x3c, 0xbb, 0x1c, 0x5d, 0x75, + 0xf9, 0xe7, 0x5a, 0xb1, 0x77, 0xf9, 0xe6, 0xaa, 0x0e, 0xa3, 0x43, 0x82, 0x0d, 0x31, 0x2f, 0xf3, + 0x3e, 0xa8, 0xe8, 0x8d, 0xd6, 0x5a, 0x0e, 0x09, 0xfd, 0x10, 0xad, 0x4d, 0x81, 0xfd, 0xf2, 0x73, + 0x90, 0x7d, 0x4f, 0xf6, 0x90, 0x6e, 0x02, 0x3c, 0x03, 0x41, 0x40, 0xee, 0xb4, 0xff, 0x5e, 0xd1, + 0x09, 0x5c, 0x94, 0xad, 0xfc, 0x7a, 0xdd, 0x8f, 0xdc, 0x69, 0x1a, 0x81, 0x87, 0x0b, 0x52, 0x3a, + 0x73, 0x23, 0x3f, 0x2c, 0x02, 0xbd, 0x0e, 0x98, 0x50, 0x65, 0x57, 0x4d, 0x62, 0xa2, 0x45, 0xa9, + 0xbb, 0x21, 0x66, 0x10, 0xe6, 0x6e, 0xa5, 0x0f, 0x2d, 0xe7, 0x44, 0x4d, 0xb3, 0x2d, 0x27, 0x06, + 0xbb, 0x41, 0x66, 0x54, 0x31, 0xdd, 0x2a, 0xc0, 0x64, 0x1a, 0xcb, 0x65, 0xe1, 0x4e, 0x3f, 0x71, + 0x1b, 0x1e, 0x8a, 0x39, 0x2d, 0x5a, 0x78, 0x25, 0x38, 0x2b, 0x30, 0x46, 0xa6, 0xae, 0x8e, 0xbe, + 0x40, 0x51, 0x6b, 0x4a, 0x97, 0xc7, 0xd9, 0x8f, 0x23, 0x79, 0x87, 0x0d, 0x37, 0xf5, 0xb6, 0x41, + 0x3d, 0xc5, 0x00, 0xac, 0x1d, 0x9c, 0xa9, 0x37, 0x99, 0xf1, 0x46, 0x9d, 0x8d, 0x95, 0xef, 0x44, + 0x07, 0xdb, 0xe8, 0x77, 0xf1, 0x8e, 0x60, 0x91, 0x74, 0x5d, 0xa4, 0x74, 0x5d, 0x5a, 0xa0, 0x95, + 0x57, 0x0a, 0x49, 0xea, 0x34, 0x84, 0xbf, 0xcd, 0x7a, 0x39, 0x89, 0xe4, 0xf7, 0x40, 0x8e, 0x37, + 0xa0, 0xd9, 0x26, 0xd1, 0xbc, 0x6f, 0x30, 0x56, 0xea, 0x68, 0xe3, 0xa8, 0xb6, 0x6b, 0xbb, 0x1c, + 0xa2, 0xe0, 0x38, 0xe3, 0x9d, 0xa1, 0x47, 0x8a, 0xb8, 0xd0, 0xe1, 0xc6, 0x34, 0x16, 0x53, 0xd4, + 0x43, 0x72, 0x7e, 0xc0, 0x5f, 0xff, 0x32, 0xb9, 0x72, 0x23, 0x80, 0x13, 0x5c, 0x61, 0x3f, 0xf1, + 0xf0, 0x52, 0x54, 0xd6, 0xb7, 0x5c, 0x00, 0xe4, 0x5d, 0xeb, 0x63, 0x73, 0xee, 0x32, 0x23, 0x45, + 0x87, 0x26, 0x2b, 0xcc, 0x26, 0xe5, 0x42, 0xc4, 0xde, 0x35, 0x07, 0x0a, 0xc5, 0x38, 0xc9, 0x90, + 0xad, 0xa3, 0x21, 0x46, 0xe2, 0x43, 0x89, 0xcd, 0x73, 0xb6, 0x12, 0x1b, 0xfc, 0x35, 0x91, 0xbd, + 0xcc, 0xed, 0xf2, 0x8a, 0x73, 0x60, 0xca, 0xad, 0xaa, 0x21, 0xf6, 0x43, 0x57, 0x0e, 0xf2, 0x57, + 0xda, 0x3a, 0xce, 0xb4, 0xcb, 0x0b, 0x28, 0x79, 0x36, 0xf3, 0xc4, 0x27, 0x47, 0xa9, 0xb7, 0x6b, + 0x11, 0xe1, 0x61, 0x01, 0xb2, 0x5b, 0x58, 0xb2, 0xdb, 0xa4, 0x91, 0x9e, 0x50, 0x8c, 0xbe, 0xc1, + 0x4c, 0xc5, 0x1a, 0xff, 0x2c, 0x83, 0x31, 0x3f, 0x78, 0x15, 0xbb, 0x8b, 0x60, 0x55, 0x45, 0x25, + 0x18, 0xfa, 0x54, 0xda, 0x31, 0x69, 0xc9, 0x2a, 0xaf, 0x64, 0x35, 0x23, 0xb9, 0x12, 0x28, 0xab, + 0x31, 0x88, 0x54, 0x64, 0xc8, 0x6a, 0x82, 0x41, 0xb8, 0x21, 0x7c, 0xa2, 0x14, 0x3e, 0xd9, 0xb3, + 0x80, 0xb0, 0x00, 0x3c, 0x28, 0x56, 0x58, 0x2f, 0xb6, 0xf9, 0xde, 0xb8, 0xf8, 0x38, 0xd9, 0x48, + 0x43, 0x0d, 0x8d, 0xb3, 0xe2, 0xfe, 0xc5, 0xb6, 0x1d, 0x1f, 0x7c, 0x74, 0xdc, 0xb5, 0xec, 0x03, + 0x5e, 0x83, 0xb9, 0x35, 0xd9, 0x58, 0x57, 0x02, 0xa9, 0xbb, 0x4d, 0x40, 0xce, 0x21, 0x76, 0x1e, + 0xa1, 0xef, 0xd4, 0xe5, 0x91, 0x2f, 0xb4, 0xe4, 0xdf, 0x05, 0x1b, 0xda, 0x7e, 0xe9, 0x9c, 0x5e, + 0x82, 0x81, 0xb8, 0x82, 0xc0, 0x47, 0x79, 0xa4, 0xf7, 0xe4, 0xf4, 0xd4, 0xb4, 0x83, 0x40, 0x27, + 0x4d, 0xe2, 0x1e, 0x00, 0xbe, 0xc4, 0xf2, 0x39, 0x57, 0xd6, 0xc5, 0x5d, 0x55, 0xae, 0xa9, 0x70, + 0xce, 0xed, 0xe7, 0xb6, 0x6f, 0x0f, 0x87, 0x36, 0x6d, 0xb3, 0xf8, 0xaf, 0x04, 0x77, 0x0c, 0x7e, + 0xe2, 0x44, 0x9b, 0xe2, 0x3b, 0xef, 0x77, 0x11, 0x25, 0x7d, 0xfb, 0xbf, 0x50, 0x5a, 0x14, 0xfd, + 0x20, 0x62, 0x52, 0x5f, 0x71, 0xb4, 0x74, 0xac, 0xbb, 0x76, 0x97, 0x4f, 0xd1, 0x2f, 0xa5, 0x66, + 0x29, 0xe9, 0xc2, 0xeb, 0x49, 0x1e, 0xd3, 0x4c, 0x54, 0x4e, 0x5f, 0x87, 0x9d, 0x30, 0xfc, 0xc4, + 0x0e, 0x25, 0x62, 0x54, 0x1d, 0x75, 0xaa, 0x12, 0xed, 0x3a, 0x76, 0x68, 0x12, 0xed, 0x42, 0x02, + 0x53, 0xc9, 0x0f, 0xfb, 0x6a, 0xba, 0xc4, 0x2c, 0xb5, 0x40, 0x8e, 0x2a, 0x23, 0xcb, 0x0c, 0xe3, + 0xca, 0xf2, 0x09, 0xed, 0x87, 0xf4, 0xa3, 0x42, 0xed, 0x47, 0xad, 0x3a, 0x81, 0xef, 0x50, 0x3f, + 0xda, 0x7b, 0x72, 0x37, 0xd5, 0x7c, 0x40, 0x83, 0x75, 0xf5, 0xb4, 0x02, 0x57, 0x05, 0xc3, 0x60, + 0x06, 0xe4, 0x66, 0x31, 0x15, 0x6a, 0x80, 0xc8, 0x94, 0x0d, 0xb2, 0x6e, 0xc2, 0xbd, 0x0b, 0xe2, + 0xf3, 0xd4, 0x5f, 0xba, 0x73, 0xb8, 0xce, 0xfc, 0x85, 0x7b, 0x0d, 0xd7, 0x8d, 0x7f, 0xe7, 0xde, + 0xc0, 0x75, 0xed, 0xcf, 0xdd, 0xf7, 0xc1, 0xdf, 0x59, 0xbe, 0xf4, 0xe6, 0xb1, 0x00, 0x3f, 0xa0, + 0xbf, 0x19, 0xdc, 0x39, 0xc3, 0x7d, 0xc7, 0x7d, 0x5b, 0x6b, 0x5d, 0x0f, 0xe6, 0xd4, 0x3a, 0x0d, + 0x0e, 0x4f, 0x5e, 0x5d, 0xbb, 0xbf, 0xe3, 0xe5, 0xc6, 0xbd, 0x0d, 0xa6, 0xa7, 0xc1, 0xd7, 0xc7, + 0x27, 0xe7, 0xe3, 0x43, 0x7f, 0x7c, 0xe2, 0x7e, 0x40, 0xd1, 0xdd, 0xce, 0xf2, 0x5b, 0xff, 0xa2, + 0x08, 0x24, 0x59, 0x5f, 0x93, 0x3c, 0xf6, 0xef, 0xdc, 0xb9, 0x33, 0xb9, 0xc0, 0x2f, 0x90, 0x6f, + 0x94, 0xac, 0xba, 0x23, 0xf8, 0x0f, 0x35, 0x4b, 0x56, 0x7f, 0x13, 0x5c, 0x60, 0xde, 0x91, 0xde, + 0xa2, 0x6e, 0xed, 0xeb, 0xf7, 0x84, 0x85, 0x52, 0xb0, 0x5f, 0xb8, 0x38, 0xfd, 0xa7, 0xc6, 0xf4, + 0x53, 0xf7, 0x77, 0x67, 0xf2, 0x09, 0xed, 0x4e, 0xfc, 0x41, 0x1e, 0x21, 0x3c, 0x1f, 0xd1, 0x3f, + 0xdb, 0x95, 0xad, 0xbf, 0x00, 0x6a, 0x69, 0x46, 0xea, 0x09, 0x02, 0xd3, 0xc7, 0x24, 0x08, 0x0f, + 0x46, 0x13, 0x7e, 0x7a, 0x33, 0xe1, 0x10, 0x1b, 0xeb, 0xb6, 0x1c, 0xda, 0xf2, 0xd3, 0xeb, 0x49, + 0x0e, 0x6d, 0x5b, 0x6c, 0x00, 0x7b, 0xf5, 0xea, 0x7a, 0x0f, 0x34, 0x01, 0x48, 0x1a, 0x6c, 0x1a, + 0x9c, 0x73, 0xbc, 0x70, 0xf0, 0x29, 0xf2, 0xc1, 0x7b, 0x08, 0x06, 0xf8, 0xe0, 0x2d, 0x90, 0xdf, + 0x56, 0xeb, 0xa1, 0xfe, 0x03, 0x47, 0x5f, 0x9c, 0x05, 0x23, 0xf8, 0x7b, 0x7a, 0xf7, 0xf2, 0x65, + 0x44, 0xb7, 0xd1, 0xe9, 0xfc, 0xfe, 0xfe, 0x19, 0xbc, 0xc9, 0xb1, 0x8d, 0x9f, 0xce, 0xf5, 0xe4, + 0xf1, 0x79, 0xf4, 0xea, 0x6e, 0x4f, 0xf8, 0x1c, 0xfe, 0xe6, 0x13, 0xe0, 0x23, 0x54, 0xd8, 0x74, + 0x34, 0xde, 0x7f, 0x73, 0x79, 0xf8, 0x0a, 0x8c, 0x00, 0x5d, 0xf6, 0xc6, 0xfa, 0x66, 0x5f, 0xdf, + 0x1c, 0x5c, 0x51, 0xaa, 0x77, 0x9b, 0x82, 0x36, 0x59, 0x00, 0x7b, 0x4d, 0x91, 0xb3, 0x96, 0xfc, + 0xf6, 0x42, 0xfc, 0xb2, 0x98, 0x82, 0x5a, 0x9a, 0x7c, 0x90, 0xea, 0x07, 0xec, 0x8c, 0x89, 0x9a, + 0x8f, 0x29, 0xbc, 0x45, 0x55, 0xe6, 0x82, 0xe4, 0x87, 0xf4, 0x77, 0x55, 0x38, 0x1f, 0x4d, 0x44, + 0x41, 0xc4, 0xe1, 0x1e, 0x9e, 0xc0, 0x7f, 0x9d, 0x02, 0xad, 0x97, 0xa5, 0x26, 0xd2, 0x7b, 0x44, + 0x54, 0xe3, 0x87, 0x2b, 0xc4, 0x6f, 0xc1, 0x18, 0x46, 0xe2, 0xd9, 0xed, 0x27, 0xae, 0x51, 0x3f, + 0xa6, 0x7f, 0x76, 0xd9, 0xde, 0x98, 0x93, 0xf2, 0x4f, 0x60, 0xae, 0xf6, 0xf6, 0x0f, 0xdd, 0x30, + 0xc8, 0xe0, 0x82, 0xa4, 0x13, 0x60, 0x7b, 0x6e, 0xf7, 0xec, 0xf5, 0xad, 0xf5, 0x6d, 0x1a, 0xb1, + 0xd8, 0xae, 0xc1, 0xac, 0x57, 0x46, 0x23, 0x7b, 0x5e, 0x9e, 0xd6, 0xfb, 0xe6, 0x52, 0x68, 0xcf, + 0xbf, 0xc5, 0x4f, 0xed, 0x02, 0x5b, 0x7e, 0x65, 0xa7, 0x1b, 0xbf, 0x03, 0x11, 0x42, 0x68, 0x03, + 0x7b, 0x15, 0x85, 0x61, 0xcc, 0xf5, 0xcc, 0x17, 0xf0, 0x0e, 0x7c, 0xa9, 0x99, 0x1b, 0x3a, 0x85, + 0x2c, 0xbb, 0xea, 0xe7, 0xa0, 0x82, 0xa0, 0xb7, 0x3d, 0xb6, 0xfd, 0x28, 0xf8, 0x30, 0x99, 0x02, + 0x8b, 0x7d, 0x9a, 0x50, 0xcb, 0x3e, 0xb6, 0xd0, 0xd7, 0xb6, 0xfd, 0x0f, 0x8e, 0xf9, 0xe2, 0x00, + 0x5f, 0x50, 0xa1, 0x22, 0xbc, 0x28, 0xa4, 0xde, 0xad, 0x78, 0x15, 0xed, 0x4b, 0x29, 0xac, 0xcc, + 0x41, 0x73, 0x54, 0x3e, 0x82, 0x1d, 0x22, 0x73, 0x53, 0x69, 0x42, 0x9a, 0x85, 0x4b, 0xc6, 0x00, + 0x36, 0x04, 0xef, 0x60, 0x84, 0x21, 0x2d, 0x3a, 0x52, 0x19, 0x1a, 0x49, 0x5e, 0x85, 0x6a, 0xc0, + 0x7e, 0x68, 0x09, 0x81, 0xd9, 0xf8, 0xb3, 0x20, 0x60, 0xe0, 0xca, 0x07, 0x62, 0x32, 0x0e, 0x82, + 0x64, 0x90, 0x9f, 0x67, 0x92, 0x5a, 0xcc, 0xf1, 0xd5, 0x1d, 0xd8, 0x0c, 0xf0, 0x13, 0xc0, 0x19, + 0x13, 0x05, 0x43, 0x37, 0x5e, 0x27, 0x31, 0xc6, 0x94, 0x43, 0x93, 0x59, 0xd6, 0xee, 0x71, 0xfa, + 0x2d, 0x0e, 0xcf, 0x2a, 0x48, 0x0d, 0x1e, 0x05, 0x93, 0x4c, 0xb3, 0x2b, 0xdd, 0xad, 0x35, 0x65, + 0xd6, 0x4c, 0xc2, 0x9b, 0x65, 0x26, 0x9d, 0xda, 0x56, 0x56, 0x64, 0x94, 0xb0, 0x61, 0x9c, 0xc8, + 0xce, 0xb3, 0x73, 0x51, 0x65, 0x58, 0x9e, 0x53, 0x86, 0xc5, 0xcb, 0xc5, 0x7b, 0x71, 0xc3, 0xd3, + 0xd7, 0x80, 0xfd, 0xbe, 0x83, 0x3f, 0x5c, 0x76, 0x21, 0x7e, 0x00, 0xb2, 0xec, 0x1f, 0x1d, 0xb9, + 0xea, 0x7f, 0x46, 0x2b, 0x81, 0x67, 0x1e, 0x5f, 0x68, 0xf1, 0x00, 0x34, 0x1f, 0xc0, 0x12, 0x60, + 0x59, 0x79, 0x73, 0x05, 0x47, 0x6d, 0x1a, 0xa2, 0x54, 0xf3, 0xed, 0xb0, 0xef, 0x39, 0xc3, 0x85, + 0x6b, 0xbf, 0x18, 0xbf, 0x18, 0x43, 0xa7, 0x49, 0x69, 0x8f, 0x31, 0x47, 0x84, 0xb9, 0x53, 0xe4, + 0xa0, 0xa1, 0xb7, 0xdd, 0x2f, 0x86, 0x0b, 0x47, 0x5b, 0xed, 0x92, 0xda, 0xdc, 0x1d, 0x1f, 0x3b, + 0x7a, 0x77, 0x20, 0xb2, 0x39, 0x88, 0x6c, 0x02, 0x22, 0xcb, 0x4c, 0x06, 0xa8, 0x43, 0xad, 0x02, + 0xc4, 0x52, 0xec, 0xf6, 0xfa, 0xfc, 0xf4, 0x74, 0x7c, 0x7c, 0x9f, 0x9f, 0x9e, 0x9e, 0xdc, 0x27, + 0x88, 0x88, 0x0f, 0xe4, 0x84, 0xf4, 0x71, 0x66, 0x2f, 0xa3, 0xb4, 0xc2, 0xe0, 0xd8, 0xd8, 0xac, + 0xe4, 0x57, 0x6d, 0x57, 0x81, 0xa5, 0x80, 0x87, 0xca, 0xfc, 0x95, 0xc1, 0x4a, 0x14, 0x72, 0xe6, + 0x92, 0xe6, 0x09, 0xb8, 0xac, 0xe0, 0xe9, 0xe5, 0xd5, 0x2c, 0x35, 0x45, 0x8b, 0x1e, 0x58, 0xe9, + 0x7f, 0x85, 0x2a, 0x88, 0x7a, 0x5b, 0xd2, 0x90, 0x3a, 0xd9, 0xe8, 0x44, 0xc8, 0x14, 0x00, 0xcc, + 0xad, 0x43, 0xff, 0x7c, 0x52, 0x06, 0xb5, 0xa8, 0xe7, 0x5f, 0xe3, 0x67, 0x48, 0x20, 0x80, 0xf6, + 0x3e, 0x84, 0xb0, 0xdb, 0x1b, 0xd2, 0x3c, 0x2c, 0xfc, 0x1e, 0x0b, 0x5b, 0x60, 0xaa, 0xf8, 0x8e, + 0xbc, 0x23, 0x0d, 0x2d, 0xf3, 0xc8, 0xdb, 0xfd, 0xb0, 0x12, 0x22, 0x5f, 0xc2, 0x8e, 0xff, 0xcf, + 0x86, 0xe1, 0xf7, 0x54, 0x81, 0xbd, 0x84, 0xa9, 0xc1, 0x07, 0x6e, 0xbc, 0x7e, 0x9b, 0xb0, 0x69, + 0x8c, 0x89, 0x85, 0x71, 0x69, 0x36, 0x12, 0x32, 0x1b, 0x26, 0xb2, 0x6b, 0x4e, 0x13, 0x6d, 0x4b, + 0x2e, 0x86, 0x0e, 0x13, 0xca, 0xc5, 0xf9, 0x1d, 0x5b, 0xc5, 0xa0, 0x11, 0x7c, 0xe9, 0x3c, 0x51, + 0x13, 0xde, 0x61, 0x93, 0x81, 0x9d, 0x50, 0x1d, 0x79, 0x93, 0xcb, 0xae, 0xe8, 0x86, 0xf2, 0x0b, + 0xea, 0x6e, 0xa2, 0xb4, 0x4a, 0xa2, 0xb4, 0x0a, 0x25, 0xdf, 0x7d, 0x16, 0xd8, 0xe0, 0x7d, 0x03, + 0xb1, 0x28, 0x12, 0x19, 0xea, 0x24, 0xbd, 0x7c, 0x6b, 0x6a, 0x16, 0x80, 0xa3, 0xd9, 0xf9, 0x76, + 0x80, 0x40, 0x51, 0x77, 0xba, 0x31, 0xbb, 0x13, 0x94, 0x38, 0x00, 0xd1, 0x3a, 0x04, 0x96, 0x8d, + 0xe4, 0xbc, 0xf9, 0x6d, 0x6e, 0x17, 0xfa, 0xb8, 0x0a, 0x83, 0x8b, 0xef, 0x30, 0xd2, 0xb8, 0xe4, + 0x57, 0x2a, 0xb4, 0x60, 0x94, 0xa4, 0xec, 0x8a, 0x88, 0x29, 0x3b, 0xdf, 0xa2, 0x31, 0x03, 0xf2, + 0x46, 0xf2, 0x60, 0x0f, 0xfc, 0x65, 0x4f, 0xef, 0x1f, 0x5d, 0xe6, 0xbc, 0xf0, 0x5e, 0x6c, 0xb3, + 0xe2, 0x23, 0x34, 0x53, 0x7d, 0x0c, 0xc4, 0x33, 0x38, 0x6f, 0xca, 0xaf, 0xc1, 0x08, 0x54, 0xf3, + 0xc6, 0x06, 0x8b, 0x12, 0x92, 0x1f, 0xf0, 0xfc, 0xd4, 0x67, 0x10, 0x9d, 0x8e, 0x5f, 0xed, 0x23, + 0x80, 0x4e, 0xe7, 0xaf, 0xfc, 0x78, 0xa1, 0xd3, 0xf5, 0x33, 0x8e, 0x73, 0x41, 0x8f, 0x96, 0x0c, + 0x80, 0xbf, 0xab, 0x88, 0x3f, 0x22, 0x74, 0x21, 0x7e, 0x45, 0xe0, 0xb6, 0x92, 0x8a, 0x10, 0xeb, + 0x6c, 0x2f, 0xf3, 0x2b, 0x7f, 0xab, 0x57, 0xfd, 0x8d, 0xd2, 0xaf, 0x89, 0x2b, 0xd7, 0xf8, 0x0d, + 0x34, 0x3b, 0x73, 0x67, 0x62, 0xb5, 0x62, 0x49, 0xf8, 0x1b, 0x04, 0x17, 0x1f, 0x91, 0x1e, 0xd6, + 0xe0, 0x5f, 0x16, 0x86, 0x50, 0x96, 0xae, 0x89, 0x42, 0xf4, 0x98, 0x39, 0x47, 0x6b, 0x10, 0x5a, + 0x5f, 0x61, 0x86, 0xf6, 0x2b, 0x6b, 0xf0, 0x83, 0x65, 0xbf, 0x96, 0xdf, 0xe6, 0x0d, 0x2e, 0x90, + 0x30, 0x56, 0x8b, 0x4d, 0x3e, 0x56, 0x0b, 0xcc, 0xe7, 0x4f, 0x59, 0x61, 0x6b, 0x23, 0xe7, 0xcd, + 0x19, 0xd8, 0xf2, 0x47, 0xae, 0x54, 0x14, 0x85, 0xe9, 0xa4, 0x13, 0xe3, 0x3f, 0x40, 0x26, 0x03, + 0x95, 0x2a, 0xf4, 0xd8, 0x01, 0x5c, 0xfe, 0x25, 0xdb, 0x37, 0xe0, 0xa9, 0x91, 0x08, 0x00, 0x2b, + 0xdf, 0xa0, 0x2c, 0x83, 0xdd, 0xac, 0x0e, 0xc6, 0x6c, 0xcb, 0xb2, 0x3d, 0x79, 0x6e, 0xd8, 0x2f, + 0x23, 0x23, 0x4b, 0x85, 0x64, 0xb0, 0x1a, 0x90, 0x13, 0xd4, 0x9a, 0x54, 0xd8, 0x20, 0x16, 0x6e, + 0x76, 0xe5, 0x54, 0x27, 0x51, 0xcf, 0xf4, 0x49, 0x54, 0x76, 0x7f, 0x2f, 0x8f, 0x28, 0xa3, 0x8c, + 0xae, 0xe0, 0xe5, 0x9f, 0x53, 0x90, 0x88, 0xd0, 0xfb, 0xe0, 0x42, 0x95, 0x59, 0x35, 0x5e, 0x69, + 0xee, 0xd6, 0x2c, 0xbc, 0x39, 0x0b, 0x77, 0xce, 0x01, 0x91, 0x38, 0x66, 0x6f, 0xec, 0x14, 0xfd, + 0x0c, 0x6d, 0xd5, 0x47, 0xbf, 0x9a, 0x18, 0xc3, 0xb2, 0x1c, 0x9b, 0xf1, 0xf5, 0x47, 0xc7, 0x08, + 0xc9, 0x94, 0xab, 0x81, 0xfb, 0x35, 0xc4, 0x49, 0x87, 0x5d, 0x10, 0xc6, 0xab, 0xb3, 0x25, 0x1b, + 0xb0, 0xb1, 0xcf, 0x0f, 0xc0, 0x8f, 0x28, 0x2b, 0xb0, 0x9c, 0x32, 0x11, 0xae, 0xe9, 0x47, 0x27, + 0x75, 0x2d, 0x49, 0x0f, 0xa3, 0x6b, 0xa0, 0xa7, 0xf0, 0xa8, 0x28, 0xc7, 0x2b, 0x4b, 0xa3, 0x03, + 0x5b, 0x7e, 0xe5, 0x6a, 0xbb, 0xad, 0x57, 0xba, 0xc8, 0x38, 0x00, 0x78, 0x47, 0xa3, 0x3d, 0x7b, + 0x95, 0x75, 0x74, 0xba, 0x88, 0x56, 0xa0, 0xa3, 0xbf, 0x57, 0x20, 0x07, 0xb6, 0xfc, 0x04, 0x1a, + 0x3b, 0x52, 0xd9, 0x0e, 0x26, 0x2c, 0x29, 0xc9, 0x2b, 0x2b, 0x5b, 0x6c, 0x37, 0xaf, 0x4e, 0x28, + 0x5b, 0x20, 0x62, 0x75, 0x3c, 0xc0, 0x18, 0x77, 0x0e, 0xa5, 0x1f, 0x1a, 0x01, 0xd1, 0x8f, 0xc9, + 0x0d, 0x54, 0xcc, 0x05, 0x26, 0x49, 0xd4, 0xb2, 0x62, 0xb1, 0x9e, 0x3e, 0xda, 0x85, 0x81, 0xa8, + 0x7b, 0x76, 0xfd, 0x5d, 0xb0, 0x8d, 0x99, 0xfc, 0x16, 0x8e, 0xca, 0xb7, 0xed, 0x97, 0x5d, 0x58, + 0x6a, 0x77, 0xe2, 0x31, 0x03, 0x03, 0x37, 0x22, 0x0c, 0xd6, 0xb3, 0x78, 0x6e, 0xe5, 0x59, 0x35, + 0x6a, 0x7f, 0x9c, 0x5a, 0x4f, 0xd0, 0xdf, 0xc6, 0xa9, 0x1e, 0x9d, 0xcf, 0x69, 0x62, 0xa8, 0x8f, + 0x8c, 0xc1, 0xbf, 0x6c, 0xf7, 0x48, 0xf9, 0x4a, 0x5c, 0x63, 0x8c, 0xee, 0x02, 0x74, 0x74, 0x92, + 0xa7, 0xa3, 0x78, 0xcc, 0x9d, 0x82, 0x53, 0x94, 0xa9, 0x49, 0x54, 0x95, 0x56, 0x60, 0x53, 0x99, + 0x96, 0xc1, 0x85, 0x65, 0x92, 0x80, 0xec, 0x61, 0x70, 0x80, 0xe7, 0xd4, 0x64, 0x13, 0x83, 0xd1, + 0x44, 0xb3, 0xde, 0x0e, 0x5c, 0x67, 0x4d, 0x5c, 0xeb, 0xb9, 0x30, 0x09, 0x61, 0xfa, 0x2c, 0x94, + 0x08, 0xca, 0xd1, 0x49, 0xbc, 0xbf, 0xef, 0xf3, 0x6e, 0x88, 0x1a, 0xf9, 0x4f, 0x5e, 0x65, 0x54, + 0x45, 0x50, 0x95, 0xbd, 0x50, 0x38, 0x12, 0x57, 0x39, 0xfa, 0x08, 0x53, 0x9b, 0x31, 0xb0, 0xa4, + 0xc6, 0x4b, 0x8c, 0x06, 0xb2, 0x89, 0xa7, 0xfc, 0x92, 0x5d, 0x75, 0xae, 0xea, 0xb2, 0xbd, 0x3d, + 0x97, 0x9d, 0x05, 0x79, 0xe5, 0x45, 0xc2, 0xbe, 0x01, 0xfa, 0x8e, 0x01, 0xf4, 0x09, 0x3e, 0x10, + 0x1f, 0x0c, 0x25, 0xf9, 0xbb, 0x2f, 0x5f, 0x02, 0x6e, 0x67, 0xa9, 0x80, 0xe0, 0x44, 0x60, 0xb4, + 0x4a, 0xd8, 0x2f, 0x44, 0xfd, 0xc0, 0xa6, 0xce, 0x0e, 0x99, 0x43, 0xc3, 0xb3, 0x8e, 0x84, 0xff, + 0x0a, 0x11, 0x87, 0xc5, 0x5f, 0x36, 0x55, 0x50, 0x34, 0x37, 0x55, 0x3c, 0x34, 0x6a, 0x93, 0xab, + 0x41, 0x11, 0x1d, 0xe7, 0x56, 0x07, 0x76, 0x94, 0x10, 0x6d, 0xa7, 0x7e, 0x75, 0x8d, 0x19, 0x16, + 0xfd, 0x34, 0x28, 0xa8, 0x4e, 0x14, 0x6c, 0x3c, 0xbc, 0xe9, 0xde, 0xbf, 0x6e, 0x97, 0x3f, 0xd4, + 0x81, 0x78, 0xc4, 0x1f, 0x63, 0x8b, 0xc1, 0xcb, 0x07, 0xdf, 0x30, 0xf4, 0x50, 0x8c, 0x35, 0xdf, + 0xc2, 0x32, 0x73, 0x78, 0x81, 0x9e, 0x1f, 0xfe, 0xb2, 0x90, 0x6d, 0x78, 0xf6, 0x94, 0x58, 0x7a, + 0x2c, 0x64, 0x8a, 0xc9, 0x1f, 0x02, 0x4e, 0x52, 0xb3, 0x09, 0x5b, 0xa8, 0x8f, 0xfa, 0x76, 0xc0, + 0x85, 0xbf, 0xac, 0x64, 0x40, 0xa5, 0x6b, 0x8f, 0xd1, 0x46, 0x71, 0x0f, 0x35, 0x30, 0x00, 0xa5, + 0x8e, 0x0b, 0xcb, 0x43, 0x25, 0x23, 0x4b, 0x5e, 0xfb, 0x10, 0xdf, 0xf1, 0xc0, 0xb3, 0x48, 0xef, + 0xe4, 0xc1, 0xb1, 0x48, 0xbf, 0x8d, 0xe3, 0xbe, 0x2c, 0xfb, 0x77, 0x55, 0xd9, 0xb1, 0x6b, 0xe9, + 0x5a, 0x56, 0x5b, 0xb2, 0x71, 0x12, 0x3c, 0x1b, 0x95, 0x7e, 0xb6, 0xbd, 0x06, 0x15, 0x48, 0x52, + 0xdb, 0x4f, 0xd8, 0x75, 0xb4, 0x60, 0x30, 0x87, 0x87, 0xbf, 0x5b, 0xbd, 0x01, 0x67, 0xf9, 0xfe, + 0xbe, 0x6a, 0x03, 0x82, 0xa7, 0xef, 0x55, 0xbb, 0x73, 0x6e, 0xbf, 0x05, 0x56, 0x00, 0x19, 0x5e, + 0xad, 0x85, 0xf5, 0xdf, 0xff, 0xd7, 0x12, 0x98, 0x41, 0x63, 0xf9, 0x7f, 0xff, 0xbf, 0x34, 0x12, + 0x10, 0x71, 0x5f, 0x2c, 0xa3, 0xcc, 0x9a, 0x47, 0x3c, 0x0e, 0x2d, 0xb8, 0x29, 0xbf, 0x61, 0xd0, + 0x76, 0xb5, 0x92, 0xd1, 0x32, 0x65, 0x89, 0xdb, 0x03, 0xcc, 0x8b, 0x8c, 0x63, 0x4d, 0xc3, 0xa5, + 0xc6, 0xc8, 0x95, 0x74, 0xbb, 0x9e, 0xe5, 0x2a, 0x51, 0x4c, 0xf0, 0x67, 0x78, 0x18, 0x28, 0x82, + 0x9c, 0x32, 0x34, 0x13, 0x01, 0xa1, 0xad, 0x44, 0xf3, 0xa4, 0x22, 0xac, 0xaa, 0xec, 0xa3, 0x10, + 0x62, 0x05, 0xb8, 0xe0, 0x21, 0xa5, 0x08, 0x40, 0xdb, 0x51, 0x36, 0x9e, 0xa8, 0x46, 0x71, 0xad, + 0x22, 0xe4, 0xfd, 0xbd, 0xe6, 0x15, 0xb3, 0xd5, 0xd9, 0xc2, 0x52, 0x63, 0xe9, 0x3f, 0x17, 0x22, + 0x10, 0x9e, 0x8c, 0x4f, 0x7f, 0x14, 0x21, 0x2f, 0x60, 0x9a, 0x0c, 0xa4, 0xf6, 0x19, 0x40, 0x8d, + 0xd1, 0xec, 0x3f, 0x11, 0x60, 0x50, 0x96, 0x7d, 0x07, 0xa6, 0xa2, 0x6a, 0x6e, 0x9a, 0x08, 0x4d, + 0xfa, 0xcb, 0x97, 0x23, 0xba, 0xa7, 0xf3, 0x0e, 0x25, 0xef, 0x0e, 0x12, 0xba, 0x21, 0x02, 0xea, + 0x67, 0x3b, 0x6c, 0xa7, 0x54, 0x7f, 0xbc, 0x3c, 0x19, 0x25, 0xd5, 0xfa, 0x77, 0x99, 0x57, 0xbf, + 0xbf, 0xa7, 0x4a, 0x38, 0xc4, 0x0f, 0x30, 0x4f, 0xd9, 0xc8, 0x54, 0xc5, 0x4e, 0x5e, 0xe7, 0x87, + 0xbe, 0x5d, 0xff, 0x65, 0x0f, 0x34, 0xd6, 0xa0, 0x0c, 0x77, 0x59, 0xf2, 0xb6, 0x81, 0x6d, 0x8c, + 0x76, 0xf3, 0x86, 0xf9, 0xc0, 0x21, 0xa4, 0x7a, 0x30, 0x55, 0x12, 0x80, 0x17, 0x02, 0x28, 0xa3, + 0x84, 0xd0, 0xb6, 0x4b, 0x94, 0x9a, 0x9b, 0x4c, 0x1e, 0x01, 0x2f, 0xd6, 0xc7, 0xa8, 0x09, 0xe4, + 0xa2, 0xa0, 0xee, 0x40, 0x2f, 0x25, 0x45, 0xf9, 0x9b, 0x85, 0x6d, 0x05, 0xa5, 0x7f, 0xc5, 0x10, + 0x64, 0x13, 0x58, 0xac, 0x2d, 0x54, 0x30, 0xfe, 0xc1, 0xd1, 0x62, 0xbd, 0x6b, 0x64, 0x5b, 0x1c, + 0xeb, 0xdf, 0x04, 0x80, 0x79, 0xa2, 0x13, 0x2e, 0x49, 0x6f, 0x45, 0x43, 0xfa, 0x82, 0x68, 0xce, + 0x53, 0xd9, 0x38, 0x31, 0x4a, 0x48, 0x20, 0x56, 0x31, 0xfb, 0xe3, 0x79, 0x52, 0x5e, 0x73, 0x45, + 0x3e, 0x52, 0x3a, 0xc4, 0xc2, 0x72, 0xb2, 0xf2, 0xdb, 0xf8, 0x67, 0x1f, 0x5d, 0x43, 0x67, 0x3c, + 0xbc, 0x17, 0x0a, 0xb2, 0x94, 0x9e, 0x56, 0x2b, 0xa9, 0xb8, 0xab, 0xd0, 0x87, 0xd1, 0x0f, 0xd4, + 0xa2, 0xd6, 0x04, 0x74, 0xc7, 0x86, 0xcd, 0xb0, 0xcb, 0x93, 0x95, 0xcb, 0x28, 0x65, 0x89, 0xb3, + 0xad, 0xef, 0x53, 0xbf, 0x7c, 0xda, 0x4e, 0x65, 0x72, 0xb1, 0xde, 0xd3, 0xfe, 0x13, 0x9f, 0x5d, + 0xd8, 0x93, 0x3a, 0xca, 0xaa, 0xe4, 0x7f, 0xab, 0xe6, 0xdb, 0xd9, 0x8d, 0xc6, 0xaa, 0xec, 0xa9, + 0xa3, 0x32, 0xc9, 0xac, 0x13, 0xc9, 0x1f, 0x53, 0x35, 0x56, 0x3b, 0x93, 0x2d, 0x3e, 0x5b, 0xb8, + 0x04, 0xfe, 0x4c, 0x55, 0x47, 0xc6, 0x8a, 0x46, 0x11, 0xd3, 0x84, 0xe9, 0x3a, 0x32, 0xfb, 0x83, + 0x51, 0x32, 0x66, 0xe9, 0xc2, 0xf9, 0x50, 0x55, 0x87, 0xd9, 0x4f, 0x39, 0x81, 0x69, 0xe0, 0x09, + 0x3f, 0x14, 0x78, 0x3c, 0x76, 0xca, 0x44, 0x9f, 0x59, 0x8b, 0x5b, 0x7d, 0x3d, 0xd9, 0x59, 0x8c, + 0xab, 0x02, 0x44, 0x7e, 0xae, 0x8a, 0x2d, 0x64, 0x39, 0xa3, 0x79, 0x5c, 0xe6, 0x3e, 0x1b, 0x39, + 0x7e, 0xad, 0xc5, 0x28, 0x97, 0xf9, 0xdc, 0xb9, 0xda, 0xb6, 0x70, 0xeb, 0xe7, 0xeb, 0x58, 0x48, + 0x83, 0x15, 0xaa, 0x54, 0xc2, 0xb0, 0x6b, 0x49, 0xa7, 0xc9, 0x30, 0xcd, 0x2f, 0x35, 0x9e, 0xc4, + 0x31, 0x1d, 0x27, 0xdb, 0xe0, 0x76, 0x48, 0x66, 0x55, 0xe4, 0x57, 0x94, 0x2d, 0x8d, 0xef, 0x4c, + 0xaf, 0xe4, 0xdd, 0xa4, 0x51, 0xce, 0x29, 0x2b, 0xad, 0xeb, 0xa1, 0xd5, 0xb9, 0x9b, 0x8d, 0x8d, + 0x10, 0xf3, 0xae, 0x23, 0xf9, 0xdd, 0xd5, 0xac, 0x02, 0xce, 0xa0, 0x77, 0x39, 0x77, 0xe8, 0xf1, + 0x5b, 0x3e, 0x7b, 0x2d, 0x53, 0x05, 0x98, 0x58, 0x5d, 0xdf, 0xd9, 0x4f, 0x9b, 0xaa, 0x55, 0x89, + 0xda, 0xc0, 0x51, 0xe3, 0xc3, 0x0f, 0xe7, 0x33, 0xba, 0x69, 0x57, 0xed, 0x76, 0xfb, 0xf8, 0x3f, + 0xef, 0xaa, 0xff, 0x68, 0xa4, 0xd8, 0x6c, 0x12, 0x4b, 0x7b, 0x8f, 0x6a, 0x57, 0xde, 0x00, 0xf7, + 0x3b, 0xe8, 0x0c, 0xa0, 0x5f, 0x0b, 0x7e, 0x70, 0x5e, 0x23, 0xa7, 0x71, 0xba, 0xf6, 0x44, 0x3d, + 0xa8, 0x8f, 0x05, 0x93, 0xae, 0xe3, 0x40, 0x55, 0x5b, 0xa5, 0xcf, 0xf3, 0x32, 0x43, 0xff, 0xed, + 0x3e, 0xc4, 0x9b, 0xe8, 0x7a, 0x26, 0x56, 0x56, 0x3b, 0x65, 0x26, 0xac, 0x7a, 0xbb, 0x8f, 0x84, + 0xd4, 0x2c, 0xd2, 0xcf, 0xbe, 0xbb, 0x7b, 0x8d, 0xe6, 0x18, 0x13, 0x61, 0xed, 0x02, 0x88, 0xe4, + 0x41, 0xf0, 0x26, 0x46, 0xe5, 0x75, 0xee, 0x54, 0x11, 0x56, 0x95, 0xcf, 0x40, 0x7f, 0xa6, 0xe1, + 0x1a, 0xcb, 0x14, 0x69, 0x72, 0x6e, 0xe3, 0xcf, 0x6f, 0x81, 0x53, 0x48, 0x4e, 0x57, 0xd1, 0x10, + 0xa5, 0xc6, 0xaf, 0x32, 0x74, 0x6d, 0x8c, 0x9c, 0xd9, 0x1d, 0x14, 0xe8, 0x3a, 0xb8, 0x35, 0xcd, + 0x90, 0x0c, 0x28, 0x93, 0xd3, 0x60, 0xff, 0xe8, 0xe8, 0x3c, 0xf1, 0xe1, 0xaf, 0x81, 0xe1, 0x72, + 0xc3, 0xaa, 0xa1, 0x1b, 0xb0, 0x2f, 0x80, 0x49, 0xed, 0xa9, 0x0b, 0x30, 0xbd, 0x68, 0x62, 0x2e, + 0xaa, 0xab, 0x5f, 0xfe, 0xac, 0x3d, 0x7e, 0x34, 0x39, 0xeb, 0x15, 0x29, 0x49, 0x47, 0x40, 0xd4, + 0x34, 0xf1, 0x49, 0x47, 0xc1, 0x1a, 0xff, 0x72, 0x9b, 0x5c, 0x72, 0x39, 0x84, 0xac, 0x79, 0x77, + 0xe0, 0xa7, 0xed, 0x7f, 0x57, 0xe0, 0x65, 0x22, 0xaf, 0x7d, 0x80, 0xf4, 0xc5, 0x42, 0xd2, 0x75, + 0x26, 0xa5, 0x4d, 0x9a, 0x49, 0xcc, 0x3f, 0x21, 0x1d, 0x0f, 0x8b, 0x45, 0xad, 0x6c, 0xe0, 0x8b, + 0xb7, 0x51, 0x2f, 0x42, 0x48, 0x3a, 0x2b, 0x26, 0xf4, 0xb6, 0xd8, 0xd3, 0xb6, 0xc5, 0xce, 0xfb, + 0x9a, 0x82, 0x07, 0xed, 0xe0, 0x98, 0x76, 0x08, 0xb6, 0x5a, 0x77, 0x19, 0xef, 0x88, 0x9f, 0x9d, + 0xc6, 0xf6, 0x0d, 0x17, 0xe1, 0x8b, 0x37, 0x6f, 0xba, 0x1b, 0x9d, 0x5b, 0x67, 0x6d, 0xce, 0x76, + 0x6b, 0x27, 0x94, 0x4f, 0x40, 0x47, 0x76, 0xde, 0x97, 0x66, 0xf1, 0xab, 0x77, 0x73, 0xeb, 0x4e, + 0x6c, 0xac, 0x1b, 0x96, 0x00, 0xaf, 0xc7, 0xb1, 0xa5, 0x7e, 0xfa, 0xcf, 0xfc, 0x18, 0x1d, 0xe3, + 0x73, 0xf0, 0x36, 0x72, 0x94, 0x0a, 0x7b, 0x64, 0x7f, 0xe5, 0xda, 0xea, 0x07, 0x64, 0x6d, 0x17, + 0x3f, 0xda, 0xc0, 0x82, 0x4a, 0x9e, 0x7f, 0x9b, 0x83, 0xb3, 0x38, 0x85, 0x00, 0x16, 0x76, 0x42, + 0x9f, 0x00, 0xdb, 0xae, 0x5d, 0x7d, 0xe4, 0xeb, 0xec, 0xc0, 0x79, 0x29, 0x56, 0x07, 0x80, 0xfd, + 0xcf, 0xcc, 0xf3, 0x88, 0x6f, 0x89, 0x77, 0xe6, 0x3d, 0x4a, 0xc2, 0x36, 0x29, 0x68, 0x7e, 0x08, + 0xfc, 0x39, 0xdf, 0xc8, 0xe2, 0x2d, 0x7f, 0x31, 0xdf, 0xe1, 0x2f, 0x9a, 0x04, 0xd4, 0x79, 0x6a, + 0x97, 0x3d, 0x50, 0xd2, 0x69, 0x94, 0x7b, 0x96, 0xdf, 0x8f, 0x3a, 0x66, 0x20, 0x00, 0x21, 0xfc, + 0x0e, 0x4f, 0x32, 0xd9, 0x91, 0x87, 0x62, 0xf5, 0x44, 0x5b, 0x7e, 0xbe, 0x23, 0xd5, 0xa9, 0xbc, + 0xc9, 0x8e, 0x9a, 0x48, 0xa0, 0x49, 0xd6, 0x4d, 0xb6, 0xfa, 0x10, 0xe8, 0x09, 0x48, 0xc5, 0xdf, + 0xac, 0x91, 0x9f, 0x60, 0xe2, 0x0f, 0x39, 0xd2, 0xff, 0xb9, 0xd8, 0xff, 0x07, 0xff, 0x03, 0x8d, + 0x05, 0x6d, 0x6c, 0x00, 0x00 +}; diff --git a/wled00/html_settings.h b/wled00/html_settings.h index 7291b5c9..8082d0b8 100644 --- a/wled00/html_settings.h +++ b/wled00/html_settings.h @@ -1,348 +1,2235 @@ /* * More web UI HTML source arrays. * This file is auto generated, please don't make any changes manually. - * Instead, see https://github.com/Aircoookie/WLED/wiki/Add-own-functionality#web-ui + * Instead, 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/style.css, do not edit!! -const char PAGE_settingsCss[] PROGMEM = R"=====()====="; +const uint16_t PAGE_settingsCss_length = 888; +const uint8_t PAGE_settingsCss[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xad, 0x56, 0x51, 0x8b, 0xab, 0x38, + 0x14, 0xfe, 0x2b, 0x2e, 0x65, 0x60, 0x2e, 0x54, 0x51, 0xab, 0x9d, 0xde, 0xc8, 0xc2, 0xb2, 0xef, + 0xf7, 0x6d, 0x58, 0x16, 0x96, 0x79, 0x88, 0xe6, 0x58, 0x43, 0x63, 0x22, 0x49, 0xbc, 0xb5, 0x23, + 0xfe, 0xf7, 0x4d, 0xa2, 0x8e, 0xb6, 0x23, 0x73, 0x5f, 0x2e, 0xa5, 0x25, 0xe6, 0xc4, 0xe4, 0x3b, + 0xdf, 0xf9, 0xce, 0x97, 0x56, 0xba, 0x66, 0xbd, 0x16, 0x6d, 0x51, 0xf9, 0xb8, 0xd0, 0x54, 0x70, + 0x54, 0x63, 0x4e, 0x9b, 0x96, 0x61, 0xfb, 0x30, 0xe4, 0x82, 0xdc, 0xfa, 0x52, 0x70, 0xed, 0x97, + 0xb8, 0xa6, 0xec, 0x86, 0xfe, 0x01, 0x49, 0x30, 0xc7, 0x7b, 0x85, 0xb9, 0xf2, 0x15, 0x48, 0x5a, + 0x66, 0x2e, 0xac, 0xe8, 0x3b, 0xa0, 0x48, 0x42, 0x9d, 0x69, 0xe8, 0xb4, 0x8f, 0x19, 0x3d, 0x73, + 0x54, 0x00, 0xd7, 0x20, 0xb3, 0x1c, 0x17, 0x97, 0xb3, 0x14, 0x2d, 0x27, 0x68, 0x17, 0xc7, 0x71, + 0x56, 0x08, 0x26, 0x24, 0xda, 0x95, 0x65, 0x99, 0x31, 0xca, 0xc1, 0xaf, 0x80, 0x9e, 0x2b, 0x8d, + 0xe2, 0x30, 0x7c, 0xca, 0x6a, 0x2c, 0xcf, 0x94, 0xa3, 0x70, 0xa8, 0x64, 0x9f, 0x0b, 0x49, 0x40, + 0xfa, 0xd3, 0xf2, 0xe3, 0xf1, 0x68, 0x26, 0x03, 0x65, 0xf0, 0x5e, 0x29, 0xd1, 0x15, 0x8a, 0x8f, + 0x61, 0xd3, 0x0d, 0x78, 0x8f, 0x51, 0x25, 0x7e, 0x82, 0xec, 0xa7, 0x75, 0xf1, 0xa9, 0x1c, 0x31, + 0x10, 0x28, 0x84, 0x74, 0x69, 0x20, 0x2e, 0x38, 0x0c, 0x41, 0xae, 0xf9, 0x3e, 0x6f, 0xb5, 0x16, + 0xbc, 0x5f, 0x43, 0x3a, 0x1c, 0x0e, 0x6b, 0x48, 0xbf, 0xc8, 0x76, 0x04, 0x85, 0x82, 0x43, 0x51, + 0x79, 0x4a, 0x30, 0x4a, 0x3c, 0xb7, 0xc1, 0x84, 0x55, 0x62, 0x42, 0x5b, 0x85, 0xe2, 0xa4, 0xe9, + 0x32, 0x42, 0x55, 0xc3, 0xf0, 0x0d, 0x51, 0xee, 0xb2, 0xcc, 0x99, 0x28, 0x2e, 0x2b, 0xb2, 0x62, + 0x83, 0x7e, 0x4e, 0x37, 0x8a, 0x9b, 0xce, 0x3b, 0x8d, 0xdf, 0xac, 0xc1, 0x84, 0x50, 0x7e, 0x46, + 0xf6, 0xd9, 0x06, 0xb2, 0x9a, 0x72, 0x7f, 0x4c, 0x39, 0xb1, 0xf1, 0xa2, 0x95, 0xca, 0x80, 0x6d, + 0x04, 0x75, 0xec, 0x6e, 0xe6, 0x3a, 0xa6, 0xe9, 0xc8, 0x5a, 0x6d, 0xf7, 0x88, 0xd2, 0x22, 0x58, + 0x55, 0x2f, 0xbd, 0x3f, 0x6b, 0x85, 0x2f, 0xf4, 0xec, 0x27, 0xb2, 0x7c, 0xef, 0x54, 0x81, 0x79, + 0x3f, 0xce, 0xfb, 0x5a, 0x34, 0xc8, 0x77, 0xd3, 0x81, 0x19, 0x4a, 0x71, 0xed, 0xed, 0x4c, 0x98, + 0x35, 0x42, 0x51, 0x07, 0x46, 0x69, 0x5a, 0x5c, 0x6e, 0x2b, 0x05, 0xcc, 0xd5, 0xb4, 0x3a, 0x78, + 0xf7, 0x29, 0x27, 0xd0, 0xa1, 0x68, 0x08, 0x18, 0xbf, 0x4c, 0xe5, 0x36, 0xa5, 0x0f, 0x2a, 0x60, + 0xcd, 0xdf, 0xfd, 0x4a, 0x48, 0x0c, 0x4a, 0xbd, 0x6c, 0x8a, 0x73, 0x43, 0x7c, 0xab, 0x21, 0x1b, + 0x81, 0x3a, 0x19, 0x04, 0x15, 0x25, 0xd0, 0xcf, 0x94, 0x8f, 0xf5, 0x06, 0x39, 0x8b, 0x42, 0x02, + 0x19, 0x82, 0x2b, 0x96, 0x7c, 0x16, 0x49, 0x89, 0xc3, 0x81, 0xf2, 0xa6, 0xd5, 0xbf, 0x41, 0x09, + 0xe9, 0x9d, 0x12, 0xc6, 0x6d, 0x91, 0x41, 0x82, 0x73, 0x06, 0x64, 0x3e, 0xf0, 0x74, 0x3a, 0x8d, + 0x91, 0xff, 0xf4, 0xad, 0x81, 0x3f, 0x79, 0x5b, 0xe7, 0x20, 0xdf, 0xf6, 0xab, 0x29, 0x9b, 0xee, + 0xdb, 0x5e, 0x01, 0x83, 0x42, 0xf7, 0x4b, 0x55, 0x6a, 0x30, 0xb5, 0xaa, 0xe7, 0x42, 0x18, 0x39, + 0x6c, 0x6c, 0x33, 0x35, 0x44, 0x02, 0xf5, 0x46, 0x30, 0xe8, 0xba, 0xb9, 0x63, 0xa2, 0x30, 0xdc, + 0x7c, 0x3f, 0xf8, 0x58, 0x71, 0x4a, 0xb7, 0x17, 0xcc, 0xf1, 0x63, 0xb2, 0x1d, 0xaf, 0xa7, 0x78, + 0x7a, 0xdc, 0x8e, 0xab, 0x7e, 0x11, 0xf0, 0x26, 0x80, 0x8f, 0x05, 0x0f, 0x08, 0x8b, 0x0a, 0x8a, + 0x4b, 0x2e, 0xba, 0xb7, 0x5e, 0x4b, 0x43, 0x7d, 0x29, 0x64, 0x8d, 0x8c, 0x02, 0x19, 0x3c, 0x47, + 0x41, 0xfa, 0x6d, 0xa2, 0xc5, 0x97, 0xce, 0x40, 0x9c, 0x10, 0x35, 0xf1, 0x36, 0x5f, 0xbf, 0x5b, + 0x29, 0xc1, 0x18, 0x86, 0x5e, 0x9f, 0x53, 0x52, 0x06, 0x6f, 0x2b, 0xda, 0x23, 0x9b, 0xc8, 0x54, + 0x8c, 0x85, 0xfb, 0xec, 0xb7, 0xab, 0x65, 0x3c, 0x22, 0x68, 0xa8, 0xed, 0xa9, 0x6e, 0x6a, 0xbd, + 0xc8, 0x75, 0xa6, 0xd3, 0xbf, 0x75, 0xb6, 0x92, 0x89, 0x2b, 0x02, 0xc6, 0x68, 0xa3, 0xa8, 0x1a, + 0xb4, 0xec, 0xd7, 0xae, 0x69, 0x6a, 0xfa, 0x64, 0x92, 0xfe, 0x68, 0x73, 0xab, 0x90, 0x80, 0xa4, + 0x33, 0x9d, 0xc6, 0x91, 0xff, 0xa0, 0x75, 0x23, 0xa4, 0xc6, 0x5c, 0x0f, 0x81, 0x61, 0x6e, 0x9d, + 0x64, 0x90, 0x5a, 0xc7, 0xbe, 0x37, 0x94, 0x61, 0xf7, 0xfa, 0xe3, 0xd5, 0xd3, 0x56, 0xbd, 0x8b, + 0x6c, 0x9e, 0x86, 0x5d, 0xad, 0xce, 0xf7, 0xfd, 0xb5, 0xd3, 0x02, 0x2b, 0xdd, 0x8b, 0x06, 0x17, + 0x54, 0xdf, 0x4c, 0xd7, 0x7f, 0xee, 0xf2, 0x24, 0x49, 0x1e, 0x3c, 0x27, 0x75, 0x2e, 0x64, 0xcc, + 0xa9, 0x76, 0x5a, 0xfa, 0x44, 0xe0, 0x88, 0xeb, 0x65, 0x65, 0x83, 0xb6, 0x12, 0xd9, 0x84, 0xcd, + 0x37, 0x75, 0xe3, 0x5a, 0xb9, 0xf3, 0x17, 0x3f, 0x28, 0x69, 0x07, 0x64, 0xe3, 0xde, 0x99, 0xfd, + 0x25, 0xcd, 0x16, 0xed, 0xb8, 0x91, 0xb9, 0xdc, 0xe0, 0xdf, 0x67, 0x3f, 0x0d, 0x9f, 0xac, 0x82, + 0x66, 0xd6, 0xbf, 0x9b, 0xeb, 0xc7, 0x1a, 0x0d, 0x4a, 0x6d, 0xba, 0x2e, 0xb9, 0x40, 0x55, 0xc6, + 0xd3, 0xe6, 0x0c, 0xa3, 0x2d, 0x1f, 0x3b, 0x26, 0x99, 0xb9, 0x30, 0xeb, 0xd1, 0x7b, 0x4b, 0x4c, + 0x80, 0x72, 0x2f, 0x48, 0xd5, 0x7e, 0x19, 0x7a, 0xb1, 0xfd, 0x71, 0x92, 0x53, 0x33, 0x6b, 0xd6, + 0x9b, 0x84, 0xfc, 0x72, 0xe7, 0x3c, 0x8e, 0x36, 0x77, 0x1e, 0xfe, 0xb2, 0x96, 0x80, 0x3d, 0x55, + 0x48, 0x00, 0xee, 0x61, 0x4e, 0xbc, 0xe7, 0x25, 0x89, 0x97, 0xa3, 0xe1, 0xee, 0x5b, 0xbf, 0x52, + 0x36, 0xd4, 0x98, 0xb2, 0x3b, 0xa7, 0x71, 0x5a, 0xdf, 0x7f, 0xed, 0x46, 0x0d, 0x56, 0xea, 0x6a, + 0x2a, 0xf7, 0x60, 0x51, 0xec, 0xb3, 0x65, 0x3d, 0x36, 0xcd, 0xd7, 0xf8, 0x92, 0x53, 0xf8, 0x80, + 0xef, 0x93, 0x45, 0x84, 0xbf, 0xb0, 0x88, 0xc3, 0x83, 0x09, 0x8e, 0xad, 0x3b, 0xfd, 0x27, 0xb0, + 0x37, 0xef, 0xb0, 0x33, 0x37, 0xbd, 0xf2, 0xa6, 0xee, 0x9d, 0x34, 0x9c, 0xd8, 0xc0, 0xf0, 0x3f, + 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
-
-)====="; +const uint16_t PAGE_settings_length = 1115; +const uint8_t PAGE_settings[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0a, 0xb5, 0x56, 0xdb, 0x52, 0xe3, 0x46, + 0x10, 0x7d, 0xf7, 0x57, 0x0c, 0xb3, 0x15, 0x56, 0x2a, 0x64, 0xf9, 0x42, 0x2a, 0x95, 0xc8, 0x96, + 0xa9, 0x2c, 0x97, 0x8d, 0x53, 0x50, 0x4b, 0x05, 0x58, 0x92, 0x4a, 0xf2, 0x30, 0xd6, 0xb4, 0xac, + 0x59, 0xa4, 0x19, 0xd5, 0x4c, 0xcb, 0xe0, 0xb0, 0xfc, 0x7b, 0x7a, 0x64, 0x63, 0x60, 0xe1, 0x21, + 0x9b, 0x90, 0x17, 0xcb, 0xd3, 0xea, 0x39, 0x7d, 0xfa, 0xf4, 0xc5, 0x1e, 0x6f, 0x1d, 0x7c, 0xd8, + 0x3f, 0xff, 0xed, 0xf4, 0x90, 0x15, 0x58, 0x95, 0x93, 0xb1, 0xff, 0x64, 0xa5, 0xd0, 0xf3, 0x94, + 0x83, 0xe6, 0x74, 0x06, 0x21, 0x27, 0xe3, 0x0a, 0x50, 0xb0, 0xac, 0x10, 0xd6, 0x01, 0xa6, 0xfc, + 0xe2, 0xfc, 0xa8, 0xfb, 0x3d, 0x5f, 0x5b, 0x3b, 0x99, 0xd1, 0x08, 0x9a, 0xcc, 0xd7, 0x4a, 0x62, + 0x91, 0x4a, 0x58, 0xa8, 0x0c, 0xba, 0xed, 0x21, 0x52, 0x5a, 0xa1, 0x12, 0x65, 0xd7, 0x65, 0xa2, + 0x84, 0x74, 0x10, 0x55, 0xe2, 0x46, 0x55, 0x4d, 0xb5, 0x39, 0x37, 0x0e, 0x6c, 0x7b, 0x10, 0x33, + 0x3a, 0x6b, 0xc3, 0x59, 0x47, 0x8b, 0x0a, 0x52, 0xbe, 0x50, 0x70, 0x5d, 0x1b, 0x8b, 0x14, 0x05, + 0x15, 0x96, 0x30, 0xb9, 0x3c, 0x3e, 0x3c, 0x60, 0x67, 0x80, 0xa8, 0xf4, 0xdc, 0x8d, 0x7b, 0x2b, + 0xe3, 0xd8, 0x65, 0x56, 0xd5, 0x38, 0xe9, 0x2c, 0x84, 0x65, 0xa5, 0xc9, 0x54, 0x1d, 0xc9, 0x54, + 0x9a, 0xac, 0xa9, 0x88, 0x50, 0x44, 0x86, 0x74, 0x6b, 0xe0, 0x1f, 0xb5, 0x35, 0x68, 0x52, 0x5e, + 0x20, 0xd6, 0x09, 0x1f, 0xe5, 0x8d, 0xce, 0x50, 0x19, 0xcd, 0xe6, 0x53, 0x19, 0x60, 0x78, 0x6b, + 0x01, 0x1b, 0xab, 0x99, 0x8c, 0xe7, 0x80, 0x87, 0x25, 0xf8, 0xbb, 0xef, 0x96, 0xed, 0xab, 0xbb, + 0x8d, 0x6b, 0x69, 0x84, 0xfc, 0xf9, 0x2c, 0xc0, 0xc8, 0xa4, 0x5b, 0xfd, 0xf0, 0xb6, 0x04, 0x64, + 0x90, 0xca, 0x38, 0xb3, 0x20, 0x10, 0xd6, 0x97, 0x02, 0xbe, 0xa2, 0xc3, 0xc3, 0x11, 0xc4, 0x24, + 0xd4, 0x8f, 0x88, 0x56, 0xcd, 0x1a, 0x04, 0x7a, 0x61, 0x33, 0x1e, 0x61, 0x18, 0x7d, 0x69, 0xc7, + 0x65, 0x0d, 0x3c, 0xe2, 0x08, 0x37, 0xd8, 0xfb, 0x24, 0x16, 0xe2, 0x1e, 0xe0, 0x99, 0xa3, 0x70, + 0x4b, 0x4d, 0x10, 0x26, 0x8c, 0x64, 0x3c, 0x33, 0x72, 0x19, 0x8b, 0xba, 0x06, 0x2d, 0xf7, 0x0b, + 0x55, 0xca, 0x00, 0xbc, 0xbf, 0x90, 0xf2, 0x70, 0x41, 0x2c, 0x8e, 0x95, 0xa3, 0x72, 0x80, 0x0d, + 0xb8, 0xe7, 0xcc, 0xa3, 0x20, 0x4c, 0x27, 0xb7, 0xef, 0x01, 0x3f, 0x06, 0xe1, 0xdd, 0xcb, 0x7e, + 0x60, 0xad, 0xb1, 0x44, 0x8f, 0xfc, 0xa8, 0x96, 0xce, 0x94, 0x10, 0x97, 0x66, 0x1e, 0xf0, 0x43, + 0x6f, 0x67, 0xeb, 0xe4, 0x49, 0x76, 0x96, 0xab, 0x12, 0xda, 0x34, 0xa8, 0x78, 0x96, 0xd2, 0x3d, + 0x5e, 0xdb, 0x4d, 0xce, 0xe8, 0x62, 0xae, 0xe6, 0x8d, 0x15, 0xad, 0x5a, 0xab, 0x34, 0x58, 0x2e, + 0xe8, 0x82, 0x8c, 0xff, 0xd0, 0x53, 0x9d, 0x99, 0xaa, 0x26, 0xd1, 0x80, 0xd5, 0x62, 0x0e, 0x4c, + 0x0a, 0x14, 0x5b, 0x9c, 0xf8, 0x3c, 0x08, 0x7c, 0x16, 0xac, 0x54, 0xc5, 0xf4, 0x5a, 0x69, 0x69, + 0xae, 0x89, 0x42, 0xd6, 0x82, 0x8d, 0x54, 0x1e, 0x70, 0x1f, 0x39, 0xe1, 0x69, 0x8a, 0x71, 0x5b, + 0xca, 0xcc, 0x94, 0x61, 0x5b, 0xdd, 0x7e, 0x14, 0xb4, 0x65, 0x4f, 0xbd, 0x77, 0x79, 0x86, 0xc6, + 0x12, 0xbc, 0xaf, 0xe3, 0x14, 0xa1, 0xf2, 0x0a, 0x64, 0xd3, 0x9a, 0x87, 0xe1, 0xe7, 0xcf, 0x6b, + 0x37, 0xba, 0x5d, 0xd5, 0xc4, 0xfc, 0x88, 0xf0, 0xd8, 0x89, 0x91, 0x10, 0xb3, 0xd3, 0x12, 0x84, + 0x03, 0x46, 0x8a, 0x80, 0x65, 0x6d, 0x97, 0x4d, 0x4f, 0x89, 0x5b, 0xf4, 0x04, 0xd1, 0x3d, 0x45, + 0x8c, 0x5a, 0xb4, 0x90, 0xea, 0x5c, 0x3a, 0x68, 0x69, 0x1b, 0x4f, 0x4d, 0x60, 0xe1, 0xbb, 0x37, + 0x82, 0xd4, 0xc4, 0xae, 0xa4, 0x29, 0x08, 0x06, 0x91, 0x89, 0xa9, 0x4c, 0xee, 0x52, 0x61, 0x11, + 0xf0, 0x1e, 0x0f, 0xf7, 0xba, 0x83, 0x64, 0x61, 0x94, 0x64, 0xfd, 0x30, 0x76, 0x75, 0xa9, 0xb0, + 0xb5, 0x52, 0xbf, 0x94, 0xa0, 0xe7, 0x58, 0x4c, 0x06, 0xdb, 0xdb, 0xc1, 0xa6, 0x61, 0x1f, 0xb2, + 0x8d, 0xd6, 0xd9, 0xae, 0xb2, 0xc0, 0xb8, 0x30, 0x0e, 0x7d, 0xa8, 0x9d, 0x80, 0x7c, 0x68, 0x4c, + 0xf6, 0x78, 0xc2, 0x77, 0x56, 0x5f, 0x13, 0xce, 0xc3, 0x1d, 0x02, 0xdd, 0x81, 0xdf, 0xfb, 0x7f, + 0x86, 0x77, 0xeb, 0xc6, 0x25, 0x49, 0x2e, 0x7e, 0x39, 0xa6, 0x60, 0x6e, 0x3d, 0x42, 0x3d, 0x17, + 0x7f, 0x72, 0x7b, 0x75, 0xda, 0xa7, 0x54, 0xb7, 0x06, 0x8f, 0x0a, 0xb1, 0xf6, 0xdc, 0xcc, 0x85, + 0xa7, 0xb3, 0x77, 0x4f, 0x89, 0x80, 0x09, 0xb9, 0x65, 0xd1, 0xc6, 0xc1, 0xbb, 0xce, 0xb8, 0xb7, + 0x1e, 0xc2, 0xb1, 0xc3, 0x25, 0xcd, 0x64, 0xc7, 0xb7, 0xe7, 0xad, 0xef, 0xe8, 0xae, 0x28, 0xd5, + 0x5c, 0x27, 0x59, 0x2b, 0xed, 0x68, 0x26, 0xb2, 0xab, 0xb9, 0x35, 0x8d, 0x96, 0xc9, 0x9b, 0xe1, + 0x70, 0x38, 0x2a, 0x40, 0xcd, 0x0b, 0x4c, 0x06, 0xfd, 0x7e, 0x7d, 0x33, 0xaa, 0x84, 0x9d, 0x2b, + 0x9d, 0xf4, 0xef, 0xfc, 0xfa, 0xb9, 0xed, 0x76, 0x8b, 0xe4, 0x87, 0x45, 0x71, 0x47, 0xad, 0x8f, + 0x46, 0xdf, 0x3e, 0xbe, 0xb9, 0xbb, 0xbb, 0x3b, 0x22, 0x3d, 0x8c, 0x4d, 0xde, 0xe4, 0x79, 0x3e, + 0xca, 0x69, 0xf7, 0x74, 0x73, 0x51, 0xa9, 0x72, 0x99, 0x7c, 0x04, 0x2b, 0x85, 0x16, 0xd1, 0x4f, + 0x50, 0x2e, 0x00, 0x55, 0x26, 0x22, 0x27, 0xb4, 0xeb, 0xd2, 0x86, 0x51, 0xf9, 0x48, 0x2a, 0x52, + 0x5b, 0x2c, 0x93, 0x19, 0x71, 0xbf, 0x1a, 0xcd, 0x8c, 0x95, 0x60, 0x93, 0x41, 0x7d, 0xc3, 0xa8, + 0xdd, 0xa9, 0x1a, 0x2d, 0xee, 0xca, 0xda, 0xb5, 0xd4, 0xd4, 0x8d, 0x4b, 0x68, 0xa5, 0x04, 0x44, + 0x24, 0x5c, 0xc5, 0x70, 0xea, 0x2f, 0x48, 0xbe, 0x5b, 0x54, 0x4a, 0xdf, 0x13, 0xdf, 0xbc, 0x6f, + 0x37, 0x5d, 0x42, 0xcd, 0x92, 0x05, 0x94, 0xcc, 0x37, 0xac, 0xcb, 0xbe, 0xa5, 0x94, 0xc2, 0xfb, + 0x9c, 0x86, 0x8b, 0x82, 0x89, 0x06, 0x0d, 0xeb, 0x8f, 0xb2, 0xc6, 0x3a, 0x62, 0x5e, 0x1b, 0xe5, + 0x15, 0x69, 0xa5, 0x6b, 0x25, 0x1b, 0xf7, 0x56, 0x5b, 0xd6, 0x2b, 0x47, 0xf3, 0xe6, 0x4b, 0x96, + 0x72, 0x1a, 0x08, 0x5a, 0x7e, 0x2b, 0x09, 0x98, 0xdf, 0x13, 0x29, 0x77, 0xcd, 0xac, 0x52, 0xc8, + 0x99, 0xa2, 0xd7, 0x33, 0x5a, 0x95, 0x46, 0x67, 0xd4, 0x65, 0x57, 0xe9, 0xdb, 0x2f, 0x46, 0x26, + 0xdd, 0x54, 0x9b, 0x87, 0x6f, 0x27, 0xef, 0x48, 0xbe, 0x71, 0x6f, 0x05, 0x34, 0x61, 0x2f, 0x23, + 0xfe, 0x03, 0xa8, 0x4d, 0xe3, 0x5c, 0xab, 0x5c, 0x79, 0xdc, 0x4b, 0x75, 0xa4, 0xfc, 0x46, 0x6e, + 0xea, 0xe7, 0xe8, 0x9d, 0xa7, 0xf0, 0x5f, 0x81, 0x4e, 0x0b, 0xc3, 0x79, 0x74, 0x3f, 0x88, 0xa7, + 0x16, 0x72, 0xb0, 0xa0, 0x33, 0x70, 0x9d, 0xe7, 0x31, 0xbc, 0x0a, 0x43, 0x39, 0x43, 0xcd, 0x59, + 0x2b, 0x63, 0xca, 0xef, 0x8b, 0xac, 0x8d, 0x06, 0xfe, 0x1f, 0x32, 0x1c, 0x1e, 0x78, 0x06, 0xc3, + 0x03, 0xb6, 0xff, 0x78, 0xab, 0x3d, 0x30, 0xe8, 0xbc, 0x2c, 0xe2, 0x57, 0x44, 0x68, 0x5a, 0x05, + 0x3b, 0x17, 0xd4, 0x9c, 0x6c, 0xea, 0x9b, 0x21, 0x17, 0x19, 0xbc, 0x9c, 0xa2, 0xac, 0x6e, 0xfe, + 0x8f, 0x1c, 0x09, 0xd6, 0x53, 0x38, 0x38, 0xf9, 0x95, 0x7d, 0x68, 0xb0, 0x6e, 0xf0, 0x35, 0x8b, + 0xd8, 0xfe, 0x58, 0x11, 0xfa, 0x19, 0x3d, 0x1f, 0xf2, 0x7b, 0xa9, 0x88, 0xff, 0x3e, 0x01, 0x54, + 0x15, 0xf8, 0x18, 0xe7, 0xf4, 0x64, 0xdb, 0xec, 0x44, 0x64, 0xd6, 0xb8, 0x57, 0xad, 0x51, 0xe5, + 0xe1, 0x7d, 0x89, 0x2a, 0x23, 0x5f, 0x97, 0xbb, 0x83, 0x95, 0x3c, 0x40, 0x1b, 0x41, 0xe1, 0x92, + 0xe8, 0x5f, 0xd4, 0xf4, 0x9b, 0x08, 0x0f, 0xfc, 0x7d, 0x38, 0xda, 0x06, 0x7e, 0x35, 0xf8, 0xbf, + 0x65, 0x7f, 0x03, 0x8f, 0x35, 0xff, 0x66, 0xa6, 0x09, 0x00, 0x00 +}; // Autogenerated from wled00/data/settings_wifi.htm, do not edit!! -const char PAGE_settings_wifi[] PROGMEM = R"=====(WiFi Settings"; + messageBody += F(""); } else if (optt < 120) //redirect back after optionType-60 seconds, unused { //messageBody += ""; } else if (optt < 180) //reload parent after optionType-120 seconds { - messageBody += ""; + messageBody += F(""); } else if (optt == 253) { messageBody += F("

"); //button to settings @@ -305,35 +488,17 @@ String msgProcessor(const String& var) } -void serveMessage(AsyncWebServerRequest* request, uint16_t code, String headl, String subl, byte optionT) +void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl, byte optionT) { messageHead = headl; messageSub = subl; optionType = optionT; - + request->send_P(code, "text/html", PAGE_msg, msgProcessor); } -String settingsProcessor(const String& var) -{ - if (var == "CSS") { - char buf[2048]; - getSettingsJS(optionType, buf); - return String(buf); - } - - #ifdef WLED_ENABLE_DMX - - if (var == "DMXMENU") { - return String(F("
")); - } - - #endif - if (var == "SCSS") return String(FPSTR(PAGE_settingsCss)); - return String(); -} - +#ifdef WLED_ENABLE_DMX String dmxProcessor(const String& var) { String mapJS; @@ -342,7 +507,7 @@ String dmxProcessor(const String& var) mapJS += "\nCN=" + String(DMXChannels) + ";\n"; mapJS += "CS=" + String(DMXStart) + ";\n"; mapJS += "CG=" + String(DMXGap) + ";\n"; - mapJS += "LC=" + String(ledCount) + ";\n"; + mapJS += "LC=" + String(strip.getLengthTotal()) + ";\n"; mapJS += "var CH=["; for (int i=0;i<15;i++) { mapJS += String(DMXFixtureMap[i]) + ","; @@ -350,49 +515,131 @@ String dmxProcessor(const String& var) mapJS += "0];"; } #endif - + return mapJS; } +#endif -void serveSettings(AsyncWebServerRequest* request) +void serveSettingsJS(AsyncWebServerRequest* request) { - byte subPage = 0; - const String& url = request->url(); - if (url.indexOf("sett") >= 0) - { - if (url.indexOf("wifi") > 0) subPage = 1; - else if (url.indexOf("leds") > 0) subPage = 2; - else if (url.indexOf("ui") > 0) subPage = 3; - else if (url.indexOf("sync") > 0) subPage = 4; - else if (url.indexOf("time") > 0) subPage = 5; - else if (url.indexOf("sec") > 0) subPage = 6; - #ifdef WLED_ENABLE_DMX // include only if DMX is enabled - else if (url.indexOf("dmx") > 0) subPage = 7; - #endif - } else subPage = 255; //welcome page - - if (subPage == 1 && wifiLock && otaLock) - { - serveMessage(request, 500, "Access Denied", "Please unlock OTA in security settings!", 254); return; + char buf[SETTINGS_STACK_BUF_SIZE+37]; + buf[0] = 0; + byte subPage = request->arg(F("p")).toInt(); + if (subPage > 10) { + strcpy_P(buf, PSTR("alert('Settings for this request are not implemented.');")); + request->send(501, "application/javascript", buf); + return; } - - #ifdef WLED_DISABLE_MOBILE_UI //disable welcome page if not enough storage - if (subPage == 255) {serveIndex(request); return;} - #endif + if (subPage > 0 && !correctPIN && strlen(settingsPIN)>0) { + strcpy_P(buf, PSTR("alert('PIN incorrect.');")); + request->send(403, "application/javascript", buf); + return; + } + strcat_P(buf,PSTR("function GetV(){var d=document;")); + getSettingsJS(subPage, buf+strlen(buf)); // this may overflow by 35bytes!!! + strcat_P(buf,PSTR("}")); + request->send(200, "application/javascript", buf); +} - optionType = subPage; - + +void serveSettings(AsyncWebServerRequest* request, bool post) +{ + byte subPage = 0, originalSubPage = 0; + const String& url = request->url(); + + if (url.indexOf("sett") >= 0) + { + if (url.indexOf(".js") > 0) subPage = SUBPAGE_JS; + else if (url.indexOf(".css") > 0) subPage = SUBPAGE_CSS; + else if (url.indexOf("wifi") > 0) subPage = SUBPAGE_WIFI; + else if (url.indexOf("leds") > 0) subPage = SUBPAGE_LEDS; + else if (url.indexOf("ui") > 0) subPage = SUBPAGE_UI; + else if (url.indexOf("sync") > 0) subPage = SUBPAGE_SYNC; + else if (url.indexOf("time") > 0) subPage = SUBPAGE_TIME; + else if (url.indexOf("sec") > 0) subPage = SUBPAGE_SEC; + else if (url.indexOf("dmx") > 0) subPage = SUBPAGE_DMX; + else if (url.indexOf("um") > 0) subPage = SUBPAGE_UM; + else if (url.indexOf("2D") > 0) subPage = SUBPAGE_2D; + else if (url.indexOf("lock") > 0) subPage = SUBPAGE_LOCK; + } + else if (url.indexOf("/update") >= 0) subPage = SUBPAGE_UPDATE; // update page, for PIN check + //else if (url.indexOf("/edit") >= 0) subPage = 10; + else subPage = SUBPAGE_WELCOME; + + if (!correctPIN && strlen(settingsPIN) > 0 && (subPage > 0 && subPage < 11)) { + originalSubPage = subPage; + subPage = SUBPAGE_PINREQ; // require PIN + } + + // if OTA locked or too frequent PIN entry requests fail hard + if ((subPage == SUBPAGE_WIFI && wifiLock && otaLock) || (post && !correctPIN && millis()-lastEditTime < PIN_RETRY_COOLDOWN)) + { + serveMessage(request, 500, "Access Denied", FPSTR(s_unlock_ota), 254); return; + } + + if (post) { //settings/set POST request, saving + if (subPage != SUBPAGE_WIFI || !(wifiLock && otaLock)) handleSettingsSet(request, subPage); + + char s[32]; + char s2[45] = ""; + + switch (subPage) { + case SUBPAGE_WIFI : strcpy_P(s, PSTR("WiFi")); strcpy_P(s2, PSTR("Please connect to the new IP (if changed)")); forceReconnect = true; break; + case SUBPAGE_LEDS : strcpy_P(s, PSTR("LED")); break; + case SUBPAGE_UI : strcpy_P(s, PSTR("UI")); break; + case SUBPAGE_SYNC : strcpy_P(s, PSTR("Sync")); break; + case SUBPAGE_TIME : strcpy_P(s, PSTR("Time")); break; + case SUBPAGE_SEC : strcpy_P(s, PSTR("Security")); if (doReboot) strcpy_P(s2, PSTR("Rebooting, please wait ~10 seconds...")); break; + case SUBPAGE_DMX : strcpy_P(s, PSTR("DMX")); break; + case SUBPAGE_UM : strcpy_P(s, PSTR("Usermods")); break; + case SUBPAGE_2D : strcpy_P(s, PSTR("2D")); break; + case SUBPAGE_PINREQ : strcpy_P(s, correctPIN ? PSTR("PIN accepted") : PSTR("PIN rejected")); break; + } + + if (subPage != SUBPAGE_PINREQ) strcat_P(s, PSTR(" settings saved.")); + + if (subPage == SUBPAGE_PINREQ && correctPIN) { + subPage = originalSubPage; // on correct PIN load settings page the user intended + } else { + if (!s2[0]) strcpy_P(s2, s_redirecting); + + bool redirectAfter9s = (subPage == SUBPAGE_WIFI || ((subPage == SUBPAGE_SEC || subPage == SUBPAGE_UM) && doReboot)); + serveMessage(request, 200, s, s2, redirectAfter9s ? 129 : (correctPIN ? 1 : 3)); + return; + } + } + + AsyncWebServerResponse *response; switch (subPage) { - case 1: request->send_P(200, "text/html", PAGE_settings_wifi, settingsProcessor); break; - case 2: request->send_P(200, "text/html", PAGE_settings_leds, settingsProcessor); break; - case 3: request->send_P(200, "text/html", PAGE_settings_ui , settingsProcessor); break; - case 4: request->send_P(200, "text/html", PAGE_settings_sync, settingsProcessor); break; - case 5: request->send_P(200, "text/html", PAGE_settings_time, settingsProcessor); break; - case 6: request->send_P(200, "text/html", PAGE_settings_sec , settingsProcessor); break; - case 7: request->send_P(200, "text/html", PAGE_settings_dmx , settingsProcessor); break; - case 255: request->send_P(200, "text/html", PAGE_welcome); break; - default: request->send_P(200, "text/html", PAGE_settings , settingsProcessor); + case SUBPAGE_WIFI : response = request->beginResponse_P(200, "text/html", PAGE_settings_wifi, PAGE_settings_wifi_length); break; + case SUBPAGE_LEDS : response = request->beginResponse_P(200, "text/html", PAGE_settings_leds, PAGE_settings_leds_length); break; + case SUBPAGE_UI : response = request->beginResponse_P(200, "text/html", PAGE_settings_ui, PAGE_settings_ui_length); break; + case SUBPAGE_SYNC : response = request->beginResponse_P(200, "text/html", PAGE_settings_sync, PAGE_settings_sync_length); break; + case SUBPAGE_TIME : response = request->beginResponse_P(200, "text/html", PAGE_settings_time, PAGE_settings_time_length); break; + case SUBPAGE_SEC : response = request->beginResponse_P(200, "text/html", PAGE_settings_sec, PAGE_settings_sec_length); break; +#ifdef WLED_ENABLE_DMX + case SUBPAGE_DMX : response = request->beginResponse_P(200, "text/html", PAGE_settings_dmx, PAGE_settings_dmx_length); break; +#endif + case SUBPAGE_UM : response = request->beginResponse_P(200, "text/html", PAGE_settings_um, PAGE_settings_um_length); break; + case SUBPAGE_UPDATE : response = request->beginResponse_P(200, "text/html", PAGE_update, PAGE_update_length); break; +#ifndef WLED_DISABLE_2D + case SUBPAGE_2D : response = request->beginResponse_P(200, "text/html", PAGE_settings_2D, PAGE_settings_2D_length); break; +#endif + case SUBPAGE_LOCK : { + correctPIN = !strlen(settingsPIN); // lock if a pin is set + createEditHandler(correctPIN); + serveMessage(request, 200, strlen(settingsPIN) > 0 ? PSTR("Settings locked") : PSTR("No PIN set"), FPSTR(s_redirecting), 1); + return; + } + case SUBPAGE_PINREQ : response = request->beginResponse_P(200, "text/html", PAGE_settings_pin, PAGE_settings_pin_length); break; + case SUBPAGE_CSS : response = request->beginResponse_P(200, "text/css", PAGE_settingsCss, PAGE_settingsCss_length); break; + case SUBPAGE_JS : serveSettingsJS(request); return; + case SUBPAGE_WELCOME : response = request->beginResponse_P(200, "text/html", PAGE_welcome, PAGE_welcome_length); break; + default: response = request->beginResponse_P(200, "text/html", PAGE_settings, PAGE_settings_length); break; } + response->addHeader(FPSTR(s_content_enc),"gzip"); + setStaticContentCacheHeaders(response); + request->send(response); } diff --git a/wled00/ws.cpp b/wled00/ws.cpp new file mode 100644 index 00000000..49780d02 --- /dev/null +++ b/wled00/ws.cpp @@ -0,0 +1,234 @@ +#include "wled.h" + +/* + * WebSockets server for bidirectional communication + */ +#ifdef WLED_ENABLE_WEBSOCKETS + +uint16_t wsLiveClientId = 0; +unsigned long wsLastLiveTime = 0; +//uint8_t* wsFrameBuffer = nullptr; + +#define WS_LIVE_INTERVAL 40 + +void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) +{ + if(type == WS_EVT_CONNECT){ + //client connected + DEBUG_PRINTLN(F("WS client connected.")); + sendDataWs(client); + } else if(type == WS_EVT_DISCONNECT){ + //client disconnected + if (client->id() == wsLiveClientId) wsLiveClientId = 0; + DEBUG_PRINTLN(F("WS client disconnected.")); + } else if(type == WS_EVT_DATA){ + // data packet + AwsFrameInfo * info = (AwsFrameInfo*)arg; + if(info->final && info->index == 0 && info->len == len){ + // the whole message is in a single frame and we got all of its data (max. 1450 bytes) + if(info->opcode == WS_TEXT) + { + if (len > 0 && len < 10 && data[0] == 'p') { + // application layer ping/pong heartbeat. + // client-side socket layer ping packets are unresponded (investigate) + client->text(F("pong")); + return; + } + + bool verboseResponse = false; + if (!requestJSONBufferLock(11)) return; + + DeserializationError error = deserializeJson(doc, data, len); + JsonObject root = doc.as(); + if (error || root.isNull()) { + releaseJSONBufferLock(); + return; + } + if (root["v"] && root.size() == 1) { + //if the received value is just "{"v":true}", send only to this client + verboseResponse = true; + } else if (root.containsKey("lv")) { + wsLiveClientId = root["lv"] ? client->id() : 0; + } else { + verboseResponse = deserializeState(root); + } + releaseJSONBufferLock(); // will clean fileDoc + + if (!interfaceUpdateCallMode) { // individual client response only needed if no WS broadcast soon + if (verboseResponse) { + sendDataWs(client); + } else { + // we have to send something back otherwise WS connection closes + client->text(F("{\"success\":true}")); + } + // force broadcast in 500ms after updating client + //lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this + } + } + } else { + //message is comprised of multiple frames or the frame is split into multiple packets + //if(info->index == 0){ + //if (!wsFrameBuffer && len < 4096) wsFrameBuffer = new uint8_t[4096]; + //} + + //if (wsFrameBuffer && len < 4096 && info->index + info->) + //{ + + //} + + if((info->index + len) == info->len){ + if(info->final){ + if(info->message_opcode == WS_TEXT) { + client->text(F("{\"error\":9}")); // ERR_JSON we do not handle split packets right now + } + } + } + DEBUG_PRINTLN(F("WS multipart message.")); + } + } else if(type == WS_EVT_ERROR){ + //error was received from the other end + DEBUG_PRINTLN(F("WS error.")); + + } else if(type == WS_EVT_PONG){ + //pong message was received (in response to a ping request maybe) + DEBUG_PRINTLN(F("WS pong.")); + + } +} + +void sendDataWs(AsyncWebSocketClient * client) +{ + if (!ws.count()) return; + AsyncWebSocketMessageBuffer * buffer; + + if (!requestJSONBufferLock(12)) return; + + JsonObject state = doc.createNestedObject("state"); + serializeState(state); + JsonObject info = doc.createNestedObject("info"); + serializeInfo(info); + + size_t len = measureJson(doc); + DEBUG_PRINTF("JSON buffer size: %u for WS request (%u).\n", doc.memoryUsage(), len); + + size_t heap1 = ESP.getFreeHeap(); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + #ifdef ESP8266 + if (len>heap1) { + DEBUG_PRINTLN(F("Out of memory (WS)!")); + return; + } + #endif + buffer = ws.makeBuffer(len); // will not allocate correct memory sometimes on ESP8266 + #ifdef ESP8266 + size_t heap2 = ESP.getFreeHeap(); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + #else + size_t heap2 = 0; // ESP32 variants do not have the same issue and will work without checking heap allocation + #endif + if (!buffer || heap1-heap2lock(); + serializeJson(doc, (char *)buffer->get(), len); + + DEBUG_PRINT(F("Sending WS data ")); + if (client) { + client->text(buffer); + DEBUG_PRINTLN(F("to a single client.")); + } else { + ws.textAll(buffer); + DEBUG_PRINTLN(F("to multiple clients.")); + } + buffer->unlock(); + ws._cleanBuffers(); + + releaseJSONBufferLock(); +} + +bool sendLiveLedsWs(uint32_t wsClient) +{ + AsyncWebSocketClient * wsc = ws.client(wsClient); + if (!wsc || wsc->queueLength() > 0) return false; //only send if queue free + + size_t used = strip.getLengthTotal(); +#ifdef ESP8266 + const size_t MAX_LIVE_LEDS_WS = 256U; +#else + const size_t MAX_LIVE_LEDS_WS = 1024U; +#endif + size_t n = ((used -1)/MAX_LIVE_LEDS_WS) +1; //only serve every n'th LED if count over MAX_LIVE_LEDS_WS + size_t pos = (strip.isMatrix ? 4 : 2); // start of data + size_t bufSize = pos + (used/n)*3; + + AsyncWebSocketMessageBuffer * wsBuf = ws.makeBuffer(bufSize); + if (!wsBuf) return false; //out of memory + uint8_t* buffer = wsBuf->get(); + buffer[0] = 'L'; + buffer[1] = 1; //version + +#ifndef WLED_DISABLE_2D + size_t skipLines = 0; + if (strip.isMatrix) { + buffer[1] = 2; //version + buffer[2] = Segment::maxWidth; + buffer[3] = Segment::maxHeight; + if (used > MAX_LIVE_LEDS_WS*4) { + buffer[2] = Segment::maxWidth/4; + buffer[3] = Segment::maxHeight/4; + skipLines = 3; + } else if (used > MAX_LIVE_LEDS_WS) { + buffer[2] = Segment::maxWidth/2; + buffer[3] = Segment::maxHeight/2; + skipLines = 1; + } + } +#endif + + for (size_t i = 0; pos < bufSize -2; i += n) + { +#ifndef WLED_DISABLE_2D + if (strip.isMatrix && skipLines) { + if ((i/Segment::maxWidth)%(skipLines+1)) i += Segment::maxWidth * skipLines; + } +#endif + uint32_t c = strip.getPixelColor(i); + uint8_t r = R(c); + uint8_t g = G(c); + uint8_t b = B(c); + uint8_t w = W(c); + buffer[pos++] = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map + buffer[pos++] = scale8(qadd8(w, g), strip.getBrightness()); //G + buffer[pos++] = scale8(qadd8(w, b), strip.getBrightness()); //B + } + + wsc->binary(wsBuf); + return true; +} + +void handleWs() +{ + if (millis() - wsLastLiveTime > WS_LIVE_INTERVAL) + { + #ifdef ESP8266 + ws.cleanupClients(3); + #else + ws.cleanupClients(); + #endif + bool success = true; + if (wsLiveClientId) success = sendLiveLedsWs(wsLiveClientId); + wsLastLiveTime = millis(); + if (!success) wsLastLiveTime -= 20; //try again in 20ms if failed due to non-empty WS queue + } +} + +#else +void handleWs() {} +void sendDataWs(AsyncWebSocketClient * client) {} +#endif \ No newline at end of file diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 03265017..c2504cf9 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -1,4 +1,5 @@ #include "wled.h" +#include "wled_ethernet.h" /* * Sending XML status files to client @@ -11,9 +12,9 @@ void XML_response(AsyncWebServerRequest *request, char* dest) obuf = (dest == nullptr)? sbuf:dest; olen = 0; - oappend((const char*)F("")); + oappend(SET_F("")); oappendi((nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri); - oappend(""); + oappend(SET_F("")); for (int i = 0; i < 3; i++) { @@ -27,199 +28,233 @@ void XML_response(AsyncWebServerRequest *request, char* dest) oappendi(colSec[i]); oappend(""); } - oappend(""); + oappend(SET_F("")); oappendi(notifyDirect); - oappend(""); + oappend(SET_F("")); oappendi(receiveNotifications); - oappend(""); + oappend(SET_F("")); oappendi(nightlightActive); - oappend(""); + oappend(SET_F("")); oappendi(nightlightMode > NL_MODE_SET); - oappend(""); + oappend(SET_F("")); oappendi(nightlightDelayMins); - oappend(""); + oappend(SET_F("")); oappendi(nightlightTargetBri); - oappend(""); + oappend(SET_F("")); oappendi(effectCurrent); - oappend(""); + oappend(SET_F("")); oappendi(effectSpeed); - oappend(""); + oappend(SET_F("")); oappendi(effectIntensity); - oappend(""); + oappend(SET_F("")); oappendi(effectPalette); - oappend(""); - if (strip.rgbwMode) { + oappend(SET_F("")); + if (strip.hasWhiteChannel()) { oappendi(col[3]); } else { oappend("-1"); } - oappend(""); + oappend(SET_F("")); oappendi(colSec[3]); - oappend(""); - oappendi((currentPreset < 1) ? 0:currentPreset); - oappend(""); - oappendi(presetCyclingEnabled); - oappend(""); + oappend(SET_F("")); + oappendi(currentPreset); + oappend(SET_F("")); + oappendi(currentPlaylist >= 0); + oappend(SET_F("")); + oappend(serverDescription); if (realtimeMode) { - String mesg = "Live "; - if (realtimeMode == REALTIME_MODE_E131 || realtimeMode == REALTIME_MODE_ARTNET) - { - mesg += (realtimeMode == REALTIME_MODE_E131) ? "E1.31" : "Art-Net"; - mesg += " mode "; - mesg += DMXMode; - mesg += F(" at DMX Address "); - mesg += DMXAddress; - mesg += " from "; - mesg += realtimeIP[0]; - for (int i = 1; i < 4; i++) - { - mesg += "."; - mesg += realtimeIP[i]; - } - } else if (realtimeMode == REALTIME_MODE_UDP || realtimeMode == REALTIME_MODE_HYPERION || realtimeMode == REALTIME_MODE_TPM2NET) { - mesg += "UDP from "; - mesg += realtimeIP[0]; - for (int i = 1; i < 4; i++) - { - mesg += "."; - mesg += realtimeIP[i]; - } - } else if (realtimeMode == REALTIME_MODE_ADALIGHT) { - mesg += F("USB Adalight"); - } else { //generic - mesg += "data"; - } - oappend((char*)mesg.c_str()); - } else { - oappend(serverDescription); + oappend(SET_F(" (live)")); } - oappend(""); - oappendi(strip.getMainSegmentId()); - oappend(""); + oappend(SET_F("")); + oappendi(strip.getFirstSelectedSegId()); + oappend(SET_F("")); if (request != nullptr) request->send(200, "text/xml", obuf); } -void URL_response(AsyncWebServerRequest *request) -{ - char sbuf[256]; - char s2buf[100]; - obuf = s2buf; - olen = 0; - - char s[16]; - oappend("http://"); - IPAddress localIP = WiFi.localIP(); - sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); - oappend(s); - oappend("/win&A="); - oappendi(bri); - oappend("&CL=h"); - for (int i = 0; i < 3; i++) - { - sprintf(s,"%02X", col[i]); - oappend(s); - } - oappend("&C2=h"); - for (int i = 0; i < 3; i++) - { - sprintf(s,"%02X", colSec[i]); - oappend(s); - } - oappend("&FX="); - oappendi(effectCurrent); - oappend("&SX="); - oappendi(effectSpeed); - oappend("&IX="); - oappendi(effectIntensity); - oappend("&FP="); - oappendi(effectPalette); - - obuf = sbuf; - olen = 0; - - oappend((const char*)F("")); - oappend(s2buf); - oappend((const char*)F("")); - - if (request != nullptr) request->send(200, "text/html", obuf); -} - -//append a numeric setting to string buffer -void sappend(char stype, const char* key, int val) -{ - char ds[] = "d.Sf."; - - switch(stype) - { - case 'c': //checkbox - oappend(ds); - oappend(key); - oappend(".checked="); - oappendi(val); - oappend(";"); - break; - case 'v': //numeric - oappend(ds); - oappend(key); - oappend(".value="); - oappendi(val); - oappend(";"); - break; - case 'i': //selectedIndex - oappend(ds); - oappend(key); - oappend(".selectedIndex="); - oappendi(val); - oappend(";"); - break; +void extractPin(JsonObject &obj, const char *key) { + if (obj[key].is()) { + JsonArray pins = obj[key].as(); + for (JsonVariant pv : pins) { + if (pv.as() > -1) { oappend(","); oappendi(pv.as()); } + } + } else { + if (obj[key].as() > -1) { oappend(","); oappendi(obj[key].as()); } } } -//append a string setting to buffer -void sappends(char stype, const char* key, char* val) +// oappend used pins by scanning JsonObject (1 level deep) +void fillUMPins(JsonObject &mods) { - switch(stype) - { - case 's': //string (we can interpret val as char*) - oappend("d.Sf."); - oappend(key); - oappend(".value=\""); - oappend(val); - oappend("\";"); - break; - case 'm': //message - oappend("d.getElementsByClassName"); - oappend(key); - oappend(".innerHTML=\""); - oappend(val); - oappend("\";"); - break; + for (JsonPair kv : mods) { + // kv.key() is usermod name or subobject key + // kv.value() is object itself + JsonObject obj = kv.value(); + if (!obj.isNull()) { + // element is an JsonObject + if (!obj["pin"].isNull()) { + extractPin(obj, "pin"); + } else { + // scan keys (just one level deep as is possible with usermods) + for (JsonPair so : obj) { + const char *key = so.key().c_str(); + if (strstr(key, "pin")) { + // we found a key containing "pin" substring + if (strlen(strstr(key, "pin")) == 3) { + // and it is at the end, we found another pin + extractPin(obj, key); + continue; + } + } + if (!obj[so.key()].is()) continue; + JsonObject subObj = obj[so.key()]; + if (!subObj["pin"].isNull()) { + // get pins from subobject + extractPin(subObj, "pin"); + } + } + } + } } } +void appendGPIOinfo() { + char nS[8]; + + oappend(SET_F("d.um_p=[-1")); // has to have 1 element + if (i2c_sda > -1 && i2c_scl > -1) { + oappend(","); oappend(itoa(i2c_sda,nS,10)); + oappend(","); oappend(itoa(i2c_scl,nS,10)); + } + if (spi_mosi > -1 && spi_sclk > -1) { + oappend(","); oappend(itoa(spi_mosi,nS,10)); + oappend(","); oappend(itoa(spi_sclk,nS,10)); + } + // usermod pin reservations will become unnecessary when settings pages will read cfg.json directly + if (requestJSONBufferLock(6)) { + // if we can't allocate JSON buffer ignore usermod pins + JsonObject mods = doc.createNestedObject(F("um")); + usermods.addToConfig(mods); + if (!mods.isNull()) fillUMPins(mods); + releaseJSONBufferLock(); + } + oappend(SET_F("];")); + + // add reserved and usermod pins as d.um_p array + #if defined(CONFIG_IDF_TARGET_ESP32S2) + oappend(SET_F("d.rsvd=[22,23,24,25,26,27,28,29,30,31,32")); + #elif defined(CONFIG_IDF_TARGET_ESP32S3) + oappend(SET_F("d.rsvd=[19,20,22,23,24,25,26,27,28,29,30,31,32")); // includes 19+20 for USB OTG (JTAG) + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + oappend(SET_F("d.rsvd=[11,12,13,14,15,16,17")); + #elif defined(ESP32) + oappend(SET_F("d.rsvd=[6,7,8,9,10,11,24,28,29,30,31,37,38")); + #else + oappend(SET_F("d.rsvd=[6,7,8,9,10,11")); + #endif + + #ifdef WLED_ENABLE_DMX + oappend(SET_F(",2")); // DMX hardcoded pin + #endif + + #ifdef WLED_DEBUG + oappend(SET_F(",")); oappend(itoa(hardwareTX,nS,10));// debug output (TX) pin + #endif + + //Note: Using pin 3 (RX) disables Adalight / Serial JSON + + #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if (psramFound()) oappend(SET_F(",16,17")); // GPIO16 & GPIO17 reserved for SPI RAM on ESP32 (not on S2, S3 or C3) + #elif defined(CONFIG_IDF_TARGET_ESP32S3) + if (psramFound()) oappend(SET_F(",33,34,35,36,37")); // in use for "octal" PSRAM or "octal" FLASH -seems that octal PSRAM is very common on S3. + #endif + #endif + + #ifdef WLED_USE_ETHERNET + if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { + for (uint8_t p=0; p=0) { oappend(","); oappend(itoa(ethernetBoards[ethernetType].eth_power,nS,10)); } + if (ethernetBoards[ethernetType].eth_mdc>=0) { oappend(","); oappend(itoa(ethernetBoards[ethernetType].eth_mdc,nS,10)); } + if (ethernetBoards[ethernetType].eth_mdio>=0) { oappend(","); oappend(itoa(ethernetBoards[ethernetType].eth_mdio,nS,10)); } + switch (ethernetBoards[ethernetType].eth_clk_mode) { + case ETH_CLOCK_GPIO0_IN: + case ETH_CLOCK_GPIO0_OUT: + oappend(SET_F(",0")); + break; + case ETH_CLOCK_GPIO16_OUT: + oappend(SET_F(",16")); + break; + case ETH_CLOCK_GPIO17_OUT: + oappend(SET_F(",17")); + break; + } + } + #endif + + oappend(SET_F("];")); + + // add info for read-only GPIO + oappend(SET_F("d.ro_gpio=[")); + #if defined(CONFIG_IDF_TARGET_ESP32S2) + oappendi(46); + #elif defined(CONFIG_IDF_TARGET_ESP32S3) + // none for S3 + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + // none for C3 + #elif defined(ESP32) + oappend(SET_F("34,35,36,37,38,39")); + #else + // none for ESP8266 + #endif + oappend(SET_F("];")); + + // add info about max. # of pins + oappend(SET_F("d.max_gpio=")); + #if defined(CONFIG_IDF_TARGET_ESP32S2) + oappendi(46); + #elif defined(CONFIG_IDF_TARGET_ESP32S3) + oappendi(48); + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + oappendi(21); + #elif defined(ESP32) + oappendi(39); + #else + oappendi(16); + #endif + oappend(SET_F(";")); +} //get values for settings form in javascript void getSettingsJS(byte subPage, char* dest) { //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec - DEBUG_PRINT("settings resp"); + DEBUG_PRINT(F("settings resp")); DEBUG_PRINTLN(subPage); obuf = dest; olen = 0; - if (subPage <1 || subPage >7) return; + if (subPage <0 || subPage >10) return; - if (subPage == 1) { - sappends('s',"CS",clientSSID); + if (subPage == SUBPAGE_MENU) + { + #ifndef WLED_DISABLE_2D // include only if 2D is compiled in + oappend(PSTR("gId('2dbtn').style.display='';")); + #endif + #ifdef WLED_ENABLE_DMX // include only if DMX is enabled + oappend(PSTR("gId('dmxbtn').style.display='';")); + #endif + } + + if (subPage == SUBPAGE_WIFI) + { + sappends('s',SET_F("CS"),clientSSID); byte l = strlen(clientPass); char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - sappends('s',"CP",fpass); + sappends('s',SET_F("CP"),fpass); char k[3]; k[2] = 0; //IP addresses for (int i = 0; i<4; i++) @@ -230,30 +265,48 @@ void getSettingsJS(byte subPage, char* dest) k[0] = 'S'; sappend('v',k,staticSubnet[i]); } - sappends('s',"CM",cmDNS); - sappend('i',"AB",apBehavior); - sappends('s',"AS",apSSID); - sappend('c',"AH",apHide); + sappends('s',SET_F("CM"),cmDNS); + sappend('i',SET_F("AB"),apBehavior); + sappends('s',SET_F("AS"),apSSID); + sappend('c',SET_F("AH"),apHide); l = strlen(apPass); char fapass[l+1]; //fill password field with *** fapass[l] = 0; memset(fapass,'*',l); - sappends('s',"AP",fapass); + sappends('s',SET_F("AP"),fapass); - sappend('v',"AC",apChannel); - sappend('c',"WS",noWifiSleep); + sappend('v',SET_F("AC"),apChannel); + sappend('c',SET_F("WS"),noWifiSleep); + #ifndef WLED_DISABLE_ESPNOW + sappend('c',SET_F("RE"),enable_espnow_remote); + sappends('s',SET_F("RMAC"),linked_remote); + #else + //hide remote settings if not compiled + oappend(SET_F("document.getElementById('remd').style.display='none';")); + #endif - if (WiFi.localIP()[0] != 0) //is connected + #ifdef WLED_USE_ETHERNET + sappend('v',SET_F("ETH"),ethernetType); + #else + //hide ethernet setting if not compiled in + oappend(SET_F("document.getElementById('ethd').style.display='none';")); + #endif + + if (Network.isConnected()) //is connected { - char s[16]; - IPAddress localIP = WiFi.localIP(); + char s[32]; + IPAddress localIP = Network.localIP(); sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); - sappends('m',"(\"sip\")[0]",s); + + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) + if (Network.isEthernet()) strcat_P(s ,SET_F(" (Ethernet)")); + #endif + sappends('m',SET_F("(\"sip\")[0]"),s); } else { - sappends('m',"(\"sip\")[0]","Not connected"); + sappends('m',SET_F("(\"sip\")[0]"),(char*)F("Not connected")); } if (WiFi.softAPIP()[0] != 0) //is active @@ -261,224 +314,444 @@ void getSettingsJS(byte subPage, char* dest) char s[16]; IPAddress apIP = WiFi.softAPIP(); sprintf(s, "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]); - sappends('m',"(\"sip\")[1]",s); + sappends('m',SET_F("(\"sip\")[1]"),s); } else { - sappends('m',"(\"sip\")[1]","Not active"); + sappends('m',SET_F("(\"sip\")[1]"),(char*)F("Not active")); } + + #ifndef WLED_DISABLE_ESPNOW + if (last_signal_src[0] != 0) //Have seen an ESP-NOW Remote + { + sappends('m',SET_F("(\"rlid\")[0]"),last_signal_src); + } else if (!enable_espnow_remote) + { + sappends('m',SET_F("(\"rlid\")[0]"),(char*)F("(Enable remote to listen)")); + } else + { + sappends('m',SET_F("(\"rlid\")[0]"),(char*)F("None")); + } + #endif } - if (subPage == 2) { - #ifdef ESP8266 - #if LEDPIN == 3 - oappend("d.Sf.LC.max=500;"); - #endif - #endif - sappend('v',"LC",ledCount); - sappend('v',"MA",strip.ablMilliampsMax); - sappend('v',"LA",strip.milliampsPerLed); + if (subPage == SUBPAGE_LEDS) + { + char nS[32]; + + appendGPIOinfo(); + + // set limits + oappend(SET_F("bLimits(")); + oappend(itoa(WLED_MAX_BUSSES,nS,10)); oappend(","); + oappend(itoa(WLED_MIN_VIRTUAL_BUSSES,nS,10)); oappend(","); + oappend(itoa(MAX_LEDS_PER_BUS,nS,10)); oappend(","); + oappend(itoa(MAX_LED_MEMORY,nS,10)); oappend(","); + oappend(itoa(MAX_LEDS,nS,10)); + oappend(SET_F(");")); + + sappend('c',SET_F("MS"),autoSegments); + sappend('c',SET_F("CCT"),correctWB); + sappend('c',SET_F("CR"),cctFromRgb); + sappend('v',SET_F("CB"),strip.cctBlending); + sappend('v',SET_F("FR"),strip.getTargetFps()); + sappend('v',SET_F("AW"),Bus::getGlobalAWMode()); + sappend('c',SET_F("LD"),useGlobalLedBuffer); + + for (uint8_t s=0; s < busses.getNumBusses(); s++) { + Bus* bus = busses.getBus(s); + if (bus == nullptr) continue; + char lp[4] = "L0"; lp[2] = 48+s; lp[3] = 0; //ascii 0-9 //strip data pin + char lc[4] = "LC"; lc[2] = 48+s; lc[3] = 0; //strip length + char co[4] = "CO"; co[2] = 48+s; co[3] = 0; //strip color order + char lt[4] = "LT"; lt[2] = 48+s; lt[3] = 0; //strip type + char ls[4] = "LS"; ls[2] = 48+s; ls[3] = 0; //strip start LED + char cv[4] = "CV"; cv[2] = 48+s; cv[3] = 0; //strip reverse + char sl[4] = "SL"; sl[2] = 48+s; sl[3] = 0; //skip 1st LED + char rf[4] = "RF"; rf[2] = 48+s; rf[3] = 0; //off refresh + char aw[4] = "AW"; aw[2] = 48+s; aw[3] = 0; //auto white mode + char wo[4] = "WO"; wo[2] = 48+s; wo[3] = 0; //swap channels + char sp[4] = "SP"; sp[2] = 48+s; sp[3] = 0; //bus clock speed + oappend(SET_F("addLEDs(1);")); + uint8_t pins[5]; + uint8_t nPins = bus->getPins(pins); + for (uint8_t i = 0; i < nPins; i++) { + lp[1] = 48+i; + if (pinManager.isPinOk(pins[i]) || bus->getType()>=TYPE_NET_DDP_RGB) sappend('v',lp,pins[i]); + } + sappend('v',lc,bus->getLength()); + sappend('v',lt,bus->getType()); + sappend('v',co,bus->getColorOrder() & 0x0F); + sappend('v',ls,bus->getStart()); + sappend('c',cv,bus->isReversed()); + sappend('v',sl,bus->skippedLeds()); + sappend('c',rf,bus->isOffRefreshRequired()); + sappend('v',aw,bus->getAutoWhiteMode()); + sappend('v',wo,bus->getColorOrder() >> 4); + uint16_t speed = bus->getFrequency(); + if (bus->getType() > TYPE_ONOFF && bus->getType() < 48) { + switch (speed) { + case WLED_PWM_FREQ/3 : speed = 0; break; + case WLED_PWM_FREQ/2 : speed = 1; break; + default: + case WLED_PWM_FREQ : speed = 2; break; + case WLED_PWM_FREQ*2 : speed = 3; break; + case WLED_PWM_FREQ*3 : speed = 4; break; + } + } else { + switch (speed) { + case 1000 : speed = 0; break; + case 2000 : speed = 1; break; + default: + case 5000 : speed = 2; break; + case 10000 : speed = 3; break; + case 20000 : speed = 4; break; + } + } + sappend('v',sp,speed); + } + sappend('v',SET_F("MA"),strip.ablMilliampsMax); + sappend('v',SET_F("LA"),strip.milliampsPerLed); if (strip.currentMilliamps) { - sappends('m',"(\"pow\")[0]",""); + sappends('m',SET_F("(\"pow\")[0]"),(char*)""); olen -= 2; //delete "; oappendi(strip.currentMilliamps); - oappend("mA\";"); + oappend(SET_F("mA\";")); } - sappend('v',"CA",briS); - sappend('c',"EW",useRGBW); - sappend('i',"CO",strip.colorOrder); - sappend('v',"AW",strip.rgbwMode); + oappend(SET_F("resetCOM(")); + oappend(itoa(WLED_MAX_COLOR_ORDER_MAPPINGS,nS,10)); + oappend(SET_F(");")); + const ColorOrderMap& com = busses.getColorOrderMap(); + for (uint8_t s=0; s < com.count(); s++) { + const ColorOrderMapEntry* entry = com.get(s); + if (entry == nullptr) break; + oappend(SET_F("addCOM(")); + oappend(itoa(entry->start,nS,10)); oappend(","); + oappend(itoa(entry->len,nS,10)); oappend(","); + oappend(itoa(entry->colorOrder,nS,10)); oappend(");"); + } - sappend('c',"BO",turnOnAtBoot); - sappend('v',"BP",bootPreset); + sappend('v',SET_F("CA"),briS); - sappend('c',"GB",strip.gammaCorrectBri); - sappend('c',"GC",strip.gammaCorrectCol); - sappend('c',"TF",fadeTransition); - sappend('v',"TD",transitionDelayDefault); - sappend('c',"PF",strip.paletteFade); - sappend('v',"BF",briMultiplier); - sappend('v',"TB",nightlightTargetBri); - sappend('v',"TL",nightlightDelayMinsDefault); - sappend('v',"TW",nightlightMode); - sappend('i',"PB",strip.paletteBlend); - sappend('c',"RV",strip.reverseMode); - sappend('c',"SL",skipFirstLed); + sappend('c',SET_F("BO"),turnOnAtBoot); + sappend('v',SET_F("BP"),bootPreset); + + sappend('c',SET_F("GB"),gammaCorrectBri); + sappend('c',SET_F("GC"),gammaCorrectCol); + dtostrf(gammaCorrectVal,3,1,nS); sappends('s',SET_F("GV"),nS); + sappend('c',SET_F("TF"),fadeTransition); + sappend('v',SET_F("TD"),transitionDelayDefault); + sappend('c',SET_F("PF"),strip.paletteFade); + sappend('v',SET_F("TP"),randomPaletteChangeTime); + sappend('v',SET_F("BF"),briMultiplier); + sappend('v',SET_F("TB"),nightlightTargetBri); + sappend('v',SET_F("TL"),nightlightDelayMinsDefault); + sappend('v',SET_F("TW"),nightlightMode); + sappend('i',SET_F("PB"),strip.paletteBlend); + sappend('v',SET_F("RL"),rlyPin); + sappend('c',SET_F("RM"),rlyMde); + for (uint8_t i=0; i> 4) & 0x0F); + k[0] = 'P'; sappend('v',k,timerMonth[i] & 0x0F); + k[0] = 'D'; sappend('v',k,timerDay[i]); + k[0] = 'E'; sappend('v',k,timerDayEnd[i]); + } } } - if (subPage == 6) + if (subPage == SUBPAGE_SEC) { - sappend('c',"NO",otaLock); - sappend('c',"OW",wifiLock); - sappend('c',"AO",aOtaEnabled); - sappends('m',"(\"sip\")[0]","WLED "); + byte l = strlen(settingsPIN); + char fpass[l+1]; //fill PIN field with 0000 + fpass[l] = 0; + memset(fpass,'0',l); + sappends('s',SET_F("PIN"),fpass); + sappend('c',SET_F("NO"),otaLock); + sappend('c',SET_F("OW"),wifiLock); + sappend('c',SET_F("AO"),aOtaEnabled); + sappends('m',SET_F("(\"sip\")[0]"),(char*)F("WLED ")); olen -= 2; //delete "; oappend(versionString); - oappend(" (build "); + oappend(SET_F(" (build ")); oappendi(VERSION); - oappend(")\";"); + oappend(SET_F(")\";")); + oappend(SET_F("sd=\"")); + oappend(serverDescription); + oappend(SET_F("\";")); } - + #ifdef WLED_ENABLE_DMX // include only if DMX is enabled - if (subPage == 7) + if (subPage == SUBPAGE_DMX) { - sappend('v',"PU",e131ProxyUniverse); - - sappend('v',"CN",DMXChannels); - sappend('v',"CG",DMXGap); - sappend('v',"CS",DMXStart); - sappend('v',"SL",DMXStartLED); - - sappend('i',"CH1",DMXFixtureMap[0]); - sappend('i',"CH2",DMXFixtureMap[1]); - sappend('i',"CH3",DMXFixtureMap[2]); - sappend('i',"CH4",DMXFixtureMap[3]); - sappend('i',"CH5",DMXFixtureMap[4]); - sappend('i',"CH6",DMXFixtureMap[5]); - sappend('i',"CH7",DMXFixtureMap[6]); - sappend('i',"CH8",DMXFixtureMap[7]); - sappend('i',"CH9",DMXFixtureMap[8]); - sappend('i',"CH10",DMXFixtureMap[9]); - sappend('i',"CH11",DMXFixtureMap[10]); - sappend('i',"CH12",DMXFixtureMap[11]); - sappend('i',"CH13",DMXFixtureMap[12]); - sappend('i',"CH14",DMXFixtureMap[13]); - sappend('i',"CH15",DMXFixtureMap[14]); - } + sappend('v',SET_F("PU"),e131ProxyUniverse); + + sappend('v',SET_F("CN"),DMXChannels); + sappend('v',SET_F("CG"),DMXGap); + sappend('v',SET_F("CS"),DMXStart); + sappend('v',SET_F("SL"),DMXStartLED); + + sappend('i',SET_F("CH1"),DMXFixtureMap[0]); + sappend('i',SET_F("CH2"),DMXFixtureMap[1]); + sappend('i',SET_F("CH3"),DMXFixtureMap[2]); + sappend('i',SET_F("CH4"),DMXFixtureMap[3]); + sappend('i',SET_F("CH5"),DMXFixtureMap[4]); + sappend('i',SET_F("CH6"),DMXFixtureMap[5]); + sappend('i',SET_F("CH7"),DMXFixtureMap[6]); + sappend('i',SET_F("CH8"),DMXFixtureMap[7]); + sappend('i',SET_F("CH9"),DMXFixtureMap[8]); + sappend('i',SET_F("CH10"),DMXFixtureMap[9]); + sappend('i',SET_F("CH11"),DMXFixtureMap[10]); + sappend('i',SET_F("CH12"),DMXFixtureMap[11]); + sappend('i',SET_F("CH13"),DMXFixtureMap[12]); + sappend('i',SET_F("CH14"),DMXFixtureMap[13]); + sappend('i',SET_F("CH15"),DMXFixtureMap[14]); + } #endif - oappend("}"); + + if (subPage == SUBPAGE_UM) //usermods + { + appendGPIOinfo(); + oappend(SET_F("numM=")); + oappendi(usermods.getModCount()); + oappend(";"); + sappend('v',SET_F("SDA"),i2c_sda); + sappend('v',SET_F("SCL"),i2c_scl); + sappend('v',SET_F("MOSI"),spi_mosi); + sappend('v',SET_F("MISO"),spi_miso); + sappend('v',SET_F("SCLK"),spi_sclk); + oappend(SET_F("addInfo('SDA','")); oappendi(HW_PIN_SDA); oappend(SET_F("');")); + oappend(SET_F("addInfo('SCL','")); oappendi(HW_PIN_SCL); oappend(SET_F("');")); + oappend(SET_F("addInfo('MOSI','")); oappendi(HW_PIN_DATASPI); oappend(SET_F("');")); + oappend(SET_F("addInfo('MISO','")); oappendi(HW_PIN_MISOSPI); oappend(SET_F("');")); + oappend(SET_F("addInfo('SCLK','")); oappendi(HW_PIN_CLOCKSPI); oappend(SET_F("');")); + usermods.appendConfigData(); + } + + 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