diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d09c8a605a..1340da91c2 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -2,12 +2,7 @@ # [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 +FROM mcr.microsoft.com/devcontainers/python:0-${VARIANT} # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. # COPY requirements.txt /tmp/pip-tmp/ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2a8e4712d0..5886418245 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,10 +5,7 @@ "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/*" + "VARIANT": "3" } }, @@ -27,34 +24,35 @@ // risk to running the build directly on the host. // "runArgs": ["--privileged", "-v", "/dev/bus/usb:/dev/bus/usb", "--group-add", "dialout"], - // 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" + "customizations": { + "vscode": { + "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" + }, + "extensions": [ + "ms-python.python", + "platformio.platformio-ide" + ] + } }, - // 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", + "postCreateCommand": "bash -i -c 'nvm install && npm ci'", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 6f5a5be862..b9f43cce22 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -github: [Aircoookie,blazoncek] +github: [DedeHai,lost-hope,willmmiles,netmindz,softhack007] custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek'] diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 285ad419e4..56b63d4386 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -44,7 +44,10 @@ body: id: version attributes: label: What version of WLED? - description: You can find this in by going to Config -> Security & Updates -> Scroll to Bottom. Copy and paste the entire line after "Server message" + description: |- + Find this by going to ⚙️ ConfigSecurity & Updates → Scroll to Bottom. + Copy and paste the rest of the line that begins “Installed version: ”, + or, for older versions, the entire line after “Server message”. placeholder: "e.g. WLED 0.13.1 (build 2203150)" validations: required: true @@ -60,6 +63,8 @@ body: - ESP32-S2 - ESP32-C3 - Other + - ESP32-C6 (experimental) + - ESP32-C5 (experimental) validations: required: true - type: textarea @@ -80,7 +85,7 @@ body: 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) + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/wled-dev/WLED/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..bc1f9761a9 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,172 @@ +# WLED - ESP32/ESP8266 LED Controller Firmware + +WLED is a fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs and SPI-based chipsets. The project consists of C++ firmware for microcontrollers and a modern web interface. + +Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. + +## Working Effectively + +### Initial Setup +- Install Node.js 20+ (specified in `.nvmrc`): Check your version with `node --version` +- Install dependencies: `npm ci` (takes ~5 seconds) +- Install PlatformIO for hardware builds: `pip install -r requirements.txt` (takes ~60 seconds) + +### Build and Test Workflow +- **ALWAYS build web UI first**: `npm run build` -- takes 3 seconds. NEVER CANCEL. +- **Run tests**: `npm test` -- takes 40 seconds. NEVER CANCEL. Set timeout to 2+ minutes. +- **Development mode**: `npm run dev` -- monitors file changes and auto-rebuilds web UI +- **Hardware firmware build**: `pio run -e [environment]` -- takes 15+ minutes. NEVER CANCEL. Set timeout to 30+ minutes. + +### Build Process Details +The build has two main phases: +1. **Web UI Generation** (`npm run build`): + - Processes files in `wled00/data/` (HTML, CSS, JS) + - Minifies and compresses web content + - Generates `wled00/html_*.h` files with embedded web content + - **CRITICAL**: Must be done before any hardware build + +2. **Hardware Compilation** (`pio run`): + - Compiles C++ firmware for various ESP32/ESP8266 targets + - Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m` + - List all targets: `pio run --list-targets` + +## Before Finishing Work + +**CRITICAL: You MUST complete ALL of these steps before marking your work as complete:** + +1. **Run the test suite**: `npm test` -- Set timeout to 2+ minutes. NEVER CANCEL. + - All tests MUST pass + - If tests fail, fix the issue before proceeding + +2. **Build at least one hardware environment**: `pio run -e esp32dev` -- Set timeout to 30+ minutes. NEVER CANCEL. + - Choose `esp32dev` as it's a common, representative environment + - See "Hardware Compilation" section above for the full list of common environments + - The build MUST complete successfully without errors + - If the build fails, fix the issue before proceeding + - **DO NOT skip this step** - it validates that firmware compiles with your changes + +3. **For web UI changes only**: Manually test the interface + - See "Manual Testing Scenarios" section below + - Verify the UI loads and functions correctly + +**If any of these validation steps fail, you MUST fix the issues before finishing. Do NOT mark work as complete with failing builds or tests.** + +## Validation and Testing + +### Web UI Testing +- **ALWAYS validate web UI changes manually**: + - Start local server: `cd wled00/data && python3 -m http.server 8080` + - Open `http://localhost:8080/index.htm` in browser + - Test basic functionality: color picker, effects, settings pages +- **Check for JavaScript errors** in browser console + +### Code Validation +- **No automated linting configured** - follow existing code style in files you edit +- **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files +- **C++ formatting available**: `clang-format` is installed but not in CI +- **Always run tests before finishing**: `npm test` +- **MANDATORY: Always run a hardware build before finishing** (see "Before Finishing Work" section below) + +### Manual Testing Scenarios +After making changes to web UI, always test: +- **Load main interface**: Verify index.htm loads without errors +- **Navigation**: Test switching between main page and settings pages +- **Color controls**: Verify color picker and brightness controls work +- **Effects**: Test effect selection and parameter changes +- **Settings**: Test form submission and validation + +## Common Tasks + +### Repository Structure +``` +wled00/ # Main firmware source (C++) + ├── data/ # Web interface files + │ ├── index.htm # Main UI + │ ├── settings*.htm # Settings pages + │ └── *.js/*.css # Frontend resources + ├── *.cpp/*.h # Firmware source files + └── html_*.h # Generated embedded web files (DO NOT EDIT) +tools/ # Build tools (Node.js) + ├── cdata.js # Web UI build script + └── cdata-test.js # Test suite +platformio.ini # Hardware build configuration +package.json # Node.js dependencies and scripts +.github/workflows/ # CI/CD pipelines +``` + +### Key Files and Their Purpose +- `wled00/data/index.htm` - Main web interface +- `wled00/data/settings*.htm` - Configuration pages +- `tools/cdata.js` - Converts web files to C++ headers +- `wled00/wled.h` - Main firmware configuration +- `platformio.ini` - Hardware build targets and settings + +### Development Workflow +1. **For web UI changes**: + - Edit files in `wled00/data/` + - Run `npm run build` to regenerate headers + - Test with local HTTP server + - Run `npm test` to validate build system + +2. **For firmware changes**: + - Edit files in `wled00/` (but NOT `html_*.h` files) + - Ensure web UI is built first (`npm run build`) + - Build firmware: `pio run -e [target]` + - Flash to device: `pio run -e [target] --target upload` + +3. **For both web and firmware**: + - Always build web UI first + - Test web interface manually + - Build and test firmware if making firmware changes + +## Build Timing and Timeouts + +**IMPORTANT: Use these timeout values when running builds:** + +- **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum +- **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum +- **Hardware builds** (`pio run -e [target]`): 15-20 minutes typical for first build - Set timeout to 1800 seconds (30 minutes) minimum + - Subsequent builds are faster due to caching + - First builds download toolchains and dependencies which takes significant time +- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation require patience + +**When validating your changes before finishing, you MUST wait for the hardware build to complete successfully. Set the timeout appropriately and be patient.** + +## Troubleshooting + +### Common Issues +- **Build fails with missing html_*.h**: Run `npm run build` first +- **Web UI looks broken**: Check browser console for JavaScript errors +- **PlatformIO network errors**: Try again, downloads can be flaky +- **Node.js version issues**: Ensure Node.js 20+ is installed (check `.nvmrc`) + +### When Things Go Wrong +- **Clear generated files**: `rm -f wled00/html_*.h` then rebuild +- **Force web UI rebuild**: `npm run build -- --force` or `npm run build -- -f` +- **Clean PlatformIO cache**: `pio run --target clean` +- **Reinstall dependencies**: `rm -rf node_modules && npm install` + +## Important Notes + +- **DO NOT edit `wled00/html_*.h` files** - they are auto-generated +- **Always commit both source files AND generated html_*.h files** +- **Web UI must be built before firmware compilation** +- **Test web interface manually after any web UI changes** +- **Use VS Code with PlatformIO extension for best development experience** +- **Hardware builds require appropriate ESP32/ESP8266 development board** + +## CI/CD Pipeline + +**The GitHub Actions CI workflow will:** +1. Installs Node.js and Python dependencies +2. Runs `npm test` to validate build system (MUST pass) +3. Builds web UI with `npm run build` (automatically run by PlatformIO) +4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all) +5. Uploads build artifacts + +**To ensure CI success, you MUST locally:** +- Run `npm test` and ensure it passes +- Run `pio run -e esp32dev` (or another common environment from "Hardware Compilation" section) and ensure it completes successfully +- If either fails locally, it WILL fail in CI + +**Match this workflow in your local development to ensure CI success. Do not mark work complete until you have validated builds locally.** diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5fdfc5a3e..f0d8537035 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: build: - name: Build Enviornments + name: Build Environments runs-on: ubuntu-latest needs: get_default_envs strategy: @@ -38,8 +38,12 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: + node-version-file: '.nvmrc' cache: 'npm' - - run: npm ci + - run: | + npm ci + VERSION=`date +%y%m%d0` + sed -i -r -e "s/define VERSION .+/define VERSION $VERSION/" wled00/wled.h - name: Cache PlatformIO uses: actions/cache@v4 with: @@ -56,6 +60,7 @@ jobs: cache: 'pip' - name: Install PlatformIO run: pip install -r requirements.txt + - name: Build firmware run: pio run -e ${{ matrix.environment }} - uses: actions/upload-artifact@v4 @@ -74,7 +79,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - run: npm test diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000000..2d47aefe42 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,50 @@ + +name: Deploy Nightly +on: + # This can be used to automatically publish nightlies at UTC nighttime + schedule: + - cron: '0 2 * * *' # run at 2 AM UTC + # This can be used to allow manually triggering nightlies from the web interface + workflow_dispatch: + +jobs: + wled_build: + uses: ./.github/workflows/build.yml + nightly: + name: Deploy nightly + runs-on: ubuntu-latest + needs: wled_build + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + - name: Show Files + run: ls -la + - name: "✏️ Generate release changelog" + id: changelog + uses: janheinrichmerker/action-github-changelog-generator@v2.4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + sinceTag: v0.15.0 + output: CHANGELOG_NIGHTLY.md + # Exclude issues that were closed without resolution from changelog + excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned' + - name: Update Nightly Release + uses: andelf/nightly-release@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: nightly + name: 'Nightly Release $$' + prerelease: true + body_path: CHANGELOG_NIGHTLY.md + files: | + *.bin + *.bin.gz + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v3 + with: + repository: wled/WLED-WebInstaller + event-type: release-nightly + token: ${{ secrets.PAT_PUBLIC }} diff --git a/.github/workflows/pr-merge.yaml b/.github/workflows/pr-merge.yaml new file mode 100644 index 0000000000..1efc366cc5 --- /dev/null +++ b/.github/workflows/pr-merge.yaml @@ -0,0 +1,38 @@ + name: Notify Discord on PR Merge + on: + workflow_dispatch: + pull_request_target: + types: [closed] + + jobs: + notify: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + steps: + - name: Get User Permission + id: checkAccess + uses: actions-cool/check-user-permission@v2 + with: + require: write + username: ${{ github.triggering_actor }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Check User Permission + if: steps.checkAccess.outputs.require-result == 'false' + run: | + echo "${{ github.triggering_actor }} does not have permissions on this repo." + echo "Current permission level is ${{ steps.checkAccess.outputs.user-permission }}" + echo "Job originally triggered by ${{ github.actor }}" + exit 1 + - name: Send Discord notification + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + ACTOR: ${{ github.actor }} + run: | + jq -n \ + --arg content "Pull Request #${PR_NUMBER} \"${PR_TITLE}\" merged by ${ACTOR} + ${PR_URL} . It will be included in the next nightly builds, please test" \ + '{content: $content}' \ + | curl -H "Content-Type: application/json" -d @- ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 27beec99c3..59de4316cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,9 +18,19 @@ jobs: - uses: actions/download-artifact@v4 with: merge-multiple: true + - name: "✏️ Generate release changelog" + id: changelog + uses: janheinrichmerker/action-github-changelog-generator@v2.4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + sinceTag: v0.15.0 + maxIssues: 500 + # Exclude issues that were closed without resolution from changelog + excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned' - name: Create draft release uses: softprops/action-gh-release@v1 with: + body: ${{ steps.changelog.outputs.changelog }} draft: True files: | *.bin diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000000..a9b7aa9b68 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,13 @@ +on: + workflow_dispatch: + +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v3 + with: + repository: wled/WLED-WebInstaller + event-type: release-nightly + token: ${{ secrets.PAT_PUBLIC }} diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml new file mode 100644 index 0000000000..e8ab65066d --- /dev/null +++ b/.github/workflows/usermods.yml @@ -0,0 +1,74 @@ +name: Usermod CI + +on: + pull_request: + paths: + - usermods/** + +jobs: + + get_usermod_envs: + # Only run for pull requests from forks (not from branches within wled/WLED) + if: github.event.pull_request.head.repo.full_name != github.repository + name: Gather Usermods + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + - name: Install PlatformIO + run: pip install -r requirements.txt + - name: Get default environments + id: envs + run: | + echo "usermods=$(find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | jq -R | grep -v PWM_fan | grep -v BME68X_v2| grep -v pixels_dice_tray | jq --slurp -c)" >> $GITHUB_OUTPUT + outputs: + usermods: ${{ steps.envs.outputs.usermods }} + + + build: + # Only run for pull requests from forks (not from branches within wled/WLED) + if: github.event.pull_request.head.repo.full_name != github.repository + name: Build Enviornments + runs-on: ubuntu-latest + needs: get_usermod_envs + strategy: + fail-fast: false + matrix: + usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }} + environment: [usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermods_esp32s3] + steps: + - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + - run: npm ci + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: | + ~/.platformio/.cache + ~/.buildcache + build_output + key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} + restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}- + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + - name: Install PlatformIO + run: pip install -r requirements.txt + - name: Add usermods environment + run: | + cp -v usermods/platformio_override.usermods.ini platformio_override.ini + echo >> platformio_override.ini + echo "custom_usermods = ${{ matrix.usermod }}" >> platformio_override.ini + cat platformio_override.ini + + - name: Build firmware + run: pio run -e ${{ matrix.environment }} diff --git a/.gitignore b/.gitignore index 8f083e3f6a..62e72a9a0a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,12 @@ .pioenvs .piolibdeps .vscode +compile_commands.json +__pycache__/ + +/.dummy +/dependencies.lock +/managed_components esp01-update.sh platformio_override.ini @@ -15,6 +21,7 @@ wled-update.sh /build_output/ /node_modules/ +/logs/ /wled00/extLibs /wled00/LittleFS @@ -22,3 +29,4 @@ wled-update.sh /wled00/Release /wled00/wled00.ino.cpp /wled00/html_*.h +/wled00/js_*.h diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..10fef252a9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.18 diff --git a/CHANGELOG.md b/CHANGELOG.md index c570ac1f7e..f591fc2b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -173,7 +173,7 @@ - v0.15.0-b2 - WS2805 support (RGB + WW + CW, 600kbps) - Unified PSRAM use -- NeoPixelBus v2.7.9 +- NeoPixelBus v2.7.9 (for future WS2805 support) - Ubiquitous PSRAM mode for all variants of ESP32 - SSD1309_64 I2C Support for FLD Usermod (#3836 by @THATDONFC) - Palette cycling fix (add support for `{"seg":[{"pal":"X~Y~"}]}` or `{"seg":[{"pal":"X~Yr"}]}`) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06c221fca6..1cd77ecd14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,43 +1,177 @@ -## Thank you for making WLED better! +# Thank you for making WLED better! -Here are a few suggestions to make it easier for you to contribute! +WLED is a community-driven project, and every contribution matters! We appreciate your time and effort. -### Describe your PR +Our maintainers are here for two things: **helping you** improve your code, and **keeping WLED** lean, efficient, and maintainable. +We'll work with you to refine your contribution, but we'll also push back if something might create technical debt or add features without clear value. Don't take it personally - we're just protecting WLED's architecture while helping your contribution succeed! -Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing. +## Getting Started + +Here are a few suggestions to make it easier for you to contribute: + +### PR from a branch in your own fork +Start your pull request (PR) in a branch of your own fork. Don't make a PR directly from your main branch. +This lets you update your PR if needed, while you can work on other tasks in 'main' or in other branches. + +> [!TIP] +> **The easiest way to start your first PR** +> When viewing a file in `wled/WLED`, click on the "pen" icon and start making changes. +> When you choose to 'Commit changes', GitHub will automatically create a PR from your fork. +> +> image: fork and edit -A good description helps us to review and understand your proposed changes. For example, you could say a few words about -* what you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.) -* how your code works (short technical summary - focus on important aspects that might not be obvious when reading the code) -* testing you performed, known limitations, open ends you possibly could not solve. -* any areas where you like to get help from an experienced maintainer (yes WLED has become big 😉) ### Target branch for pull requests -Please make all PRs against the `0_15` branch. +Please make all PRs against the `main` branch. + +### Describing your PR + +Please add a description of your proposed code changes. +A PR with no description or just a few words might not get accepted, simply because very basic information is missing. +No need to write an essay! + +A good description helps us to review and understand your proposed changes. For example, you could say a few words about +* What you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.) +* How your code works (short technical summary - focus on important aspects that might not be obvious when reading the code) +* Testing you performed, known limitations, anything you couldn't quite solve. +* Let us know if you'd like guidance from a maintainer (WLED is a big project 😉) + +### Testing Your Changes + +Before submitting: + +- ✅ Does it compile? +- ✅ Does your feature/fix actually work? +- ✅ Did you break anything else? +- ✅ Tested on actual hardware if possible? + +Mention your testing in the PR description (e.g., "Tested on ESP32 + WS2812B"). + +## During Review + +We're all volunteers, so reviews can take some time (longer during busy times). +Don't worry - we haven't forgotten you! Feel free to ping after a week if there's no activity. ### Updating your code -While the PR is open - and under review by maintainers - you may be asked to modify your PR source code. -You can simply update your own branch, and push changes in response to reviewer recommendations. -Github will pick up the changes so your PR stays up-to-date. +While the PR is open, you can keep updating your branch - just push more commits! GitHub will automatically update your PR. + +You don't need to squash commits or clean up history - we'll handle that when merging. -> [!CAUTION] +> [!CAUTION] > Do not use "force-push" while your PR is open! -> It has many subtle and unexpected consequences on our github reposistory. -> For example, we regularly lost review comments when the PR author force-pushes code changes. So, pretty please, do not force-push. +> It has many subtle and unexpected consequences on our GitHub repository. +> For example, we regularly lose review comments when the PR author force-pushes code changes. Our review bot (coderabbit) may become unable to properly track changes, it gets confused or stops responding to questions. +> So, pretty please, do not force-push. + +> [!TIP] +> Use [cherry-picking](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop) to copy commits from one branch to another. + + +### Responding to Reviews + +When we ask for changes: + +- **Add new commits** - please don't amend or force-push +- **Reply in the PR** - let us know when you've addressed comments +- **Ask questions** - if something's unclear, just ask! +- **Be patient** - we're all volunteers here 😊 + +You can reference feedback in commit messages: +> ```text +> Fix naming per @Aircoookie's suggestion +> ``` + +### Dealing with Merge Conflicts + +Got conflicts with `main`? No worries - here's how to fix them: + +**Using GitHub Desktop** (easier for beginners): + +1. Click **Fetch origin**, then **Pull origin** +2. If conflicts exist, GitHub Desktop will warn you - click **View conflicts** +3. Open the conflicted files in your editor (VS Code, etc.) +4. Remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and keep the correct code +5. Save the files +6. Back in GitHub Desktop, commit the merge (it'll suggest a message) +7. Click **Push origin** +**Using command line**: -You can find a collection of very useful tips and tricks here: https://github.com/Aircoookie/WLED/wiki/How-to-properly-submit-a-PR + ```bash + git fetch origin + git merge origin/main + # Fix conflicts in your editor + git add . + git commit + git push + ``` +Either way works fine - pick what you're comfortable with! Merging is simpler than rebasing and keeps everything connected. + +#### When you MUST rebase (really rare!) + +Sometimes you might hit merge conflicts with `main` that are harder to solve. Here's what to try: + +1. **Merge instead of rebase** (safest option): + ```bash + git fetch origin + git merge origin/main + git push + ``` + Keeps review comments attached and CI results visible! + +2. **Use cherry-picking** to copy commits between branches without rewriting history - [here's how](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop). + +3. **If all else fails, use `--force-with-lease`** (not plain `--force`): + ```bash + git rebase origin/main + git push --force-with-lease + ``` + Then **leave a comment** explaining why you had to force-push, and be ready to re-address some feedback. + +### Additional Resources +Want to know more? Check out: +- 📚 [GitHub Desktop documentation](https://docs.github.com/en/desktop) - if you prefer GUI tools +- 🎓 [How to properly submit a PR](https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR) - detailed tips and tricks + + +## After Approval +Once approved, a maintainer will merge your PR (possibly squashing commits). +Your contribution will be in the next WLED release - thank you! 🎉 + + +## Coding Guidelines + +### Source Code from an AI agent or bot +> [!IMPORTANT] +> It's OK if you took help from an AI for writing your source code. +> +> AI tools can be very helpful, but as the contributor, **you're responsible for the code**. + +* Make sure you really understand the AI-generated code, don't just accept it because it "seems to work". +* Don't let the AI change existing code without double-checking by you as the contributor. Often, the result will not be complete. For example, previous source code comments may be lost. +* Remember that AI is still "Often-Wrong" ;-) +* If you don't feel confident using English, you can use AI for translating code comments and descriptions into English. AI bots are very good at understanding language. However, always check if the results are correct. The translation might still have wrong technical terms, or errors in some details. + +#### Best Practice with AI + +AI tools are powerful but "often wrong" - your judgment is essential! 😊 + +- ✅ **Understand the code** - As the person contributing to WLED, make sure you understand exactly what the AI-generated source code does +- ✅ **Review carefully** - AI can lose comments, introduce bugs, or make unnecessary changes +- ✅ **Be transparent** - Add a comment like `// This section was AI-generated` for larger chunks +- ✅ **Use AI for translation** - AI is great for translating comments to English (but verify technical terms!) ### Code style -When in doubt, it is easiest to replicate the code style you find in the files you want to edit :) -Below are the guidelines we use in the WLED repository. +Don't stress too much about style! When in doubt, just match the style in the files you're editing. 😊 + +Here are our main guidelines: #### Indentation -We use tabs for Indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files. +We use tabs for indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files. You are all set if you have enabled `Editor: Detect Indentation` in VS Code. #### Blocks @@ -55,7 +189,7 @@ if (a == b) { if (a == b) doStuff(a); ``` -Acceptable - however the first variant is usually easier to read: +Also acceptable (though the first style is usually easier to read): ```cpp if (a == b) { @@ -86,23 +220,25 @@ if( a==b ){ #### Comments Comments should have a space between the delimiting characters (e.g. `//`) and the comment text. -Note: This is a recent change, the majority of the codebase still has comments without spaces. +We're gradually adopting this style - don't worry if you see older code without spaces! Good: -``` -// This is a comment. - -/* This is a CSS inline comment */ +```cpp +// This is a short inline comment. /* - * This is a comment + * This is a longer comment * wrapping over multiple lines, * used in WLED for file headers and function explanations */ - +``` +```css +/* This is a CSS inline comment */ +``` +```html ``` There is no hard character limit for a comment within a line, though as a rule of thumb consider wrapping after 120 characters. -Inline comments are OK if they describe that line only and are not exceedingly wide. \ No newline at end of file +Inline comments are OK if they describe that line only and are not exceedingly wide. diff --git a/boards/adafruit_matrixportal_esp32s3_wled.json b/boards/adafruit_matrixportal_esp32s3_wled.json new file mode 100644 index 0000000000..3b487d0d4b --- /dev/null +++ b/boards/adafruit_matrixportal_esp32s3_wled.json @@ -0,0 +1,58 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DBOARD_HAS_PSRAM" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x239A", + "0x8125" + ], + [ + "0x239A", + "0x0125" + ], + [ + "0x239A", + "0x8126" + ] + ], + "mcu": "esp32s3", + "variant": "adafruit_matrixportal_esp32s3" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "Adafruit MatrixPortal ESP32-S3 for WLED", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.adafruit.com/product/5778", + "vendor": "Adafruit" +} diff --git a/boards/lilygo-t7-s3.json b/boards/lilygo-t7-s3.json new file mode 100644 index 0000000000..4bf071fc7e --- /dev/null +++ b/boards/lilygo-t7-s3.json @@ -0,0 +1,47 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_TTGO_T7_S3", + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0X303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi", + "bluetooth" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "LILYGO T3-S3", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.aliexpress.us/item/3256804591247074.html", + "vendor": "LILYGO" +} \ No newline at end of file diff --git a/boards/lolin_s3_mini.json b/boards/lolin_s3_mini.json new file mode 100644 index 0000000000..7f55f0bde2 --- /dev/null +++ b/boards/lolin_s3_mini.json @@ -0,0 +1,47 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_LOLIN_S3_MINI", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x8167" + ] + ], + "mcu": "esp32s3", + "variant": "lolin_s3_mini" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "WEMOS LOLIN S3 Mini", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.wemos.cc/en/latest/s3/index.html", + "vendor": "WEMOS" +} + \ No newline at end of file diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp new file mode 100644 index 0000000000..68cb9010ec --- /dev/null +++ b/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp @@ -0,0 +1,504 @@ +/* esp8266_waveform imported from platform source code + Modified for WLED to work around a fault in the NMI handling, + which can result in the system locking up and hard WDT crashes. + + Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_phase.cpp +*/ + + +/* + esp8266_waveform - General purpose waveform generation and control, + supporting outputs on all pins in parallel. + + Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. + Copyright (c) 2020 Dirk O. Kaar. + + The core idea is to have a programmable waveform generator with a unique + high and low period (defined in microseconds or CPU clock cycles). TIMER1 is + set to 1-shot mode and is always loaded with the time until the next edge + of any live waveforms. + + Up to one waveform generator per pin supported. + + Each waveform generator is synchronized to the ESP clock cycle counter, not the + timer. This allows for removing interrupt jitter and delay as the counter + always increments once per 80MHz clock. Changes to a waveform are + contiguous and only take effect on the next waveform transition, + allowing for smooth transitions. + + This replaces older tone(), analogWrite(), and the Servo classes. + + Everywhere in the code where "ccy" or "ccys" is used, it means ESP.getCycleCount() + clock cycle time, or an interval measured in clock cycles, but not TIMER1 + cycles (which may be 2 CPU clock cycles @ 160MHz). + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "core_esp8266_waveform.h" +#include +#include "debug.h" +#include "ets_sys.h" +#include + + +// ----- @willmmiles begin patch ----- +// Linker magic +extern "C" void usePWMFixedNMI(void) {}; + +// NMI crash workaround +// Sometimes the NMI fails to return, stalling the CPU. When this happens, +// the next NMI gets a return address /inside the NMI handler function/. +// We work around this by caching the last NMI return address, and restoring +// the epc3 and eps3 registers to the previous values if the observed epc3 +// happens to be pointing to the _NMILevelVector function. +extern "C" void _NMILevelVector(); +extern "C" void _UserExceptionVector_1(); // the next function after _NMILevelVector +static inline IRAM_ATTR void nmiCrashWorkaround() { + static uintptr_t epc3_backup, eps3_backup; + + uintptr_t epc3, eps3; + __asm__ __volatile__("rsr %0,epc3; rsr %1,eps3":"=a"(epc3),"=a" (eps3)); + if ((epc3 < (uintptr_t) &_NMILevelVector) || (epc3 >= (uintptr_t) &_UserExceptionVector_1)) { + // Address is good; save backup + epc3_backup = epc3; + eps3_backup = eps3; + } else { + // Address is inside the NMI handler -- restore from backup + __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); + } +} +// ----- @willmmiles end patch ----- + + +// No-op calls to override the PWM implementation +extern "C" void _setPWMFreq_weak(uint32_t freq) { (void) freq; } +extern "C" IRAM_ATTR bool _stopPWM_weak(int pin) { (void) pin; return false; } +extern "C" bool _setPWM_weak(int pin, uint32_t val, uint32_t range) { (void) pin; (void) val; (void) range; return false; } + + +// Timer is 80MHz fixed. 160MHz CPU frequency need scaling. +constexpr bool ISCPUFREQ160MHZ = clockCyclesPerMicrosecond() == 160; +// Maximum delay between IRQs, Timer1, <= 2^23 / 80MHz +constexpr int32_t MAXIRQTICKSCCYS = microsecondsToClockCycles(10000); +// Maximum servicing time for any single IRQ +constexpr uint32_t ISRTIMEOUTCCYS = microsecondsToClockCycles(18); +// The latency between in-ISR rearming of the timer and the earliest firing +constexpr int32_t IRQLATENCYCCYS = microsecondsToClockCycles(2); +// The SDK and hardware take some time to actually get to our NMI code +constexpr int32_t DELTAIRQCCYS = ISCPUFREQ160MHZ ? + microsecondsToClockCycles(2) >> 1 : microsecondsToClockCycles(2); + +// for INFINITE, the NMI proceeds on the waveform without expiry deadline. +// for EXPIRES, the NMI expires the waveform automatically on the expiry ccy. +// for UPDATEEXPIRY, the NMI recomputes the exact expiry ccy and transitions to EXPIRES. +// for UPDATEPHASE, the NMI recomputes the target timings +// for INIT, the NMI initializes nextPeriodCcy, and if expiryCcy != 0 includes UPDATEEXPIRY. +enum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, UPDATEPHASE = 3, INIT = 4}; + +// Waveform generator can create tones, PWM, and servos +typedef struct { + uint32_t nextPeriodCcy; // ESP clock cycle when a period begins. + uint32_t endDutyCcy; // ESP clock cycle when going from duty to off + int32_t dutyCcys; // Set next off cycle at low->high to maintain phase + int32_t adjDutyCcys; // Temporary correction for next period + int32_t periodCcys; // Set next phase cycle at low->high to maintain phase + uint32_t expiryCcy; // For time-limited waveform, the CPU clock cycle when this waveform must stop. If WaveformMode::UPDATE, temporarily holds relative ccy count + WaveformMode mode; + bool autoPwm; // perform PWM duty to idle cycle ratio correction under high load at the expense of precise timings +} Waveform; + +namespace { + + static struct { + Waveform pins[17]; // State of all possible pins + uint32_t states = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code + uint32_t enabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code + + // Enable lock-free by only allowing updates to waveform.states and waveform.enabled from IRQ service routine + int32_t toSetBits = 0; // Message to the NMI handler to start/modify exactly one waveform + int32_t toDisableBits = 0; // Message to the NMI handler to disable exactly one pin from waveform generation + + // toSetBits temporaries + // cheaper than packing them in every Waveform, since we permit only one use at a time + uint32_t phaseCcy; // positive phase offset ccy count + int8_t alignPhase; // < 0 no phase alignment, otherwise starts waveform in relative phase offset to given pin + + uint32_t(*timer1CB)() = nullptr; + + bool timer1Running = false; + + uint32_t nextEventCcy; + } waveform; + +} + +// Interrupt on/off control +static IRAM_ATTR void timer1Interrupt(); + +// Non-speed critical bits +#pragma GCC optimize ("Os") + +static void initTimer() { + timer1_disable(); + ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); + ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); + timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); + waveform.timer1Running = true; + timer1_write(IRQLATENCYCCYS); // Cause an interrupt post-haste +} + +static void IRAM_ATTR deinitTimer() { + ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); + timer1_disable(); + timer1_isr_init(); + waveform.timer1Running = false; +} + +extern "C" { + +// Set a callback. Pass in NULL to stop it +void setTimer1Callback_weak(uint32_t (*fn)()) { + waveform.timer1CB = fn; + std::atomic_thread_fence(std::memory_order_acq_rel); + if (!waveform.timer1Running && fn) { + initTimer(); + } else if (waveform.timer1Running && !fn && !waveform.enabled) { + deinitTimer(); + } +} + +// Start up a waveform on a pin, or change the current one. Will change to the new +// waveform smoothly on next low->high transition. For immediate change, stopWaveform() +// first, then it will immediately begin. +int startWaveformClockCycles_weak(uint8_t pin, uint32_t highCcys, uint32_t lowCcys, + uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm) { + uint32_t periodCcys = highCcys + lowCcys; + if (periodCcys < MAXIRQTICKSCCYS) { + if (!highCcys) { + periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; + } + else if (!lowCcys) { + highCcys = periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; + } + } + // sanity checks, including mixed signed/unsigned arithmetic safety + if ((pin > 16) || isFlashInterfacePin(pin) || (alignPhase > 16) || + static_cast(periodCcys) <= 0 || + static_cast(highCcys) < 0 || static_cast(lowCcys) < 0) { + return false; + } + Waveform& wave = waveform.pins[pin]; + wave.dutyCcys = highCcys; + wave.adjDutyCcys = 0; + wave.periodCcys = periodCcys; + wave.autoPwm = autoPwm; + waveform.alignPhase = (alignPhase < 0) ? -1 : alignPhase; + waveform.phaseCcy = phaseOffsetCcys; + + std::atomic_thread_fence(std::memory_order_acquire); + const uint32_t pinBit = 1UL << pin; + if (!(waveform.enabled & pinBit)) { + // wave.nextPeriodCcy and wave.endDutyCcy are initialized by the ISR + wave.expiryCcy = runTimeCcys; // in WaveformMode::INIT, temporarily hold relative cycle count + wave.mode = WaveformMode::INIT; + if (!wave.dutyCcys) { + // If initially at zero duty cycle, force GPIO off + if (pin == 16) { + GP16O = 0; + } + else { + GPOC = pinBit; + } + } + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + std::atomic_thread_fence(std::memory_order_release); + if (!waveform.timer1Running) { + initTimer(); + } + else if (T1V > IRQLATENCYCCYS) { + // Must not interfere if Timer is due shortly + timer1_write(IRQLATENCYCCYS); + } + } + else { + wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI + std::atomic_thread_fence(std::memory_order_release); + if (runTimeCcys) { + wave.expiryCcy = runTimeCcys; // in WaveformMode::UPDATEEXPIRY, temporarily hold relative cycle count + wave.mode = WaveformMode::UPDATEEXPIRY; + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + } else if (alignPhase >= 0) { + // @willmmiles new feature + wave.mode = WaveformMode::UPDATEPHASE; // recalculate start + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + } + } + std::atomic_thread_fence(std::memory_order_acq_rel); + while (waveform.toSetBits) { + esp_yield(); // Wait for waveform to update + std::atomic_thread_fence(std::memory_order_acquire); + } + return true; +} + +// Stops a waveform on a pin +IRAM_ATTR int stopWaveform_weak(uint8_t pin) { + // Can't possibly need to stop anything if there is no timer active + if (!waveform.timer1Running) { + return false; + } + // If user sends in a pin >16 but <32, this will always point to a 0 bit + // If they send >=32, then the shift will result in 0 and it will also return false + std::atomic_thread_fence(std::memory_order_acquire); + const uint32_t pinBit = 1UL << pin; + if (waveform.enabled & pinBit) { + waveform.toDisableBits = 1UL << pin; + std::atomic_thread_fence(std::memory_order_release); + // Must not interfere if Timer is due shortly + if (T1V > IRQLATENCYCCYS) { + timer1_write(IRQLATENCYCCYS); + } + while (waveform.toDisableBits) { + /* no-op */ // Can't delay() since stopWaveform may be called from an IRQ + std::atomic_thread_fence(std::memory_order_acquire); + } + } + if (!waveform.enabled && !waveform.timer1CB) { + deinitTimer(); + } + return true; +} + +}; + +// Speed critical bits +#pragma GCC optimize ("O2") + +// For dynamic CPU clock frequency switch in loop the scaling logic would have to be adapted. +// Using constexpr makes sure that the CPU clock frequency is compile-time fixed. +static inline IRAM_ATTR int32_t scaleCcys(const int32_t ccys, const bool isCPU2X) { + if (ISCPUFREQ160MHZ) { + return isCPU2X ? ccys : (ccys >> 1); + } + else { + return isCPU2X ? (ccys << 1) : ccys; + } +} + +static IRAM_ATTR void timer1Interrupt() { + const uint32_t isrStartCcy = ESP.getCycleCount(); + //int32_t clockDrift = isrStartCcy - waveform.nextEventCcy; + + // ----- @willmmiles begin patch ----- + nmiCrashWorkaround(); + // ----- @willmmiles end patch ----- + + const bool isCPU2X = CPU2X & 1; + if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) { + // Handle enable/disable requests from main app. + waveform.enabled = (waveform.enabled & ~waveform.toDisableBits) | waveform.toSetBits; // Set the requested waveforms on/off + // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) + waveform.toDisableBits = 0; + } + + if (waveform.toSetBits) { + const int toSetPin = __builtin_ffs(waveform.toSetBits) - 1; + Waveform& wave = waveform.pins[toSetPin]; + switch (wave.mode) { + case WaveformMode::INIT: + waveform.states &= ~waveform.toSetBits; // Clear the state of any just started + if (waveform.alignPhase >= 0 && waveform.enabled & (1UL << waveform.alignPhase)) { + wave.nextPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X); + } + else { + wave.nextPeriodCcy = waveform.nextEventCcy; + } + if (!wave.expiryCcy) { + wave.mode = WaveformMode::INFINITE; + break; + } + // fall through + case WaveformMode::UPDATEEXPIRY: + // in WaveformMode::UPDATEEXPIRY, expiryCcy temporarily holds relative CPU cycle count + wave.expiryCcy = wave.nextPeriodCcy + scaleCcys(wave.expiryCcy, isCPU2X); + wave.mode = WaveformMode::EXPIRES; + break; + // @willmmiles new feature + case WaveformMode::UPDATEPHASE: + // in WaveformMode::UPDATEPHASE, we recalculate the targets + if ((waveform.alignPhase >= 0) && (waveform.enabled & (1UL << waveform.alignPhase))) { + // Compute phase shift to realign with target + auto const newPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X); + auto const period = scaleCcys(wave.periodCcys, isCPU2X); + auto shift = ((static_cast (newPeriodCcy - wave.nextPeriodCcy) + period/2) % period) - (period/2); + wave.nextPeriodCcy += static_cast(shift); + if (static_cast(wave.endDutyCcy - wave.nextPeriodCcy) > 0) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + } + default: + break; + } + waveform.toSetBits = 0; + } + + // Exit the loop if the next event, if any, is sufficiently distant. + const uint32_t isrTimeoutCcy = isrStartCcy + ISRTIMEOUTCCYS; + uint32_t busyPins = waveform.enabled; + waveform.nextEventCcy = isrStartCcy + MAXIRQTICKSCCYS; + + uint32_t now = ESP.getCycleCount(); + uint32_t isrNextEventCcy = now; + while (busyPins) { + if (static_cast(isrNextEventCcy - now) > IRQLATENCYCCYS) { + waveform.nextEventCcy = isrNextEventCcy; + break; + } + isrNextEventCcy = waveform.nextEventCcy; + uint32_t loopPins = busyPins; + while (loopPins) { + const int pin = __builtin_ffsl(loopPins) - 1; + const uint32_t pinBit = 1UL << pin; + loopPins ^= pinBit; + + Waveform& wave = waveform.pins[pin]; + +/* @willmmiles - wtf? We don't want to accumulate drift + if (clockDrift) { + wave.endDutyCcy += clockDrift; + wave.nextPeriodCcy += clockDrift; + wave.expiryCcy += clockDrift; + } +*/ + + uint32_t waveNextEventCcy = (waveform.states & pinBit) ? wave.endDutyCcy : wave.nextPeriodCcy; + if (WaveformMode::EXPIRES == wave.mode && + static_cast(waveNextEventCcy - wave.expiryCcy) >= 0 && + static_cast(now - wave.expiryCcy) >= 0) { + // Disable any waveforms that are done + waveform.enabled ^= pinBit; + busyPins ^= pinBit; + } + else { + const int32_t overshootCcys = now - waveNextEventCcy; + if (overshootCcys >= 0) { + const int32_t periodCcys = scaleCcys(wave.periodCcys, isCPU2X); + if (waveform.states & pinBit) { + // active configuration and forward are 100% duty + if (wave.periodCcys == wave.dutyCcys) { + wave.nextPeriodCcy += periodCcys; + wave.endDutyCcy = wave.nextPeriodCcy; + } + else { + if (wave.autoPwm) { + wave.adjDutyCcys += overshootCcys; + } + waveform.states ^= pinBit; + if (16 == pin) { + GP16O = 0; + } + else { + GPOC = pinBit; + } + } + waveNextEventCcy = wave.nextPeriodCcy; + } + else { + wave.nextPeriodCcy += periodCcys; + if (!wave.dutyCcys) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + else { + int32_t dutyCcys = scaleCcys(wave.dutyCcys, isCPU2X); + if (dutyCcys <= wave.adjDutyCcys) { + dutyCcys >>= 1; + wave.adjDutyCcys -= dutyCcys; + } + else if (wave.adjDutyCcys) { + dutyCcys -= wave.adjDutyCcys; + wave.adjDutyCcys = 0; + } + wave.endDutyCcy = now + dutyCcys; + if (static_cast(wave.endDutyCcy - wave.nextPeriodCcy) > 0) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + waveform.states |= pinBit; + if (16 == pin) { + GP16O = 1; + } + else { + GPOS = pinBit; + } + } + waveNextEventCcy = wave.endDutyCcy; + } + + if (WaveformMode::EXPIRES == wave.mode && static_cast(waveNextEventCcy - wave.expiryCcy) > 0) { + waveNextEventCcy = wave.expiryCcy; + } + } + + if (static_cast(waveNextEventCcy - isrTimeoutCcy) >= 0) { + busyPins ^= pinBit; + if (static_cast(waveform.nextEventCcy - waveNextEventCcy) > 0) { + waveform.nextEventCcy = waveNextEventCcy; + } + } + else if (static_cast(isrNextEventCcy - waveNextEventCcy) > 0) { + isrNextEventCcy = waveNextEventCcy; + } + } + now = ESP.getCycleCount(); + } + //clockDrift = 0; + } + + int32_t callbackCcys = 0; + if (waveform.timer1CB) { + callbackCcys = scaleCcys(waveform.timer1CB(), isCPU2X); + } + now = ESP.getCycleCount(); + int32_t nextEventCcys = waveform.nextEventCcy - now; + // Account for unknown duration of timer1CB(). + if (waveform.timer1CB && nextEventCcys > callbackCcys) { + waveform.nextEventCcy = now + callbackCcys; + nextEventCcys = callbackCcys; + } + + // Timer is 80MHz fixed. 160MHz CPU frequency need scaling. + int32_t deltaIrqCcys = DELTAIRQCCYS; + int32_t irqLatencyCcys = IRQLATENCYCCYS; + if (isCPU2X) { + nextEventCcys >>= 1; + deltaIrqCcys >>= 1; + irqLatencyCcys >>= 1; + } + + // Firing timer too soon, the NMI occurs before ISR has returned. + if (nextEventCcys < irqLatencyCcys + deltaIrqCcys) { + waveform.nextEventCcy = now + IRQLATENCYCCYS + DELTAIRQCCYS; + nextEventCcys = irqLatencyCcys; + } + else { + nextEventCcys -= deltaIrqCcys; + } + + // Register access is fast and edge IRQ was configured before. + T1L = nextEventCcys; +} diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp deleted file mode 100644 index 78c7160d90..0000000000 --- a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp +++ /dev/null @@ -1,717 +0,0 @@ -/* esp8266_waveform imported from platform source code - Modified for WLED to work around a fault in the NMI handling, - which can result in the system locking up and hard WDT crashes. - - Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_pwm.cpp -*/ - -/* - esp8266_waveform - General purpose waveform generation and control, - supporting outputs on all pins in parallel. - - Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. - - The core idea is to have a programmable waveform generator with a unique - high and low period (defined in microseconds or CPU clock cycles). TIMER1 - is set to 1-shot mode and is always loaded with the time until the next - edge of any live waveforms. - - Up to one waveform generator per pin supported. - - Each waveform generator is synchronized to the ESP clock cycle counter, not - the timer. This allows for removing interrupt jitter and delay as the - counter always increments once per 80MHz clock. Changes to a waveform are - contiguous and only take effect on the next waveform transition, - allowing for smooth transitions. - - This replaces older tone(), analogWrite(), and the Servo classes. - - Everywhere in the code where "cycles" is used, it means ESP.getCycleCount() - clock cycle count, or an interval measured in CPU clock cycles, but not - TIMER1 cycles (which may be 2 CPU clock cycles @ 160MHz). - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - - -#include -#include -#include "ets_sys.h" -#include "core_esp8266_waveform.h" -#include "user_interface.h" - -extern "C" { - -// Linker magic -void usePWMFixedNMI() {}; - -// Maximum delay between IRQs -#define MAXIRQUS (10000) - -// Waveform generator can create tones, PWM, and servos -typedef struct { - uint32_t nextServiceCycle; // ESP cycle timer when a transition required - uint32_t expiryCycle; // For time-limited waveform, the cycle when this waveform must stop - uint32_t timeHighCycles; // Actual running waveform period (adjusted using desiredCycles) - uint32_t timeLowCycles; // - uint32_t desiredHighCycles; // Ideal waveform period to drive the error signal - uint32_t desiredLowCycles; // - uint32_t lastEdge; // Cycle when this generator last changed -} Waveform; - -class WVFState { -public: - Waveform waveform[17]; // State of all possible pins - uint32_t waveformState = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code - uint32_t waveformEnabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code - - // Enable lock-free by only allowing updates to waveformState and waveformEnabled from IRQ service routine - uint32_t waveformToEnable = 0; // Message to the NMI handler to start a waveform on a inactive pin - uint32_t waveformToDisable = 0; // Message to the NMI handler to disable a pin from waveform generation - - uint32_t waveformToChange = 0; // Mask of pin to change. One bit set in main app, cleared when effected in the NMI - uint32_t waveformNewHigh = 0; - uint32_t waveformNewLow = 0; - - uint32_t (*timer1CB)() = NULL; - - // Optimize the NMI inner loop by keeping track of the min and max GPIO that we - // are generating. In the common case (1 PWM) these may be the same pin and - // we can avoid looking at the other pins. - uint16_t startPin = 0; - uint16_t endPin = 0; -}; -static WVFState wvfState; - - -// Ensure everything is read/written to RAM -#define MEMBARRIER() { __asm__ volatile("" ::: "memory"); } - -// Non-speed critical bits -#pragma GCC optimize ("Os") - -// Interrupt on/off control -static IRAM_ATTR void timer1Interrupt(); -static bool timerRunning = false; - -static __attribute__((noinline)) void initTimer() { - if (!timerRunning) { - timer1_disable(); - ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); - ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); - timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); - timerRunning = true; - timer1_write(microsecondsToClockCycles(10)); - } -} - -static IRAM_ATTR void forceTimerInterrupt() { - if (T1L > microsecondsToClockCycles(10)) { - T1L = microsecondsToClockCycles(10); - } -} - -// PWM implementation using special purpose state machine -// -// Keep an ordered list of pins with the delta in cycles between each -// element, with a terminal entry making up the remainder of the PWM -// period. With this method sum(all deltas) == PWM period clock cycles. -// -// At t=0 set all pins high and set the timeout for the 1st edge. -// On interrupt, if we're at the last element reset to t=0 state -// Otherwise, clear that pin down and set delay for next element -// and so forth. - -constexpr int maxPWMs = 8; - -// PWM machine state -typedef struct PWMState { - uint32_t mask; // Bitmask of active pins - uint32_t cnt; // How many entries - uint32_t idx; // Where the state machine is along the list - uint8_t pin[maxPWMs + 1]; - uint32_t delta[maxPWMs + 1]; - uint32_t nextServiceCycle; // Clock cycle for next step - struct PWMState *pwmUpdate; // Set by main code, cleared by ISR -} PWMState; - -static PWMState pwmState; -static uint32_t _pwmFreq = 1000; -static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / _pwmFreq; - - -// If there are no more scheduled activities, shut down Timer 1. -// Otherwise, do nothing. -static IRAM_ATTR void disableIdleTimer() { - if (timerRunning && !wvfState.waveformEnabled && !pwmState.cnt && !wvfState.timer1CB) { - ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); - timer1_disable(); - timer1_isr_init(); - timerRunning = false; - } -} - -// Notify the NMI that a new PWM state is available through the mailbox. -// Wait for mailbox to be emptied (either busy or delay() as needed) -static IRAM_ATTR void _notifyPWM(PWMState *p, bool idle) { - p->pwmUpdate = nullptr; - pwmState.pwmUpdate = p; - MEMBARRIER(); - forceTimerInterrupt(); - while (pwmState.pwmUpdate) { - if (idle) { - esp_yield(); - } - MEMBARRIER(); - } -} - -static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range); - - -// Called when analogWriteFreq() changed to update the PWM total period -//extern void _setPWMFreq_weak(uint32_t freq) __attribute__((weak)); -void _setPWMFreq_weak(uint32_t freq) { - _pwmFreq = freq; - - // Convert frequency into clock cycles - uint32_t cc = microsecondsToClockCycles(1000000UL) / freq; - - // Simple static adjustment to bring period closer to requested due to overhead - // Empirically determined as a constant PWM delay and a function of the number of PWMs -#if F_CPU == 80000000 - cc -= ((microsecondsToClockCycles(pwmState.cnt) * 13) >> 4) + 110; -#else - cc -= ((microsecondsToClockCycles(pwmState.cnt) * 10) >> 4) + 75; -#endif - - if (cc == _pwmPeriod) { - return; // No change - } - - _pwmPeriod = cc; - - if (pwmState.cnt) { - PWMState p; // The working copy since we can't edit the one in use - p.mask = 0; - p.cnt = 0; - for (uint32_t i = 0; i < pwmState.cnt; i++) { - auto pin = pwmState.pin[i]; - _addPWMtoList(p, pin, wvfState.waveform[pin].desiredHighCycles, wvfState.waveform[pin].desiredLowCycles); - } - // Update and wait for mailbox to be emptied - initTimer(); - _notifyPWM(&p, true); - disableIdleTimer(); - } -} -/* -static void _setPWMFreq_bound(uint32_t freq) __attribute__((weakref("_setPWMFreq_weak"))); -void _setPWMFreq(uint32_t freq) { - _setPWMFreq_bound(freq); -} -*/ - -// Helper routine to remove an entry from the state machine -// and clean up any marked-off entries -static void _cleanAndRemovePWM(PWMState *p, int pin) { - uint32_t leftover = 0; - uint32_t in, out; - for (in = 0, out = 0; in < p->cnt; in++) { - if ((p->pin[in] != pin) && (p->mask & (1<pin[in]))) { - p->pin[out] = p->pin[in]; - p->delta[out] = p->delta[in] + leftover; - leftover = 0; - out++; - } else { - leftover += p->delta[in]; - p->mask &= ~(1<pin[in]); - } - } - p->cnt = out; - // Final pin is never used: p->pin[out] = 0xff; - p->delta[out] = p->delta[in] + leftover; -} - - -// Disable PWM on a specific pin (i.e. when a digitalWrite or analogWrite(0%/100%)) -//extern bool _stopPWM_weak(uint8_t pin) __attribute__((weak)); -IRAM_ATTR bool _stopPWM_weak(uint8_t pin) { - if (!((1<= _pwmPeriod) { - cc = _pwmPeriod - 1; - } - - if (p.cnt == 0) { - // Starting up from scratch, special case 1st element and PWM period - p.pin[0] = pin; - p.delta[0] = cc; - // Final pin is never used: p.pin[1] = 0xff; - p.delta[1] = _pwmPeriod - cc; - } else { - uint32_t ttl = 0; - uint32_t i; - // Skip along until we're at the spot to insert - for (i=0; (i <= p.cnt) && (ttl + p.delta[i] < cc); i++) { - ttl += p.delta[i]; - } - // Shift everything out by one to make space for new edge - for (int32_t j = p.cnt; j >= (int)i; j--) { - p.pin[j + 1] = p.pin[j]; - p.delta[j + 1] = p.delta[j]; - } - int off = cc - ttl; // The delta from the last edge to the one we're inserting - p.pin[i] = pin; - p.delta[i] = off; // Add the delta to this new pin - p.delta[i + 1] -= off; // And subtract it from the follower to keep sum(deltas) constant - } - p.cnt++; - p.mask |= 1<= maxPWMs) { - return false; // No space left - } - - // Sanity check for all-on/off - uint32_t cc = (_pwmPeriod * val) / range; - if ((cc == 0) || (cc >= _pwmPeriod)) { - digitalWrite(pin, cc ? HIGH : LOW); - return true; - } - - _addPWMtoList(p, pin, val, range); - - // Set mailbox and wait for ISR to copy it over - initTimer(); - _notifyPWM(&p, true); - disableIdleTimer(); - - // Potentially recalculate the PWM period if we've added another pin - _setPWMFreq(_pwmFreq); - - return true; -} -/* -static bool _setPWM_bound(int pin, uint32_t val, uint32_t range) __attribute__((weakref("_setPWM_weak"))); -bool _setPWM(int pin, uint32_t val, uint32_t range) { - return _setPWM_bound(pin, val, range); -} -*/ - -// Start up a waveform on a pin, or change the current one. Will change to the new -// waveform smoothly on next low->high transition. For immediate change, stopWaveform() -// first, then it will immediately begin. -//extern int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weak)); -int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, - int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { - (void) alignPhase; - (void) phaseOffsetUS; - (void) autoPwm; - - if ((pin > 16) || isFlashInterfacePin(pin) || (timeHighCycles == 0)) { - return false; - } - Waveform *wave = &wvfState.waveform[pin]; - wave->expiryCycle = runTimeCycles ? ESP.getCycleCount() + runTimeCycles : 0; - if (runTimeCycles && !wave->expiryCycle) { - wave->expiryCycle = 1; // expiryCycle==0 means no timeout, so avoid setting it - } - - _stopPWM(pin); // Make sure there's no PWM live here - - uint32_t mask = 1<timeHighCycles = timeHighCycles; - wave->desiredHighCycles = timeHighCycles; - wave->timeLowCycles = timeLowCycles; - wave->desiredLowCycles = timeLowCycles; - wave->lastEdge = 0; - wave->nextServiceCycle = ESP.getCycleCount() + microsecondsToClockCycles(1); - wvfState.waveformToEnable |= mask; - MEMBARRIER(); - initTimer(); - forceTimerInterrupt(); - while (wvfState.waveformToEnable) { - esp_yield(); // Wait for waveform to update - MEMBARRIER(); - } - } - - return true; -} -/* -static int startWaveformClockCycles_bound(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weakref("startWaveformClockCycles_weak"))); -int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { - return startWaveformClockCycles_bound(pin, timeHighCycles, timeLowCycles, runTimeCycles, alignPhase, phaseOffsetUS, autoPwm); -} - - -// This version falls-thru to the proper startWaveformClockCycles call and is invariant across waveform generators -int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS, - int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { - return startWaveformClockCycles_bound(pin, - microsecondsToClockCycles(timeHighUS), microsecondsToClockCycles(timeLowUS), - microsecondsToClockCycles(runTimeUS), alignPhase, microsecondsToClockCycles(phaseOffsetUS), autoPwm); -} -*/ - -// Set a callback. Pass in NULL to stop it -//extern void setTimer1Callback_weak(uint32_t (*fn)()) __attribute__((weak)); -void setTimer1Callback_weak(uint32_t (*fn)()) { - wvfState.timer1CB = fn; - if (fn) { - initTimer(); - forceTimerInterrupt(); - } - disableIdleTimer(); -} -/* -static void setTimer1Callback_bound(uint32_t (*fn)()) __attribute__((weakref("setTimer1Callback_weak"))); -void setTimer1Callback(uint32_t (*fn)()) { - setTimer1Callback_bound(fn); -} -*/ - -// Stops a waveform on a pin -//extern int stopWaveform_weak(uint8_t pin) __attribute__((weak)); -IRAM_ATTR int stopWaveform_weak(uint8_t pin) { - // Can't possibly need to stop anything if there is no timer active - if (!timerRunning) { - return false; - } - // If user sends in a pin >16 but <32, this will always point to a 0 bit - // If they send >=32, then the shift will result in 0 and it will also return false - uint32_t mask = 1<= (uintptr_t) &_UserExceptionVector_1)) { - // Address is good; save backup - epc3_backup = epc3; - eps3_backup = eps3; - } else { - // Address is inside the NMI handler -- restore from backup - __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); - } -} -// ----- @willmmiles end patch ----- - - -// The SDK and hardware take some time to actually get to our NMI code, so -// decrement the next IRQ's timer value by a bit so we can actually catch the -// real CPU cycle counter we want for the waveforms. - -// The SDK also sometimes is running at a different speed the the Arduino core -// so the ESP cycle counter is actually running at a variable speed. -// adjust(x) takes care of adjusting a delta clock cycle amount accordingly. -#if F_CPU == 80000000 - #define DELTAIRQ (microsecondsToClockCycles(9)/4) - #define adjust(x) ((x) << (turbo ? 1 : 0)) -#else - #define DELTAIRQ (microsecondsToClockCycles(9)/8) - #define adjust(x) ((x) >> 0) -#endif - -// When the time to the next edge is greater than this, RTI and set another IRQ to minimize CPU usage -#define MINIRQTIME microsecondsToClockCycles(6) - -static IRAM_ATTR void timer1Interrupt() { - // ----- @willmmiles begin patch ----- - nmiCrashWorkaround(); - // ----- @willmmiles end patch ----- - - // Flag if the core is at 160 MHz, for use by adjust() - bool turbo = (*(uint32_t*)0x3FF00014) & 1 ? true : false; - - uint32_t nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); - uint32_t timeoutCycle = GetCycleCountIRQ() + microsecondsToClockCycles(14); - - if (wvfState.waveformToEnable || wvfState.waveformToDisable) { - // Handle enable/disable requests from main app - wvfState.waveformEnabled = (wvfState.waveformEnabled & ~wvfState.waveformToDisable) | wvfState.waveformToEnable; // Set the requested waveforms on/off - wvfState.waveformState &= ~wvfState.waveformToEnable; // And clear the state of any just started - wvfState.waveformToEnable = 0; - wvfState.waveformToDisable = 0; - // No mem barrier. Globals must be written to RAM on ISR exit. - // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) - wvfState.startPin = __builtin_ffs(wvfState.waveformEnabled) - 1; - // Find the last bit by subtracting off GCC's count-leading-zeros (no offset in this one) - wvfState.endPin = 32 - __builtin_clz(wvfState.waveformEnabled); - } else if (!pwmState.cnt && pwmState.pwmUpdate) { - // Start up the PWM generator by copying from the mailbox - pwmState.cnt = 1; - pwmState.idx = 1; // Ensure copy this cycle, cause it to start at t=0 - pwmState.nextServiceCycle = GetCycleCountIRQ(); // Do it this loop! - // No need for mem barrier here. Global must be written by IRQ exit - } - - bool done = false; - if (wvfState.waveformEnabled || pwmState.cnt) { - do { - nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); - - // PWM state machine implementation - if (pwmState.cnt) { - int32_t cyclesToGo; - do { - cyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ(); - if (cyclesToGo < 0) { - if (pwmState.idx == pwmState.cnt) { // Start of pulses, possibly copy new - if (pwmState.pwmUpdate) { - // Do the memory copy from temp to global and clear mailbox - pwmState = *(PWMState*)pwmState.pwmUpdate; - } - GPOS = pwmState.mask; // Set all active pins high - if (pwmState.mask & (1<<16)) { - GP16O = 1; - } - pwmState.idx = 0; - } else { - do { - // Drop the pin at this edge - if (pwmState.mask & (1<expiryCycle) { - int32_t expiryToGo = wave->expiryCycle - now; - if (expiryToGo < 0) { - // Done, remove! - if (i == 16) { - GP16O = 0; - } - GPOC = mask; - wvfState.waveformEnabled &= ~mask; - continue; - } - } - - // Check for toggles - int32_t cyclesToGo = wave->nextServiceCycle - now; - if (cyclesToGo < 0) { - uint32_t nextEdgeCycles; - uint32_t desired = 0; - uint32_t *timeToUpdate; - wvfState.waveformState ^= mask; - if (wvfState.waveformState & mask) { - if (i == 16) { - GP16O = 1; - } - GPOS = mask; - - if (wvfState.waveformToChange & mask) { - // Copy over next full-cycle timings - wave->timeHighCycles = wvfState.waveformNewHigh; - wave->desiredHighCycles = wvfState.waveformNewHigh; - wave->timeLowCycles = wvfState.waveformNewLow; - wave->desiredLowCycles = wvfState.waveformNewLow; - wave->lastEdge = 0; - wvfState.waveformToChange = 0; - } - if (wave->lastEdge) { - desired = wave->desiredLowCycles; - timeToUpdate = &wave->timeLowCycles; - } - nextEdgeCycles = wave->timeHighCycles; - } else { - if (i == 16) { - GP16O = 0; - } - GPOC = mask; - desired = wave->desiredHighCycles; - timeToUpdate = &wave->timeHighCycles; - nextEdgeCycles = wave->timeLowCycles; - } - if (desired) { - desired = adjust(desired); - int32_t err = desired - (now - wave->lastEdge); - if (abs(err) < desired) { // If we've lost > the entire phase, ignore this error signal - err /= 2; - *timeToUpdate += err; - } - } - nextEdgeCycles = adjust(nextEdgeCycles); - wave->nextServiceCycle = now + nextEdgeCycles; - wave->lastEdge = now; - } - nextEventCycle = earliest(nextEventCycle, wave->nextServiceCycle); - } - - // Exit the loop if we've hit the fixed runtime limit or the next event is known to be after that timeout would occur - uint32_t now = GetCycleCountIRQ(); - int32_t cycleDeltaNextEvent = nextEventCycle - now; - int32_t cyclesLeftTimeout = timeoutCycle - now; - done = (cycleDeltaNextEvent > MINIRQTIME) || (cyclesLeftTimeout < 0); - } while (!done); - } // if (wvfState.waveformEnabled) - - if (wvfState.timer1CB) { - nextEventCycle = earliest(nextEventCycle, GetCycleCountIRQ() + wvfState.timer1CB()); - } - - int32_t nextEventCycles = nextEventCycle - GetCycleCountIRQ(); - - if (nextEventCycles < MINIRQTIME) { - nextEventCycles = MINIRQTIME; - } - nextEventCycles -= DELTAIRQ; - - // Do it here instead of global function to save time and because we know it's edge-IRQ - T1L = nextEventCycles >> (turbo ? 1 : 0); -} - -}; diff --git a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h b/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h new file mode 100644 index 0000000000..02e066f741 --- /dev/null +++ b/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h @@ -0,0 +1,469 @@ +/*------------------------------------------------------------------------- +NeoPixel driver for ESP32 RMTs using High-priority Interrupt + +(NB. This cannot be mixed with the non-HI driver.) + +Written by Will M. Miles. + +I invest time and resources providing this open source code, +please support me by donating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#pragma once + +#if defined(ARDUINO_ARCH_ESP32) + +// Use the NeoEspRmtSpeed types from the driver-based implementation +#include + + +namespace NeoEsp32RmtHiMethodDriver { + // Install the driver for a specific channel, specifying timing properties + esp_err_t Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t resetDuration); + + // Remove the driver on a specific channel + esp_err_t Uninstall(rmt_channel_t channel); + + // Write a buffer of data to a specific channel. + // Buffer reference is held until write completes. + esp_err_t Write(rmt_channel_t channel, const uint8_t *src, size_t src_size); + + // Wait until transaction is complete. + esp_err_t WaitForTxDone(rmt_channel_t channel, TickType_t wait_time); +}; + +template class NeoEsp32RmtHIMethodBase +{ +public: + typedef NeoNoSettings SettingsObject; + + NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize) : + _sizeData(pixelCount * elementSize + settingsSize), + _pin(pin) + { + construct(); + } + + NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize, NeoBusChannel channel) : + _sizeData(pixelCount* elementSize + settingsSize), + _pin(pin), + _channel(channel) + { + construct(); + } + + ~NeoEsp32RmtHIMethodBase() + { + // wait until the last send finishes before destructing everything + // arbitrary time out of 10 seconds + ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS)); + + ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Uninstall(_channel.RmtChannelNumber)); + + gpio_matrix_out(_pin, SIG_GPIO_OUT_IDX, false, false); + pinMode(_pin, INPUT); + + free(_dataEditing); + free(_dataSending); + } + + bool IsReadyToUpdate() const + { + return (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT_SILENT_TIMEOUT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 0))); + } + + void Initialize() + { + rmt_config_t config = {}; + + config.rmt_mode = RMT_MODE_TX; + config.channel = _channel.RmtChannelNumber; + config.gpio_num = static_cast(_pin); + config.mem_block_num = 1; + config.tx_config.loop_en = false; + + config.tx_config.idle_output_en = true; + config.tx_config.idle_level = T_SPEED::IdleLevel; + + config.tx_config.carrier_en = false; + config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; + + config.clk_div = T_SPEED::RmtClockDivider; + + ESP_ERROR_CHECK(rmt_config(&config)); // Uses ESP library + ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Install(_channel.RmtChannelNumber, T_SPEED::RmtBit0, T_SPEED::RmtBit1, T_SPEED::RmtDurationReset)); + } + + void Update(bool maintainBufferConsistency) + { + // wait for not actively sending data + // this will time out at 10 seconds, an arbitrarily long period of time + // and do nothing if this happens + if (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS))) + { + // now start the RMT transmit with the editing buffer before we swap + ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::Write(_channel.RmtChannelNumber, _dataEditing, _sizeData)); + + if (maintainBufferConsistency) + { + // copy editing to sending, + // this maintains the contract that "colors present before will + // be the same after", otherwise GetPixelColor will be inconsistent + memcpy(_dataSending, _dataEditing, _sizeData); + } + + // swap so the user can modify without affecting the async operation + std::swap(_dataSending, _dataEditing); + } + } + + bool AlwaysUpdate() + { + // this method requires update to be called only if changes to buffer + return false; + } + + bool SwapBuffers() + { + std::swap(_dataSending, _dataEditing); + return true; + } + + uint8_t* getData() const + { + return _dataEditing; + }; + + size_t getDataSize() const + { + return _sizeData; + } + + void applySettings([[maybe_unused]] const SettingsObject& settings) + { + } + +private: + const size_t _sizeData; // Size of '_data*' buffers + const uint8_t _pin; // output pin number + const T_CHANNEL _channel; // holds instance for multi channel support + + // Holds data stream which include LED color values and other settings as needed + uint8_t* _dataEditing; // exposed for get and set + uint8_t* _dataSending; // used for async send using RMT + + + void construct() + { + _dataEditing = static_cast(malloc(_sizeData)); + // data cleared later in Begin() + + _dataSending = static_cast(malloc(_sizeData)); + // no need to initialize it, it gets overwritten on every send + } +}; + +// normal +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsMethod; +typedef NeoEsp32RmtHINWs2805Method NeoEsp32RmtHINWs2814Method; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsMethod; +typedef NeoEsp32RmtHI0Ws2805Method NeoEsp32RmtHI0Ws2814Method; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsMethod; +typedef NeoEsp32RmtHI1Ws2805Method NeoEsp32RmtHI1Ws2814Method; + +#if !defined(CONFIG_IDF_TARGET_ESP32C3) + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsMethod; +typedef NeoEsp32RmtHI2Ws2805Method NeoEsp32RmtHI2Ws2814Method; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsMethod; +typedef NeoEsp32RmtHI3Ws2805Method NeoEsp32RmtHI3Ws2814Method; + +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsMethod; +typedef NeoEsp32RmtHI4Ws2805Method NeoEsp32RmtHI4Ws2814Method; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsMethod; +typedef NeoEsp32RmtHI5Ws2805Method NeoEsp32RmtHI5Ws2814Method; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsMethod; +typedef NeoEsp32RmtHI6Ws2805Method NeoEsp32RmtHI6Ws2814Method; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903Method; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsMethod; +typedef NeoEsp32RmtHI7Ws2805Method NeoEsp32RmtHI7Ws2814Method; + +#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) +#endif // !defined(CONFIG_IDF_TARGET_ESP32C3) + +// inverted +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsInvertedMethod; +typedef NeoEsp32RmtHINWs2805InvertedMethod NeoEsp32RmtHINWs2814InvertedMethod; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsInvertedMethod; +typedef NeoEsp32RmtHI0Ws2805InvertedMethod NeoEsp32RmtHI0Ws2814InvertedMethod; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsInvertedMethod; +typedef NeoEsp32RmtHI1Ws2805InvertedMethod NeoEsp32RmtHI1Ws2814InvertedMethod; + +#if !defined(CONFIG_IDF_TARGET_ESP32C3) + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsInvertedMethod; +typedef NeoEsp32RmtHI2Ws2805InvertedMethod NeoEsp32RmtHI2Ws2814InvertedMethod; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsInvertedMethod; +typedef NeoEsp32RmtHI3Ws2805InvertedMethod NeoEsp32RmtHI3Ws2814InvertedMethod; + +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsInvertedMethod; +typedef NeoEsp32RmtHI4Ws2805InvertedMethod NeoEsp32RmtHI4Ws2814InvertedMethod; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsInvertedMethod; +typedef NeoEsp32RmtHI5Ws2805InvertedMethod NeoEsp32RmtHI5Ws2814InvertedMethod; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsInvertedMethod; +typedef NeoEsp32RmtHI6Ws2805InvertedMethod NeoEsp32RmtHI6Ws2814InvertedMethod; + +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903InvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsInvertedMethod; +typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsInvertedMethod; +typedef NeoEsp32RmtHI7Ws2805InvertedMethod NeoEsp32RmtHI7Ws2814InvertedMethod; + +#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) +#endif // !defined(CONFIG_IDF_TARGET_ESP32C3) + +#endif diff --git a/lib/NeoESP32RmtHI/library.json b/lib/NeoESP32RmtHI/library.json new file mode 100644 index 0000000000..0608e59e12 --- /dev/null +++ b/lib/NeoESP32RmtHI/library.json @@ -0,0 +1,12 @@ +{ + "name": "NeoESP32RmtHI", + "build": { "libArchive": false }, + "platforms": ["espressif32"], + "dependencies": [ + { + "owner": "makuna", + "name": "NeoPixelBus", + "version": "^2.8.3" + } + ] +} diff --git a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S b/lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S new file mode 100644 index 0000000000..0c60d2ebc9 --- /dev/null +++ b/lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S @@ -0,0 +1,263 @@ +/* RMT ISR shim + * Bridges from a high-level interrupt to the C++ code. + * + * This code is largely derived from Espressif's 'hli_vector.S' Bluetooth ISR. + * + */ + +#if defined(__XTENSA__) && defined(ESP32) && !defined(CONFIG_BTDM_CTRL_HLI) + +#include +#include "sdkconfig.h" +#include "soc/soc.h" + +/* If the Bluetooth driver has hooked the high-priority interrupt, we piggyback on it and don't need this. */ +#ifndef CONFIG_BTDM_CTRL_HLI + +/* + Select interrupt based on system check level + - Base ESP32: could be 4 or 5, depends on platform config + - S2: 5 + - S3: 5 +*/ + +#if CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 +/* Use level 4 */ +#define RFI_X 4 +#define xt_highintx xt_highint4 +#else /* !CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 */ +/* Use level 5 */ +#define RFI_X 5 +#define xt_highintx xt_highint5 +#endif /* CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 */ + +// Register map, based on interrupt level +#define EPC_X (EPC + RFI_X) +#define EXCSAVE_X (EXCSAVE + RFI_X) + +// The sp mnemonic is used all over in ESP's assembly, though I'm not sure where it's expected to be defined? +#define sp a1 + +/* Interrupt stack size, for C code. */ +#define RMT_INTR_STACK_SIZE 512 + +/* Save area for the CPU state: + * - 64 words for the general purpose registers + * - 7 words for some of the special registers: + * - WINDOWBASE, WINDOWSTART — only WINDOWSTART is truly needed + * - SAR, LBEG, LEND, LCOUNT — since the C code might use these + * - EPC1 — since the C code might cause window overflow exceptions + * This is not laid out as standard exception frame structure + * for simplicity of the save/restore code. + */ +#define REG_FILE_SIZE (64 * 4) +#define SPECREG_OFFSET REG_FILE_SIZE +#define SPECREG_SIZE (7 * 4) +#define REG_SAVE_AREA_SIZE (SPECREG_OFFSET + SPECREG_SIZE) + + .data +_rmt_intr_stack: + .space RMT_INTR_STACK_SIZE +_rmt_save_ctx: + .space REG_SAVE_AREA_SIZE + + .section .iram1,"ax" + .global xt_highintx + .type xt_highintx,@function + .align 4 + +xt_highintx: + + movi a0, _rmt_save_ctx + /* save 4 lower registers */ + s32i a1, a0, 4 + s32i a2, a0, 8 + s32i a3, a0, 12 + rsr a2, EXCSAVE_X /* holds the value of a0 */ + s32i a2, a0, 0 + + /* Save special registers */ + addi a0, a0, SPECREG_OFFSET + rsr a2, WINDOWBASE + s32i a2, a0, 0 + rsr a2, WINDOWSTART + s32i a2, a0, 4 + rsr a2, SAR + s32i a2, a0, 8 + #if XCHAL_HAVE_LOOPS + rsr a2, LBEG + s32i a2, a0, 12 + rsr a2, LEND + s32i a2, a0, 16 + rsr a2, LCOUNT + s32i a2, a0, 20 + #endif + rsr a2, EPC1 + s32i a2, a0, 24 + + /* disable exception mode, window overflow */ + movi a0, PS_INTLEVEL(RFI_X+1) | PS_EXCM + wsr a0, PS + rsync + + /* Save the remaining physical registers. + * 4 registers are already saved, which leaves 60 registers to save. + * (FIXME: consider the case when the CPU is configured with physical 32 registers) + * These 60 registers are saved in 5 iterations, 12 registers at a time. + */ + movi a1, 5 + movi a3, _rmt_save_ctx + 4 * 4 + + /* This is repeated 5 times, each time the window is shifted by 12 registers. + * We come here with a1 = downcounter, a3 = save pointer, a2 and a0 unused. + */ +1: + s32i a4, a3, 0 + s32i a5, a3, 4 + s32i a6, a3, 8 + s32i a7, a3, 12 + s32i a8, a3, 16 + s32i a9, a3, 20 + s32i a10, a3, 24 + s32i a11, a3, 28 + s32i a12, a3, 32 + s32i a13, a3, 36 + s32i a14, a3, 40 + s32i a15, a3, 44 + + /* We are about to rotate the window, so that a12-a15 will become the new a0-a3. + * Copy a0-a3 to a12-15 to still have access to these values. + * At the same time we can decrement the counter and adjust the save area pointer + */ + + /* a0 is constant (_rmt_save_ctx), no need to copy */ + addi a13, a1, -1 /* copy and decrement the downcounter */ + /* a2 is scratch so no need to copy */ + addi a15, a3, 48 /* copy and adjust the save area pointer */ + beqz a13, 2f /* have saved all registers ? */ + rotw 3 /* rotate the window and go back */ + j 1b + + /* the loop is complete */ +2: + rotw 4 /* this brings us back to the original window */ + /* a0 still points to _rmt_save_ctx */ + + /* Can clear WINDOWSTART now, all registers are saved */ + rsr a2, WINDOWBASE + /* WINDOWSTART = (1 << WINDOWBASE) */ + movi a3, 1 + ssl a2 + sll a3, a3 + wsr a3, WINDOWSTART + +_highint_stack_switch: + movi a0, 0 + movi sp, _rmt_intr_stack + RMT_INTR_STACK_SIZE - 16 + s32e a0, sp, -12 /* For GDB: set null SP */ + s32e a0, sp, -16 /* For GDB: set null PC */ + movi a0, _highint_stack_switch /* For GDB: cosmetics, for the frame where stack switch happened */ + + /* Set up PS for C, disable all interrupts except NMI and debug, and clear EXCM. */ + movi a6, PS_INTLEVEL(RFI_X) | PS_UM | PS_WOE + wsr a6, PS + rsync + + /* Call C handler */ + mov a6, sp + call4 NeoEsp32RmtMethodIsr + + l32e sp, sp, -12 /* switch back to the original stack */ + + /* Done with C handler; re-enable exception mode, disabling window overflow */ + movi a2, PS_INTLEVEL(RFI_X+1) | PS_EXCM /* TOCHECK */ + wsr a2, PS + rsync + + /* Restore the special registers. + * WINDOWSTART will be restored near the end. + */ + movi a0, _rmt_save_ctx + SPECREG_OFFSET + l32i a2, a0, 8 + wsr a2, SAR + #if XCHAL_HAVE_LOOPS + l32i a2, a0, 12 + wsr a2, LBEG + l32i a2, a0, 16 + wsr a2, LEND + l32i a2, a0, 20 + wsr a2, LCOUNT + #endif + l32i a2, a0, 24 + wsr a2, EPC1 + + /* Restoring the physical registers. + * This is the reverse to the saving process above. + */ + + /* Rotate back to the final window, then start loading 12 registers at a time, + * in 5 iterations. + * Again, a1 is the downcounter and a3 is the save area pointer. + * After each rotation, a1 and a3 are copied from a13 and a15. + * To simplify the loop, we put the initial values into a13 and a15. + */ + rotw -4 + movi a15, _rmt_save_ctx + 64 * 4 /* point to the end of the save area */ + movi a13, 5 + +1: + /* Copy a1 and a3 from their previous location, + * at the same time decrementing and adjusting the save area pointer. + */ + addi a1, a13, -1 + addi a3, a15, -48 + + /* Load 12 registers */ + l32i a4, a3, 0 + l32i a5, a3, 4 + l32i a6, a3, 8 + l32i a7, a3, 12 + l32i a8, a3, 16 + l32i a9, a3, 20 + l32i a10, a3, 24 + l32i a11, a3, 28 /* ensure PS and EPC written */ + l32i a12, a3, 32 + l32i a13, a3, 36 + l32i a14, a3, 40 + l32i a15, a3, 44 + + /* Done with the loop? */ + beqz a1, 2f + /* If no, rotate the window and repeat */ + rotw -3 + j 1b + +2: + /* Done with the loop. Only 4 registers (a0-a3 in the original window) remain + * to be restored. Also need to restore WINDOWSTART, since all the general + * registers are now in place. + */ + movi a0, _rmt_save_ctx + + l32i a2, a0, SPECREG_OFFSET + 4 + wsr a2, WINDOWSTART + + l32i a1, a0, 4 + l32i a2, a0, 8 + l32i a3, a0, 12 + rsr a0, EXCSAVE_X /* holds the value of a0 before the interrupt handler */ + + /* Return from the interrupt, restoring PS from EPS_X */ + rfi RFI_X + + +/* The linker has no reason to link in this file; all symbols it exports are already defined + (weakly!) in the default int handler. Define a symbol here so we can use it to have the + linker inspect this anyway. */ + + .global ld_include_hli_vectors_rmt +ld_include_hli_vectors_rmt: + + +#endif // CONFIG_BTDM_CTRL_HLI +#endif // XTensa \ No newline at end of file diff --git a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp b/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp new file mode 100644 index 0000000000..8353201f08 --- /dev/null +++ b/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp @@ -0,0 +1,507 @@ +/*------------------------------------------------------------------------- +NeoPixel library helper functions for Esp32. + +A BIG thanks to Andreas Merkle for the investigation and implementation of +a workaround to the GCC bug that drops method attributes from template methods + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by donating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#include + +#if defined(ARDUINO_ARCH_ESP32) + +#include +#include "esp_idf_version.h" +#include "NeoEsp32RmtHIMethod.h" +#include "soc/soc.h" +#include "soc/rmt_reg.h" + +#ifdef __riscv +#include "riscv/interrupt.h" +#endif + + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) +#include "hal/rmt_ll.h" +#else +/* Shims for older ESP-IDF v3; we can safely assume original ESP32 */ +#include "soc/rmt_struct.h" + +// Selected RMT API functions borrowed from ESP-IDF v4.4.8 +// components/hal/esp32/include/hal/rmt_ll.h +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +__attribute__((always_inline)) +static inline void rmt_ll_tx_reset_pointer(rmt_dev_t *dev, uint32_t channel) +{ + dev->conf_ch[channel].conf1.mem_rd_rst = 1; + dev->conf_ch[channel].conf1.mem_rd_rst = 0; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_start(rmt_dev_t *dev, uint32_t channel) +{ + dev->conf_ch[channel].conf1.tx_start = 1; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_stop(rmt_dev_t *dev, uint32_t channel) +{ + RMTMEM.chan[channel].data32[0].val = 0; + dev->conf_ch[channel].conf1.tx_start = 0; + dev->conf_ch[channel].conf1.mem_rd_rst = 1; + dev->conf_ch[channel].conf1.mem_rd_rst = 0; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_enable_pingpong(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->apb_conf.mem_tx_wrap_en = enable; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_enable_loop(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->conf_ch[channel].conf1.tx_conti_mode = enable; +} + +__attribute__((always_inline)) +static inline uint32_t rmt_ll_tx_get_channel_status(rmt_dev_t *dev, uint32_t channel) +{ + return dev->status_ch[channel]; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_set_limit(rmt_dev_t *dev, uint32_t channel, uint32_t limit) +{ + dev->tx_lim_ch[channel].limit = limit; +} + +__attribute__((always_inline)) +static inline void rmt_ll_enable_interrupt(rmt_dev_t *dev, uint32_t mask, bool enable) +{ + if (enable) { + dev->int_ena.val |= mask; + } else { + dev->int_ena.val &= ~mask; + } +} + +__attribute__((always_inline)) +static inline void rmt_ll_enable_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->int_ena.val &= ~(1 << (channel * 3)); + dev->int_ena.val |= (enable << (channel * 3)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_enable_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->int_ena.val &= ~(1 << (channel * 3 + 2)); + dev->int_ena.val |= (enable << (channel * 3 + 2)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_enable_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->int_ena.val &= ~(1 << (channel + 24)); + dev->int_ena.val |= (enable << (channel + 24)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_clear_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel) +{ + dev->int_clr.val = (1 << (channel * 3)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_clear_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel) +{ + dev->int_clr.val = (1 << (channel * 3 + 2)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_clear_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel) +{ + dev->int_clr.val = (1 << (channel + 24)); +} + + +__attribute__((always_inline)) +static inline uint32_t rmt_ll_get_tx_thres_interrupt_status(rmt_dev_t *dev) +{ + uint32_t status = dev->int_st.val; + return (status & 0xFF000000) >> 24; +} +#endif + + +// ********************************* +// Select method for binding interrupt +// +// - If the Bluetooth driver has registered a high-level interrupt, piggyback on that API +// - If we're on a modern core, allocate the interrupt with the API (old cores are bugged) +// - Otherwise use the low-level hardware API to manually bind the interrupt + + +#if defined(CONFIG_BTDM_CTRL_HLI) +// Espressif's bluetooth driver offers a helpful sharing layer; bring in the interrupt management calls +#include "hal/interrupt_controller_hal.h" +extern "C" esp_err_t hli_intr_register(intr_handler_t handler, void* arg, uint32_t intr_reg, uint32_t intr_mask); + +#else /* !CONFIG_BTDM_CTRL_HLI*/ + +// Declare the our high-priority ISR handler +extern "C" void ld_include_hli_vectors_rmt(); // an object with an address, but no space + +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) +#include "soc/periph_defs.h" +#endif + +// Select level flag +#if defined(__riscv) +// RISCV chips don't block interrupts while scheduling; all we need to do is be higher than the WiFi ISR +#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL3 +#elif defined(CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5) +#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL4 +#else +#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL5 +#endif + +// ESP-IDF v3 cannot enable high priority interrupts through the API at all; +// and ESP-IDF v4 on XTensa cannot enable Level 5 due to incorrect interrupt descriptor tables +#if !defined(__XTENSA__) || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) || ((ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) && CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5)) +#define NEOESP32_RMT_CAN_USE_INTR_ALLOC + +// XTensa cores require the assembly bridge +#ifdef __XTENSA__ +#define HI_IRQ_HANDLER nullptr +#define HI_IRQ_HANDLER_ARG ld_include_hli_vectors_rmt +#else +#define HI_IRQ_HANDLER NeoEsp32RmtMethodIsr +#define HI_IRQ_HANDLER_ARG nullptr +#endif + +#else +/* !CONFIG_BTDM_CTRL_HLI && !NEOESP32_RMT_CAN_USE_INTR_ALLOC */ +// This is the index of the LV5 interrupt vector - see interrupt descriptor table in idf components/hal/esp32/interrupt_descriptor_table.c +#define ESP32_LV5_IRQ_INDEX 26 + +#endif /* NEOESP32_RMT_CAN_USE_INTR_ALLOC */ +#endif /* CONFIG_BTDM_CTRL_HLI */ + + +// RMT driver implementation +struct NeoEsp32RmtHIChannelState { + uint32_t rmtBit0, rmtBit1; + uint32_t resetDuration; + + const byte* txDataStart; // data array + const byte* txDataEnd; // one past end + const byte* txDataCurrent; // current location + size_t rmtOffset; +}; + +// Global variables +#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) +static intr_handle_t isrHandle = nullptr; +#endif + +static NeoEsp32RmtHIChannelState** driverState = nullptr; +constexpr size_t rmtBatchSize = RMT_MEM_ITEM_NUM / 2; + +// Fill the RMT buffer memory +// This is implemented using many arguments instead of passing the structure object to ensure we do only one lookup +// All the arguments are passed in registers, so they don't need to be looked up again +static void IRAM_ATTR RmtFillBuffer(uint8_t channel, const byte** src_ptr, const byte* end, uint32_t bit0, uint32_t bit1, size_t* offset_ptr, size_t reserve) { + // We assume that (rmtToWrite % 8) == 0 + size_t rmtToWrite = rmtBatchSize - reserve; + rmt_item32_t* dest =(rmt_item32_t*) &RMTMEM.chan[channel].data32[*offset_ptr + reserve]; // write directly in to RMT memory + const byte* psrc = *src_ptr; + + *offset_ptr ^= rmtBatchSize; + + if (psrc != end) { + while (rmtToWrite > 0) { + uint8_t data = *psrc; + for (uint8_t bit = 0; bit < 8; bit++) + { + dest->val = (data & 0x80) ? bit1 : bit0; + dest++; + data <<= 1; + } + rmtToWrite -= 8; + psrc++; + + if (psrc == end) { + break; + } + } + + *src_ptr = psrc; + } + + if (rmtToWrite > 0) { + // Add end event + rmt_item32_t bit0_val = {{.val = bit0 }}; + *dest = rmt_item32_t {{{ .duration0 = 0, .level0 = bit0_val.level1, .duration1 = 0, .level1 = bit0_val.level1 }}}; + } +} + +static void IRAM_ATTR RmtStartWrite(uint8_t channel, NeoEsp32RmtHIChannelState& state) { + // Reset context state + state.rmtOffset = 0; + + // Fill the first part of the buffer with a reset event + // FUTURE: we could do timing analysis with the last interrupt on this channel + // Use 8 words to stay aligned with the buffer fill logic + rmt_item32_t bit0_val = {{.val = state.rmtBit0 }}; + rmt_item32_t fill = {{{ .duration0 = 100, .level0 = bit0_val.level1, .duration1 = 100, .level1 = bit0_val.level1 }}}; + rmt_item32_t* dest = (rmt_item32_t*) &RMTMEM.chan[channel].data32[0]; + for (auto i = 0; i < 7; ++i) dest[i] = fill; + fill.duration1 = state.resetDuration > 1400 ? (state.resetDuration - 1400) : 100; + dest[7] = fill; + + // Fill the remaining buffer with real data + RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 8); + RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0); + + // Start operation + rmt_ll_clear_tx_thres_interrupt(&RMT, channel); + rmt_ll_tx_reset_pointer(&RMT, channel); + rmt_ll_tx_start(&RMT, channel); +} + +extern "C" void IRAM_ATTR NeoEsp32RmtMethodIsr(void *arg) { + // Tx threshold interrupt + uint32_t status = rmt_ll_get_tx_thres_interrupt_status(&RMT); + while (status) { + uint8_t channel = __builtin_ffs(status) - 1; + if (driverState[channel]) { + // Normal case + NeoEsp32RmtHIChannelState& state = *driverState[channel]; + RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0); + } else { + // Danger - another driver got invoked? + rmt_ll_tx_stop(&RMT, channel); + } + rmt_ll_clear_tx_thres_interrupt(&RMT, channel); + status = rmt_ll_get_tx_thres_interrupt_status(&RMT); + } +}; + +// Wrapper around the register analysis defines +// For all currently supported chips, this is constant for all channels; but this is not true of *all* ESP32 +static inline bool _RmtStatusIsTransmitting(rmt_channel_t channel, uint32_t status) { + uint32_t v; + switch(channel) { +#ifdef RMT_STATE_CH0 + case 0: v = (status >> RMT_STATE_CH0_S) & RMT_STATE_CH0_V; break; +#endif +#ifdef RMT_STATE_CH1 + case 1: v = (status >> RMT_STATE_CH1_S) & RMT_STATE_CH1_V; break; +#endif +#ifdef RMT_STATE_CH2 + case 2: v = (status >> RMT_STATE_CH2_S) & RMT_STATE_CH2_V; break; +#endif +#ifdef RMT_STATE_CH3 + case 3: v = (status >> RMT_STATE_CH3_S) & RMT_STATE_CH3_V; break; +#endif +#ifdef RMT_STATE_CH4 + case 4: v = (status >> RMT_STATE_CH4_S) & RMT_STATE_CH4_V; break; +#endif +#ifdef RMT_STATE_CH5 + case 5: v = (status >> RMT_STATE_CH5_S) & RMT_STATE_CH5_V; break; +#endif +#ifdef RMT_STATE_CH6 + case 6: v = (status >> RMT_STATE_CH6_S) & RMT_STATE_CH6_V; break; +#endif +#ifdef RMT_STATE_CH7 + case 7: v = (status >> RMT_STATE_CH7_S) & RMT_STATE_CH7_V; break; +#endif + default: v = 0; + } + + return v != 0; +} + + +esp_err_t NeoEsp32RmtHiMethodDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset) { + // Validate channel number + if (channel >= RMT_CHANNEL_MAX) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t err = ESP_OK; + if (!driverState) { + // First time init + driverState = reinterpret_cast(heap_caps_calloc(RMT_CHANNEL_MAX, sizeof(NeoEsp32RmtHIChannelState*), MALLOC_CAP_INTERNAL)); + if (!driverState) return ESP_ERR_NO_MEM; + + // Ensure all interrupts are cleared before binding + RMT.int_ena.val = 0; + RMT.int_clr.val = 0xFFFFFFFF; + + // Bind interrupt handler +#if defined(CONFIG_BTDM_CTRL_HLI) + // Bluetooth driver has taken the empty high-priority interrupt. Fortunately, it allows us to + // hook up another handler. + err = hli_intr_register(NeoEsp32RmtMethodIsr, nullptr, (uintptr_t) &RMT.int_st, 0xFF000000); + // 25 is the magic number of the bluetooth ISR on ESP32 - see soc/soc.h. + intr_matrix_set(cpu_hal_get_core_id(), ETS_RMT_INTR_SOURCE, 25); + intr_cntrl_ll_enable_interrupts(1<<25); +#elif defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) + // Use the platform code to allocate the interrupt + // If we need the additional assembly bridge, we pass it as the "arg" to the IDF so it gets linked in + err = esp_intr_alloc(ETS_RMT_INTR_SOURCE, INT_LEVEL_FLAG | ESP_INTR_FLAG_IRAM, HI_IRQ_HANDLER, (void*) HI_IRQ_HANDLER_ARG, &isrHandle); + //err = ESP_ERR_NOT_FINISHED; +#else + // Broken IDF API does not allow us to reserve the interrupt; do it manually + static volatile const void* __attribute__((used)) pleaseLinkAssembly = (void*) ld_include_hli_vectors_rmt; + intr_matrix_set(xPortGetCoreID(), ETS_RMT_INTR_SOURCE, ESP32_LV5_IRQ_INDEX); + ESP_INTR_ENABLE(ESP32_LV5_IRQ_INDEX); +#endif + + if (err != ESP_OK) { + heap_caps_free(driverState); + driverState = nullptr; + return err; + } + } + + if (driverState[channel] != nullptr) { + return ESP_ERR_INVALID_STATE; // already in use + } + + NeoEsp32RmtHIChannelState* state = reinterpret_cast(heap_caps_calloc(1, sizeof(NeoEsp32RmtHIChannelState), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); + if (state == nullptr) { + return ESP_ERR_NO_MEM; + } + + // Store timing information + state->rmtBit0 = rmtBit0; + state->rmtBit1 = rmtBit1; + state->resetDuration = reset; + + // Initialize hardware + rmt_ll_tx_stop(&RMT, channel); + rmt_ll_tx_reset_pointer(&RMT, channel); + rmt_ll_enable_tx_err_interrupt(&RMT, channel, false); + rmt_ll_enable_tx_end_interrupt(&RMT, channel, false); + rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); + rmt_ll_clear_tx_err_interrupt(&RMT, channel); + rmt_ll_clear_tx_end_interrupt(&RMT, channel); + rmt_ll_clear_tx_thres_interrupt(&RMT, channel); + + rmt_ll_tx_enable_loop(&RMT, channel, false); + rmt_ll_tx_enable_pingpong(&RMT, channel, true); + rmt_ll_tx_set_limit(&RMT, channel, rmtBatchSize); + + driverState[channel] = state; + + rmt_ll_enable_tx_thres_interrupt(&RMT, channel, true); + + return err; +} + +esp_err_t NeoEsp32RmtHiMethodDriver::Uninstall(rmt_channel_t channel) { + if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; + + NeoEsp32RmtHIChannelState* state = driverState[channel]; + + WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); + + // Done or not, we're out of here + rmt_ll_tx_stop(&RMT, channel); + rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); + driverState[channel] = nullptr; + heap_caps_free(state); + +#if !defined(CONFIG_BTDM_CTRL_HLI) /* Cannot unbind from bluetooth ISR */ + // Turn off the driver ISR and release global state if none are left + for (uint8_t channelIndex = 0; channelIndex < RMT_CHANNEL_MAX; ++channelIndex) { + if (driverState[channelIndex]) return ESP_OK; // done + } + +#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) + esp_intr_free(isrHandle); +#else + ESP_INTR_DISABLE(ESP32_LV5_IRQ_INDEX); +#endif + + heap_caps_free(driverState); + driverState = nullptr; +#endif /* !defined(CONFIG_BTDM_CTRL_HLI) */ + + return ESP_OK; +} + +esp_err_t NeoEsp32RmtHiMethodDriver::Write(rmt_channel_t channel, const uint8_t *src, size_t src_size) { + if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; + + NeoEsp32RmtHIChannelState& state = *driverState[channel]; + esp_err_t result = WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); + + if (result == ESP_OK) { + state.txDataStart = src; + state.txDataCurrent = src; + state.txDataEnd = src + src_size; + RmtStartWrite(channel, state); + } + return result; +} + +esp_err_t NeoEsp32RmtHiMethodDriver::WaitForTxDone(rmt_channel_t channel, TickType_t wait_time) { + if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; + + NeoEsp32RmtHIChannelState& state = *driverState[channel]; + // yield-wait until wait_time + esp_err_t rv = ESP_OK; + uint32_t status; + while(1) { + status = rmt_ll_tx_get_channel_status(&RMT, channel); + if (!_RmtStatusIsTransmitting(channel, status)) break; + if (wait_time == 0) { rv = ESP_ERR_TIMEOUT; break; }; + + TickType_t sleep = std::min(wait_time, (TickType_t) 5); + vTaskDelay(sleep); + wait_time -= sleep; + }; + + return rv; +} + +#endif \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0f73bff0c4..b1313aa103 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,28 @@ { "name": "wled", - "version": "0.15.0-b7", + "version": "16.0-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.0-b7", + "version": "16.0-alpha", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", - "inliner": "^1.13.1", - "nodemon": "^3.0.2" + "nodemon": "^3.1.14", + "web-resource-inliner": "^7.0.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -32,6 +36,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -40,6 +45,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -48,6 +54,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -56,21 +63,24 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -78,62 +88,20 @@ "node": ">=0.4.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", - "dependencies": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -142,70 +110,20 @@ "node": ">= 8" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" - }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dependencies": { - "tweetnacl": "^0.14.3" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -213,24 +131,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -241,89 +158,24 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, - "node_modules/camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "node_modules/center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", - "dependencies": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/charset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", - "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/cheerio": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz", - "integrity": "sha512-Fwcm3zkR37STnPC8FepSHeSYJM5Rd596TZOcfDUdojR4Q735aK1Xn+M+ISagNneuCwMjK28w4kX+ETILGNT/UQ==", - "dependencies": { - "css-select": "~1.0.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "~3.8.1", - "lodash": "^3.2.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cheerio/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -343,21 +195,11 @@ "fsevents": "~2.3.2" } }, - "node_modules/clap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", - "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", - "dependencies": { - "chalk": "^1.1.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, @@ -365,281 +207,141 @@ "node": ">= 10.0" } }, - "node_modules/cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", - "dependencies": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "node_modules/coa": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", - "integrity": "sha512-KAGck/eNAmCL0dcT3BiuYwLbExK6lduR8DxM3C1TyDzaXhZHyZ8ooX5I5+na2e3dPFuibfxrGdorr0/Lr7RYCQ==", - "dependencies": { - "q": "^1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", "engines": { "node": ">=14" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/configstore": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-1.4.0.tgz", - "integrity": "sha512-Zcx2SVdZC06IuRHd2MhkVYFNJBkZBj166LGdsJXRcqNC8Gs5Bwh8mosStNeCBBmtIm4wNii2uarD50qztjKOjw==", + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "object-assign": "^4.0.1", - "os-tmpdir": "^1.0.0", - "osenv": "^0.1.0", - "uuid": "^2.0.1", - "write-file-atomic": "^1.1.2", - "xdg-basedir": "^2.0.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/configstore/node_modules/uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details." - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/css-select": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", - "integrity": "sha512-/xPlD7betkfd7ChGkLGGWx5HWyiHDOSn7aACLzdH0nwucPvB0EAm8hMBm7Xn7vGfAeRRN7KZ8wumGm8NoNcMRw==", - "dependencies": { - "boolbase": "~1.0.0", - "css-what": "1.0", - "domutils": "1.4", - "nth-check": "~1.0.0" - } - }, - "node_modules/css-what": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-1.0.0.tgz", - "integrity": "sha512-60SUMPBreXrLXgvpM8kYpO0AOyMRhdRlXFX5BMQbZq1SIJCyNE56nqFQhmvREQdUJpedbGRYZ5wOyq3/F6q5Zw==", - "engines": { - "node": "*" - } - }, - "node_modules/csso": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-2.0.0.tgz", - "integrity": "sha512-tckZA0LhyEnToPoQDmncCA+TUS3aoIVl/MsSaoipR52Sfa+H83fJvIHRVOHMFn9zW6kIV1L0D7tUDFFjvN28lg==", - "dependencies": { - "clap": "^1.0.9", - "source-map": "^0.5.3" - }, - "bin": { - "csso": "bin/csso" + "node": ">=6.0" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/csso/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", "dependencies": { - "assert-plus": "^1.0.0" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" }, - "engines": { - "node": ">=0.10" + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "domelementtype": "^2.2.0" + }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "dependencies": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/dom-serializer/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "license": "BSD-2-Clause", "dependencies": { - "domelementtype": "1" + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/domutils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", - "integrity": "sha512-ZkVgS/PpxjyJMb+S2iVHHEZjVnOUtjGp0/zstqKGTE9lrZtNHlNQmLwP/lhLMEApYbzc08BKMx9IFpKhaSbW1w==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", "dependencies": { - "domelementtype": "1" + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, - "node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/duplexify/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/duplexify/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexify/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/duplexify/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -647,58 +349,23 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es6-promise": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.3.0.tgz", - "integrity": "sha512-oyOjMhyKMLEjOOtvkwg0G4pAzLQ9WdbbeX7WdqKzvYXu+UFgD0Zo/Brq5Q49zNmnGPPzV5rmYvrr0jz1zWx8Iw==" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "license": "MIT", "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "node": ">=10" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -706,32 +373,12 @@ "node": ">=8" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -740,18 +387,11 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -759,75 +399,11 @@ "node": ">= 6" } }, - "node_modules/got": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/got/-/got-3.3.1.tgz", - "integrity": "sha512-7chPlc0pWHjvq7B6dEEXz4GphoDupOvBSSl6AwRsAJX7GPTZ+bturaZiIigX4Dp6KrAP67nvzuKkNc0SLA0DKg==", - "dependencies": { - "duplexify": "^3.2.0", - "infinity-agent": "^2.0.0", - "is-redirect": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "nested-error-stacks": "^1.0.0", - "object-assign": "^3.0.0", - "prepend-http": "^1.0.0", - "read-all-stream": "^3.0.0", - "timed-out": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/got/node_modules/object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", "engines": { "node": ">=4" } @@ -836,6 +412,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "~5.3.2", @@ -853,117 +430,40 @@ } }, "node_modules/htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "license": "MIT", "dependencies": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - } - }, - "node_modules/htmlparser2/node_modules/domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ==" - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/infinity-agent": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/infinity-agent/-/infinity-agent-2.0.3.tgz", - "integrity": "sha512-CnfUJe5o2S9aAQWXGMhDZI4UL39MAJV3guOTfHHIdos4tuVHkl1j/J+1XLQn+CLIvqcpgQR/p+xXYXzcrhCe5w==" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/inliner": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/inliner/-/inliner-1.13.1.tgz", - "integrity": "sha512-yoS+56puOu+Ug8FBRtxtTFnEn2NHqFs8BNQgSOvzh3J0ommbwNw8VKiaVNYjWK6fgPuByq95KyV0LC+qV9IwLw==", - "dependencies": { - "ansi-escapes": "^1.4.0", - "ansi-styles": "^2.2.1", - "chalk": "^1.1.3", - "charset": "^1.0.0", - "cheerio": "^0.19.0", - "debug": "^2.2.0", - "es6-promise": "^2.3.0", - "iconv-lite": "^0.4.11", - "jschardet": "^1.3.0", - "lodash.assign": "^3.2.0", - "lodash.defaults": "^3.1.2", - "lodash.foreach": "^3.0.3", - "mime": "^1.3.4", - "minimist": "^1.1.3", - "request": "^2.74.0", - "svgo": "^0.6.6", - "then-fs": "^2.0.0", - "uglify-js": "^2.8.0", - "update-notifier": "^0.5.0" - }, - "bin": { - "inliner": "cli/index.js" - } + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "license": "ISC" }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -971,34 +471,20 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1006,367 +492,77 @@ "node": ">=0.10.0" } }, - "node_modules/is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha512-9r39FIr3d+KD9SbX0sfMsHzb5PP3uimOiwr3YupUaUFG4W0l1U57Rx3utpttV7qz5U3jmrO5auUa04LU9pyHsg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } }, - "node_modules/is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, - "node_modules/js-yaml": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", - "integrity": "sha512-BLv3oxhfET+w5fjPwq3PsAsxzi9i3qzU//HMpWVz0A6KplF86HdR9x2TGnv9DXhSUrO7LO8czUiTd3yb3mLSvg==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, - "node_modules/jschardet": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.6.0.tgz", - "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/latest-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-1.0.1.tgz", - "integrity": "sha512-HERbxp4SBlmI380+eM0B0u4nxjfTaPeydIMzl9+9UQ4nSu3xMWKlX9WoT34e4wy7VWe67c53Nv9qPVjS8fHKgg==", - "dependencies": { - "package-json": "^1.0.0" - }, - "bin": { - "latest-version": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==" - }, - "node_modules/lodash._arrayeach": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", - "integrity": "sha512-Mn7HidOVcl3mkQtbPsuKR0Fj0N6Q6DQB77CtYncZcJc0bx5qv2q4Gl6a0LC1AN+GSxpnBDNnK3CKEm9XNA4zqQ==" - }, - "node_modules/lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha512-t3N26QR2IdSN+gqSy9Ds9pBu/J1EAFEshKlUHpJG3rvyJOYgcELIxcIeKKfZk7sjOz11cFfzJRsyFry/JyabJQ==", - "dependencies": { - "lodash._basecopy": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "node_modules/lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==" - }, - "node_modules/lodash._baseeach": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", - "integrity": "sha512-IqUZ9MQo2UT1XPGuBntInqTOlc+oV+bCo0kMp+yuKGsfvRSNgUW0YjWVZUrG/gs+8z/Eyuc0jkJjOBESt9BXxg==", - "dependencies": { - "lodash.keys": "^3.0.0" - } - }, - "node_modules/lodash._bindcallback": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==" - }, - "node_modules/lodash._createassigner": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", - "integrity": "sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==", - "dependencies": { - "lodash._bindcallback": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash.restparam": "^3.0.0" - } - }, - "node_modules/lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==" - }, - "node_modules/lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==" - }, - "node_modules/lodash.assign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", - "integrity": "sha512-/VVxzgGBmbphasTg51FrztxQJ/VgAUpol6zmJuSVSGcNg4g7FA4z7rQV8Ovr9V3vFBNWZhvKWHfpAytjTVUfFA==", - "dependencies": { - "lodash._baseassign": "^3.0.0", - "lodash._createassigner": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "node_modules/lodash.defaults": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", - "integrity": "sha512-X7135IXFQt5JDFnYxOVAzVz+kFvwDn3N8DJYf+nrz/mMWEuSu7+OL6rWqsk3+VR1T4TejFCSu5isBJOLSID2bg==", - "dependencies": { - "lodash.assign": "^3.0.0", - "lodash.restparam": "^3.0.0" - } - }, - "node_modules/lodash.foreach": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-3.0.3.tgz", - "integrity": "sha512-PA7Lp7pe2HMJBoB1vELegEIF3waUFnM0fWDKJVYolwZ4zHh6WTmnq0xmzfQksD66gx2quhDNyBdyaE2T8/DP3Q==", - "dependencies": { - "lodash._arrayeach": "^3.0.0", - "lodash._baseeach": "^3.0.0", - "lodash._bindcallback": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "node_modules/lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==" - }, - "node_modules/lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", - "dependencies": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "node_modules/lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==" - }, - "node_modules/longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.3" } }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" + "node": ">=4.0.0" } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" + "node": "18 || 20 || >=22" }, - "bin": { - "mkdirp": "bin/cmd.js" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/nested-error-stacks": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz", - "integrity": "sha512-o32anp9JA7oezPOFSfG2BBXSdHepOm5FpJvwxHWDtfJ3Bg3xdi68S6ijPlEOfUg6quxZWyvJM+8fHk1yMDKspA==", - "dependencies": { - "inherits": "~2.0.1" - } + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", @@ -1385,112 +581,11 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dependencies": { - "boolbase": "~1.0.0" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/package-json": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-1.2.0.tgz", - "integrity": "sha512-knDtirWWqKVJrLY3gEBLflVvueTMpyjbAwX/9j/EKi2DsjNemp5voS8cyKyGh57SNaMJNhNRZbIaWdneOcLU1g==", - "dependencies": { - "got": "^3.2.0", - "registry-url": "^3.0.0" - }, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1499,6 +594,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -1508,20 +604,17 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1529,155 +622,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dependencies": { - "asap": "~2.0.3" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/read-all-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", - "integrity": "sha512-DI1drPHbmBcUDWrJ7ull/F2Qb8HkwBncVx8/RpKYFSIACYaVRQReISYPdZz/mt1y1+qMCOrfReTopERmaxtP6w==", - "dependencies": { - "pinkie-promise": "^2.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-all-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/read-all-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/read-all-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/read-all-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "license": "MIT" }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -1685,122 +640,20 @@ "node": ">=8.10.0" } }, - "node_modules/registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", - "dependencies": { - "rc": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/repeating": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", - "integrity": "sha512-Nh30JLeMHdoI+AsQ5eblhZ7YlTsM9wiJQe/AHIunlK3KWzvXhXb36IJ7K1IOeRjIOtzMjdUHjwXUFxKJoPTSOg==", - "dependencies": { - "is-finite": "^1.0.0" - }, - "bin": { - "repeating": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", - "dependencies": { - "align-text": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1808,29 +661,11 @@ "node": ">=10" } }, - "node_modules/semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==", - "dependencies": { - "semver": "^5.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/semver-diff/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -1838,18 +673,11 @@ "node": ">=10" } }, - "node_modules/slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", - "engines": { - "node": "*" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -1858,113 +686,29 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" - }, - "node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" - }, - "node_modules/string-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", - "integrity": "sha512-MNCACnufWUf3pQ57O5WTBMkKhzYIaKEcUioO0XHrTMafrbBaNk4IyDOLHBv5xbXO0jLLdsYWeFjpjG2hVHRDtw==", - "dependencies": { - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/svgo": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.6.6.tgz", - "integrity": "sha512-C5A1r5SjFesNoKsmc+kWBxmB04iBGH2D/nFy8HJaME9+SyZKcmqcN8QG+GwxIc7D2+JWhaaW7uaM9+XwfplTEQ==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", "dependencies": { - "coa": "~1.0.1", - "colors": "~1.1.2", - "csso": "~2.0.0", - "js-yaml": "~3.6.0", - "mkdirp": "~0.5.1", - "sax": "~1.2.1", - "whet.extend": "~0.9.9" - }, - "bin": { - "svgo": "bin/svgo" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/terser": { - "version": "5.34.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.0.tgz", - "integrity": "sha512-y5NUX+U9HhVsK/zihZwoq4r9dICLyV2jXGOriDAVOeKhq3LKVjgJbGO90FisozXLlJfvjHqgckGmJFBb9KYoWQ==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -1981,28 +725,14 @@ "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/then-fs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/then-fs/-/then-fs-2.0.0.tgz", - "integrity": "sha512-5ffcBcU+vFUCYDNi/o507IqjqrTkuGsLVZ1Fp50hwgZRY7ufVFa9jFfTy5uZ2QnSKacKigWKeaXkOqLa4DsjLw==", - "dependencies": { - "promise": ">=3.2 <8" - } - }, - "node_modules/timed-out": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", - "integrity": "sha512-pqqJOi1rF5zNs/ps4vmbE4SFCrM4iR7LW+GHAsHqO/EumqbIWceioevYLM5xZRgQSH6gFgL9J/uB7EcJhQ9niQ==", - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -2014,196 +744,46 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "license": "ISC", "bin": { "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, - "node_modules/uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==", - "dependencies": { - "source-map": "~0.5.1", - "yargs": "~3.10.0" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - }, - "optionalDependencies": { - "uglify-to-browserify": "~1.0.0" - } - }, - "node_modules/uglify-js/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==", - "optional": true + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "license": "MIT" }, - "node_modules/update-notifier": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-0.5.0.tgz", - "integrity": "sha512-zOGOlUKDAgDlLHLv7Oiszz3pSj8fKlSJ3i0u49sEakjXUEVJ6DMjo/Mh/B6mg2eOALvRTJkd0kbChcipQoYCng==", - "dependencies": { - "chalk": "^1.0.0", - "configstore": "^1.0.0", - "is-npm": "^1.0.0", - "latest-version": "^1.0.0", - "repeating": "^1.1.2", - "semver-diff": "^2.0.0", - "string-length": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, - "node_modules/whet.extend": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha512-mmIPAft2vTgEILgPeZFqE/wWh24SEsR/k+N9fJ3Jxrz44iDFy9aemCxdksfURSHYFCLmvs/d/7Iso5XjPpNfrA==", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "license": "MIT", "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "slide": "^1.1.5" + "node": ">=10" } }, - "node_modules/xdg-basedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", - "integrity": "sha512-NF1pPn594TaRSUO/HARoB4jK8I+rWgcpVlpQCK6/6o5PHyLUt2CSiDrpUZbQ6rROck+W2EwF8mBJcTs+W98J9w==", + "node_modules/web-resource-inliner": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-7.0.0.tgz", + "integrity": "sha512-NlfnGF8MY9ZUwFjyq3vOUBx7KwF8bmE+ywR781SB0nWB6MoMxN4BA8gtgP1KGTZo/O/AyWJz7HZpR704eaj4mg==", + "license": "MIT", "dependencies": { - "os-homedir": "^1.0.0" + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "valid-data-url": "^3.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==", - "dependencies": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" + "node": ">=10.0.0" } } } diff --git a/package.json b/package.json index a11a135539..de017bc875 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.15.0-b7", + "version": "16.0-alpha", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { @@ -14,19 +14,19 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Aircoookie/WLED.git" + "url": "git+https://github.com/wled/WLED.git" }, "author": "", "license": "ISC", "bugs": { - "url": "https://github.com/Aircoookie/WLED/issues" + "url": "https://github.com/wled/WLED/issues" }, - "homepage": "https://github.com/Aircoookie/WLED#readme", + "homepage": "https://github.com/wled/WLED#readme", "dependencies": { "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", - "inliner": "^1.13.1", - "nodemon": "^3.1.7" + "web-resource-inliner": "^7.0.0", + "nodemon": "^3.1.14" }, "engines": { "node": ">=20.0.0" diff --git a/pio-scripts/build_ui.py b/pio-scripts/build_ui.py index f3688a5d4a..eb7a01b366 100644 --- a/pio-scripts/build_ui.py +++ b/pio-scripts/build_ui.py @@ -1,3 +1,21 @@ -Import('env') +Import("env") +import shutil -env.Execute("npm run build") \ No newline at end of file +node_ex = shutil.which("node") +# Check if Node.js is installed and present in PATH if it failed, abort the build +if node_ex is None: + print('\x1b[0;31;43m' + 'Node.js is not installed or missing from PATH html css js will not be processed check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m') + exitCode = env.Execute("null") + exit(exitCode) +else: + # Install the necessary node packages for the pre-build asset bundling script + print('\x1b[6;33;42m' + 'Installing node packages' + '\x1b[0m') + env.Execute("npm ci") + + # Call the bundling script + exitCode = env.Execute("npm run build") + + # If it failed, abort the build + if (exitCode): + print('\x1b[0;31;43m' + 'npm run build fails check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m') + exit(exitCode) diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py new file mode 100644 index 0000000000..38a08401e6 --- /dev/null +++ b/pio-scripts/load_usermods.py @@ -0,0 +1,107 @@ +Import('env') +from collections import deque +from pathlib import Path # For OS-agnostic path manipulation +from click import secho +from SCons.Script import Exit +from platformio.builder.tools.piolib import LibBuilderBase + +usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods" + +# Utility functions +def find_usermod(mod: str) -> Path: + """Locate this library in the usermods folder. + We do this to avoid needing to rename a bunch of folders; + this could be removed later + """ + # Check name match + mp = usermod_dir / mod + if mp.exists(): + return mp + mp = usermod_dir / f"{mod}_v2" + if mp.exists(): + return mp + mp = usermod_dir / f"usermod_v2_{mod}" + if mp.exists(): + return mp + raise RuntimeError(f"Couldn't locate module {mod} in usermods directory!") + +def is_wled_module(dep: LibBuilderBase) -> bool: + """Returns true if the specified library is a wled module + """ + return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-") + +## Script starts here +# Process usermod option +usermods = env.GetProjectOption("custom_usermods","") + +# Handle "all usermods" case +if usermods == '*': + usermods = [f.name for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()] +else: + usermods = usermods.split() + +if usermods: + # Inject usermods in to project lib_deps + symlinks = [f"symlink://{find_usermod(mod).resolve()}" for mod in usermods] + env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + symlinks) + +# Utility function for assembling usermod include paths +def cached_add_includes(dep, dep_cache: set, includes: deque): + """ Add dep's include paths to includes if it's not in the cache """ + if dep not in dep_cache: + dep_cache.add(dep) + for include in dep.get_include_dirs(): + if include not in includes: + includes.appendleft(include) + if usermod_dir not in Path(dep.src_dir).parents: + # Recurse, but only for NON-usermods + for subdep in dep.depbuilders: + cached_add_includes(subdep, dep_cache, includes) + +# Monkey-patch ConfigureProjectLibBuilder to mark up the dependencies +# Save the old value +old_ConfigureProjectLibBuilder = env.ConfigureProjectLibBuilder + +# Our new wrapper +def wrapped_ConfigureProjectLibBuilder(xenv): + # Call the wrapped function + result = old_ConfigureProjectLibBuilder.clone(xenv)() + + # Fix up include paths + # In PlatformIO >=6.1.17, this could be done prior to ConfigureProjectLibBuilder + wled_dir = xenv["PROJECT_SRC_DIR"] + # Build a list of dependency include dirs + # TODO: Find out if this is the order that PlatformIO/SCons puts them in?? + processed_deps = set() + extra_include_dirs = deque() # Deque used for fast prepend + for dep in result.depbuilders: + cached_add_includes(dep, processed_deps, extra_include_dirs) + + wled_deps = [dep for dep in result.depbuilders if is_wled_module(dep)] + + broken_usermods = [] + for dep in wled_deps: + # Add the wled folder to the include path + dep.env.PrependUnique(CPPPATH=str(wled_dir)) + # Add WLED's own dependencies + for dir in extra_include_dirs: + dep.env.PrependUnique(CPPPATH=str(dir)) + # Enforce that libArchive is not set; we must link them directly to the executable + if dep.lib_archive: + broken_usermods.append(dep) + + if broken_usermods: + broken_usermods = [usermod.name for usermod in broken_usermods] + secho( + f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- modules will not compile in correctly", + fg="red", + err=True) + Exit(1) + + # Save the depbuilders list for later validation + xenv.Replace(WLED_MODULES=wled_deps) + + return result + +# Apply the wrapper +env.AddMethod(wrapped_ConfigureProjectLibBuilder, "ConfigureProjectLibBuilder") diff --git a/pio-scripts/output_bins.py b/pio-scripts/output_bins.py index 4d1594d843..d369ed7927 100644 --- a/pio-scripts/output_bins.py +++ b/pio-scripts/output_bins.py @@ -2,6 +2,7 @@ import os import shutil import gzip +import json OUTPUT_DIR = "build_output{}".format(os.path.sep) #OUTPUT_DIR = os.path.join("build_output") @@ -22,7 +23,8 @@ def create_release(source): release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME") if release_name_def: release_name = release_name_def.replace("\\\"", "") - version = _get_cpp_define_value(env, "WLED_VERSION") + with open("package.json", "r") as package: + version = json.load(package)["version"] release_file = os.path.join(OUTPUT_DIR, "release", f"WLED_{version}_{release_name}.bin") release_gz_file = release_file + ".gz" print(f"Copying {source} to {release_file}") diff --git a/pio-scripts/set_metadata.py b/pio-scripts/set_metadata.py new file mode 100644 index 0000000000..7c8c223038 --- /dev/null +++ b/pio-scripts/set_metadata.py @@ -0,0 +1,116 @@ +Import('env') +import subprocess +import json +import re + +def get_github_repo(): + """Extract GitHub repository name from git remote URL. + + Uses the remote that the current branch tracks, falling back to 'origin'. + This handles cases where repositories have multiple remotes or where the + main remote is not named 'origin'. + + Returns: + str: Repository name in 'owner/repo' format for GitHub repos, + 'unknown' for non-GitHub repos, missing git CLI, or any errors. + """ + try: + remote_name = 'origin' # Default fallback + + # Try to get the remote for the current branch + try: + # Get current branch name + branch_result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + capture_output=True, text=True, check=True) + current_branch = branch_result.stdout.strip() + + # Get the remote for the current branch + remote_result = subprocess.run(['git', 'config', f'branch.{current_branch}.remote'], + capture_output=True, text=True, check=True) + tracked_remote = remote_result.stdout.strip() + + # Use the tracked remote if we found one + if tracked_remote: + remote_name = tracked_remote + except subprocess.CalledProcessError: + # If branch config lookup fails, continue with 'origin' as fallback + pass + + # Get the remote URL for the determined remote + result = subprocess.run(['git', 'remote', 'get-url', remote_name], + capture_output=True, text=True, check=True) + remote_url = result.stdout.strip() + + # Check if it's a GitHub URL + if 'github.com' not in remote_url.lower(): + return None + + # Parse GitHub URL patterns: + # https://github.com/owner/repo.git + # git@github.com:owner/repo.git + # https://github.com/owner/repo + + # Remove .git suffix if present + if remote_url.endswith('.git'): + remote_url = remote_url[:-4] + + # Handle HTTPS URLs + https_match = re.search(r'github\.com/([^/]+/[^/]+)', remote_url, re.IGNORECASE) + if https_match: + return https_match.group(1) + + # Handle SSH URLs + ssh_match = re.search(r'github\.com:([^/]+/[^/]+)', remote_url, re.IGNORECASE) + if ssh_match: + return ssh_match.group(1) + + return None + + except FileNotFoundError: + # Git CLI is not installed or not in PATH + return None + except subprocess.CalledProcessError: + # Git command failed (e.g., not a git repo, no remote, etc.) + return None + except Exception: + # Any other unexpected error + return None + +# WLED version is managed by package.json; this is picked up in several places +# - It's integrated in to the UI code +# - Here, for wled_metadata.cpp +# - The output_bins script +# We always take it from package.json to ensure consistency +with open("package.json", "r") as package: + WLED_VERSION = json.load(package)["version"] + +def has_def(cppdefs, name): + """ Returns true if a given name is set in a CPPDEFINES collection """ + for f in cppdefs: + if isinstance(f, tuple): + f = f[0] + if f == name: + return True + return False + + +def add_wled_metadata_flags(env, node): + cdefs = env["CPPDEFINES"].copy() + + if not has_def(cdefs, "WLED_REPO"): + repo = get_github_repo() + if repo: + cdefs.append(("WLED_REPO", f"\\\"{repo}\\\"")) + + cdefs.append(("WLED_VERSION", WLED_VERSION)) + + # This transforms the node in to a Builder; it cannot be modified again + return env.Object( + node, + CPPDEFINES=cdefs + ) + +env.AddBuildMiddleware( + add_wled_metadata_flags, + "*/wled_metadata.cpp" +) diff --git a/pio-scripts/set_version.py b/pio-scripts/set_version.py deleted file mode 100644 index 1d8e076ea8..0000000000 --- a/pio-scripts/set_version.py +++ /dev/null @@ -1,8 +0,0 @@ -Import('env') -import json - -PACKAGE_FILE = "package.json" - -with open(PACKAGE_FILE, "r") as package: - version = json.load(package)["version"] - env.Append(BUILD_FLAGS=[f"-DWLED_VERSION={version}"]) diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py new file mode 100644 index 0000000000..d63b609ac8 --- /dev/null +++ b/pio-scripts/validate_modules.py @@ -0,0 +1,80 @@ +import re +from pathlib import Path # For OS-agnostic path manipulation +from typing import Iterable +from click import secho +from SCons.Script import Action, Exit +from platformio.builder.tools.piolib import LibBuilderBase + + +def is_wled_module(env, dep: LibBuilderBase) -> bool: + """Returns true if the specified library is a wled module + """ + usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods" + return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-") + + +def read_lines(p: Path): + """ Read in the contents of a file for analysis """ + with p.open("r", encoding="utf-8", errors="ignore") as f: + return f.readlines() + + +def check_map_file_objects(map_file: list[str], dirs: Iterable[str]) -> set[str]: + """ Identify which dirs contributed to the final build + + Returns the (sub)set of dirs that are found in the output ELF + """ + # Pattern to match symbols in object directories + # Join directories into alternation + usermod_dir_regex = "|".join([re.escape(dir) for dir in dirs]) + # Matches nonzero address, any size, and any path in a matching directory + object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o") + + found = set() + for line in map_file: + matches = object_path_regex.findall(line) + for m in matches: + found.add(m) + return found + + +def count_usermod_objects(map_file: list[str]) -> int: + """ Returns the number of usermod objects in the usermod list """ + # Count the number of entries in the usermods table section + return len([x for x in map_file if ".dtors.tbl.usermods.1" in x]) + + +def validate_map_file(source, target, env): + """ Validate that all modules appear in the output build """ + build_dir = Path(env.subst("$BUILD_DIR")) + map_file_path = build_dir / env.subst("${PROGNAME}.map") + + if not map_file_path.exists(): + secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True) + Exit(1) + + # Identify the WLED module builders, set by load_usermods.py + module_lib_builders = env['WLED_MODULES'] + + # Extract the values we care about + modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders} + secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules") + + # Now parse the map file + map_file_contents = read_lines(map_file_path) + usermod_object_count = count_usermod_objects(map_file_contents) + secho(f"INFO: {usermod_object_count} usermod object entries") + + confirmed_modules = check_map_file_objects(map_file_contents, modules.keys()) + missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules] + if missing_modules: + secho( + f"ERROR: No object files from {missing_modules} found in linked output!", + fg="red", + err=True) + Exit(1) + return None + +Import("env") +env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) +env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file')) diff --git a/platformio.ini b/platformio.ini index fe02213ff2..60dedd473b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,28 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover +default_envs = nodemcuv2 + esp8266_2m + esp01_1m_full + nodemcuv2_160 + esp8266_2m_160 + esp01_1m_full_160 + nodemcuv2_compat + esp8266_2m_compat + esp01_1m_full_compat + esp32dev + esp32dev_debug + esp32_eth + esp32_wrover + lolin_s2_mini + esp32c3dev + esp32c3dev_qio + esp32S3_wroom2 + esp32s3dev_16MB_opi + esp32s3dev_8MB_opi + esp32s3dev_8MB_qspi + esp32s3_4M_qspi + usermods src_dir = ./wled00 data_dir = ./wled00/data @@ -100,6 +121,7 @@ build_flags = -D DECODE_SAMSUNG=true -D DECODE_LG=true -DWLED_USE_MY_CONFIG + -D WLED_PS_DONT_REPLACE_FX ; PS replacement FX are purely a flash memory saving feature, do not replace classic FX until we run out of flash build_unflags = @@ -110,11 +132,13 @@ ldscript_4m1m = eagle.flash.4m1m.ld [scripts_defaults] extra_scripts = - pre:pio-scripts/set_version.py + pre:pio-scripts/set_metadata.py post:pio-scripts/output_bins.py post:pio-scripts/strip-floats.py pre:pio-scripts/user_config_copy.py + pre:pio-scripts/load_usermods.py pre:pio-scripts/build_ui.py + post:pio-scripts/validate_modules.py ;; double-check the build output usermods ; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging) # ------------------------------------------------------------------------------ @@ -138,9 +162,9 @@ lib_compat_mode = strict lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.8.0 - #https://github.com/makuna/NeoPixelBus.git#CoreShaderBeta - https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1 + https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2 + marvinroger/AsyncMqttClient @ 0.9.0 # for I2C interface ;Wire # ESP-NOW library @@ -157,25 +181,18 @@ lib_deps = ;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 #For MAX1704x Lipo Monitor / Fuel Gauge uncomment following ; https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 ; https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 #For MPU6050 IMU uncomment follwoing ;electroniccats/MPU6050 @1.0.1 - # For -D USERMOD_ANIMARTRIX - # CC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if you accept the terms! - ;https://github.com/netmindz/animartrix.git#18bf17389e57c69f11bc8d04ebe1d215422c7fb7 # SHT85 ;robtillaart/SHT85@~0.3.3 - # Audioreactive usermod - ;kosme/arduinoFFT @ 2.0.1 extra_scripts = ${scripts_defaults.extra_scripts} [esp8266] +build_unflags = ${common.build_unflags} build_flags = -DESP8266 -DFP_IN_IROM @@ -235,104 +252,114 @@ lib_deps_compat = IRremoteESP8266 @ 2.8.2 makuna/NeoPixelBus @ 2.7.9 https://github.com/blazoncek/QuickESPNow.git#optional-debug - https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1 + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 +[esp32_all_variants] +lib_deps = + esp32async/AsyncTCP @ 3.4.7 + bitbank2/AnimatedGIF@^1.4.7 + https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e +build_flags = + -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D CONFIG_ASYNC_TCP_STACK_SIZE=8192 + -D WLED_ENABLE_GIF [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 +platform = ${esp32_idf_V4.platform} +platform_packages = +build_unflags = ${common.build_unflags} +build_flags = ${esp32_idf_V4.build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv big_partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; 1.8MB firmware, 256KB filesystem, coredump support large_partitions = tools/WLED_ESP32_8MB.csv extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv -lib_deps = - https://github.com/lorol/LITTLEFS.git - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 - ${env.lib_deps} -# additional build flags for audioreactive -AR_build_flags = -D USERMOD_AUDIOREACTIVE - -D sqrt_internal=sqrtf ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster) -AR_lib_deps = kosme/arduinoFFT @ 2.0.1 + +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs +# additional build flags for audioreactive - must be applied globally +AR_build_flags = ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster) +AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility + [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. +;; build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 +;; *** important: build flags from esp32_idf_V4 are inherited by _all_ esp32-based MCUs: esp32, esp32s2, esp32s3, esp32c3 ;; ;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly. ;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio. -platform = espressif32@ ~6.3.2 -platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) + +platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.00/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.18 with IPv6 support, based on IDF 4.4.8 +platform_packages = +build_unflags = ${common.build_unflags} build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 - -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 + ${esp32_all_variants.build_flags} + -D WLED_ENABLE_DMX_INPUT lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} + https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0 ${env.lib_deps} [esp32s2] ;; generic definitions for all ESP32-S2 boards -platform = espressif32@ ~6.3.2 -platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} 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 + ${esp32_idf_V4.build_flags} lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 - ${env.lib_deps} + ${esp32_idf_V4.lib_deps} +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs [esp32c3] ;; generic definitions for all ESP32-C3 boards -platform = espressif32@ ~6.3.2 -platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} 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 + ${esp32_idf_V4.build_flags} lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 - ${env.lib_deps} + ${esp32_idf_V4.lib_deps} +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs +board_build.flash_mode = qio [esp32s3] ;; generic definitions for all ESP32-S3 boards -platform = espressif32@ ~6.3.2 -platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} 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 + ${esp32_idf_V4.build_flags} lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 - ${env.lib_deps} + ${esp32_idf_V4.lib_deps} +board_build.partitions = ${esp32.large_partitions} ;; default partioning for 8MB flash - can be overridden in build envs # ------------------------------------------------------------------------------ @@ -346,6 +373,7 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder @@ -355,13 +383,15 @@ extends = env:nodemcuv2 platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D ;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9 [env:nodemcuv2_160] extends = env:nodemcuv2 board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_160\" #-DWLED_DISABLE_2D - -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM2D +custom_usermods = audioreactive [env:esp8266_2m] board = esp_wroom_02 @@ -370,6 +400,8 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02\" + -D WLED_DISABLE_PARTICLESYSTEM2D + -D WLED_DISABLE_PARTICLESYSTEM1D lib_deps = ${esp8266.lib_deps} [env:esp8266_2m_compat] @@ -378,12 +410,16 @@ extends = env:esp8266_2m platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp8266_2m_160] extends = env:esp8266_2m board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_160\" - -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D +custom_usermods = audioreactive [env:esp01_1m_full] board = esp01_1m @@ -393,6 +429,8 @@ board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01\" -D WLED_DISABLE_OTA ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full_compat] @@ -401,51 +439,63 @@ extends = env:esp01_1m_full platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP01_compat\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp01_1m_full_160] extends = env:esp01_1m_full board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01_160\" -D WLED_DISABLE_OTA - -D USERMOD_AUDIOREACTIVE ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D +custom_usermods = audioreactive [env:esp32dev] board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET - ${esp32.AR_build_flags} -lib_deps = ${esp32.lib_deps} - ${esp32.AR_lib_deps} +custom_usermods = audioreactive +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET + -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 +lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} +board_build.flash_mode = dio + +[env:esp32dev_debug] +extends = env:esp32dev +upload_speed = 921600 +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} + -D WLED_DEBUG + -D WLED_RELEASE_NAME=\"ESP32_DEBUG\" [env:esp32dev_8M] board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_8M\" #-D WLED_DISABLE_BROWNOUT_DET - ${esp32.AR_build_flags} + -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32_idf_V4.lib_deps} - ${esp32.AR_lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.large_partitions} board_upload.flash_size = 8MB board_upload.maximum_size = 8388608 ; board_build.f_flash = 80000000L -; board_build.flash_mode = qio +board_build.flash_mode = dio [env:esp32dev_16M] board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-D WLED_DISABLE_BROWNOUT_DET - ${esp32.AR_build_flags} + -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32_idf_V4.lib_deps} - ${esp32.AR_lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.extreme_partitions} board_upload.flash_size = 16MB @@ -453,49 +503,36 @@ board_upload.maximum_size = 16777216 board_build.f_flash = 80000000L board_build.flash_mode = dio -;[env:esp32dev_audioreactive] -;board = esp32dev -;platform = ${esp32.platform} -;platform_packages = ${esp32.platform_packages} -;build_unflags = ${common.build_unflags} -;build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_audioreactive\" #-D WLED_DISABLE_BROWNOUT_DET -; ${esp32.AR_build_flags} -;lib_deps = ${esp32.lib_deps} -; ${esp32.AR_lib_deps} -;monitor_filters = esp32_exception_decoder -;board_build.partitions = ${esp32.default_partitions} -;; board_build.f_flash = 80000000L -;; board_build.flash_mode = dio - [env:esp32_eth] board = esp32-poe -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_Ethernet\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only - ${esp32.AR_build_flags} + -D SR_DMTYPE=-1 -D AUDIOPIN=-1 -D I2S_SDPIN=-1 -D I2S_WSPIN=-1 -D I2S_CKPIN=-1 -D MCLK_PIN=-1 ;; force AR to not allocate any PINs at startup + -D DATA_PINS=4 ;; default led pin = 16 conflicts with pins used for ethernet + ; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only => uncomment if your board uses ETH_CLOCK_GPIO0_OUT, ETH_CLOCK_GPIO16_OUT, ETH_CLOCK_GPIO17_OUT + -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.default_partitions} +board_build.flash_mode = dio [env:esp32_wrover] extends = esp32_idf_V4 -platform = ${esp32_idf_V4.platform} -platform_packages = ${esp32_idf_V4.platform_packages} board = ttgo-t7-v14-mini32 board_build.f_flash = 80000000L board_build.flash_mode = qio board_build.partitions = ${esp32.extended_partitions} +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\" + -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html -D DATA_PINS=25 - ${esp32.AR_build_flags} lib_deps = ${esp32_idf_V4.lib_deps} - ${esp32.AR_lib_deps} - + [env:esp32c3dev] extends = esp32c3 platform = ${esp32c3.platform} @@ -511,6 +548,12 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME= upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} +board_build.flash_mode = dio ; safe default, required for OTA updates to 0.16 from older version which used dio (must match the bootloader!) + +[env:esp32c3dev_qio] +extends = env:esp32c3dev +build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-C3-QIO\" +board_build.flash_mode = qio ; qio is faster and works on almost all boards (some boards may use dio to get 2 extra pins) [env:esp32s3dev_16MB_opi] ;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) @@ -519,15 +562,14 @@ board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi\" - -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip - -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM - ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.extreme_partitions} board_upload.flash_size = 16MB board_upload.maximum_size = 16777216 @@ -542,20 +584,32 @@ board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_opi\" - -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip - -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM - ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder +[env:esp32s3dev_8MB_qspi] +;; generic ESP32-S3 board with 8MB FLASH and PSRAM (memory_type: qio_qspi). Try this one if esp32s3dev_8MB_opi does not work on your board +extends = env:esp32s3dev_8MB_opi +board_build.arduino.memory_type = qio_qspi +board_build.flash_mode = qio +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_qspi\" + -D WLED_WATCHDOG_TIMEOUT=0 + ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip + -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + ;; -DLOLIN_WIFI_FIX ;; uncomment if you have WiFi connectivity problems +monitor_filters = esp32_exception_decoder + [env:esp32S3_wroom2] ;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1 ;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi) @@ -564,40 +618,55 @@ platform_packages = ${esp32s3.platform_packages} board = esp32s3camlcd ;; this is the only standard board with "opi_opi" board_build.arduino.memory_type = opi_opi upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2\" - -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D WLED_WATCHDOG_TIMEOUT=0 -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip - ;; -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + ;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM -D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1 - -D WLED_DEBUG - ${esp32.AR_build_flags} + ;;-D WLED_DEBUG -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic lib_deps = ${esp32s3.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.extreme_partitions} board_upload.flash_size = 16MB board_upload.maximum_size = 16777216 monitor_filters = esp32_exception_decoder +[env:esp32S3_wroom2_32MB] +;; For ESP32-S3 WROOM-2 with 32MB Flash, and >= 8MB PSRAM (memory_type: opi_opi) +extends = env:esp32S3_wroom2 +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2_32MB\" + -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip + ;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + -D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED + -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1 + ;;-D WLED_DEBUG + -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic +board_build.partitions = tools/WLED_ESP32_32MB.csv +board_upload.flash_size = 32MB +board_upload.maximum_size = 33554432 +monitor_filters = esp32_exception_decoder + [env:esp32s3_4M_qspi] ;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi) board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\" - -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this -D WLED_WATCHDOG_TIMEOUT=0 - ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio @@ -610,6 +679,7 @@ board = lolin_s2_mini board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = qio board_build.f_flash = 80000000L +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S2\" -DARDUINO_USB_CDC_ON_BOOT=1 @@ -618,7 +688,6 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME= -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this -D WLED_WATCHDOG_TIMEOUT=0 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -D DATA_PINS=16 -D HW_PIN_SCL=35 -D HW_PIN_SDA=33 @@ -626,6 +695,18 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME= -D HW_PIN_DATASPI=11 -D HW_PIN_MISOSPI=9 ; -D STATUSLED=15 - ${esp32.AR_build_flags} lib_deps = ${esp32s2.lib_deps} - ${esp32.AR_lib_deps} + + +[env:usermods] +board = esp32dev +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\" + -DTOUCH_CS=9 +lib_deps = ${esp32_idf_V4.lib_deps} +monitor_filters = esp32_exception_decoder +board_build.flash_mode = dio +custom_usermods = * ; Expands to all usermods in usermods folder +board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat diff --git a/platformio_override.ini b/platformio_override.ini new file mode 100644 index 0000000000..3dda38011e --- /dev/null +++ b/platformio_override.ini @@ -0,0 +1,30 @@ +[platformio] +default_envs = + gledopto + gledopto_eth + +[custom_flags] +build_flags = + -D WLED_BRAND="\"GLEDOPTO\"" + -D SERVERNAME='"WLED-Gledopto"' + +[env:gledopto] +extends = env:esp32dev +build_unflags = ${env:esp32dev.build_unflags} + -D WLED_RELEASE_NAME +build_flags = ${env:esp32dev.build_flags} + ${custom_flags.build_flags} + -D SERVERNAME='"WLED-Gledopto"' + -D WLED_PRODUCT_NAME="\"GLED\"" + -D WLED_RELEASE_NAME="\"ESP32\"" + +[env:gledopto_eth] +extends = env:esp32dev +build_unflags = ${env:esp32dev.build_unflags} + -D WLED_RELEASE_NAME +build_flags = ${env:esp32dev.build_flags} + ${custom_flags.build_flags} + -D WLED_PRODUCT_NAME="\"GLED_Ethernet\"" + -D WLED_RELEASE_NAME="\"ESP32_Ethernet\"" +# Gledopto Ethernet + -D WLED_ETH_DEFAULT=13 diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index dd3630d82c..2fc9aacfb4 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -5,7 +5,7 @@ # Please visit documentation: https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = WLED_tasmota_1M # define as many as you need +default_envs = WLED_generic8266_1M, esp32dev_V4_dio80 # put the name(s) of your own build environment here. You can define as many as you need #---------- # SAMPLE @@ -28,13 +28,11 @@ lib_deps = ${esp8266.lib_deps} ; robtillaart/SHT85@~0.3.3 ; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug ; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library -; ${esp32.AR_lib_deps} ;; used for USERMOD_AUDIOREACTIVE -; bitbank2/PNGdec@^1.0.1 ;; used for POV display uncomment following build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} ; -; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. +; *** To use the below defines/overrides, copy and paste each onto its own line just below build_flags in the section above. ; ; Set a release name that may be used to distinguish required binary for flashing ; -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\" @@ -141,7 +139,6 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; -D PIR_SENSOR_MAX_SENSORS=2 # max allowable sensors (uses OR logic for triggering) ; ; Use Audioreactive usermod and configure I2S microphone -; -D USERMOD_AUDIOREACTIVE ; -D AUDIOPIN=-1 ; -D DMTYPE=1 # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM ; -D I2S_SDPIN=36 @@ -157,17 +154,22 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; -D USERMOD_POV_DISPLAY ; Use built-in or custom LED as a status indicator (assumes LED is connected to GPIO16) ; -D STATUSLED=16 -; +; ; 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 +; -D PIXEL_COUNTS=30 ; or this for multiple outputs ; -D PIXEL_COUNTS=30,30 ; ; set the default LED type -; -D DEFAULT_LED_TYPE=22 # see const.h (TYPE_xxxx) +; -D LED_TYPES=22 # see const.h (TYPE_xxxx) +; or this for multiple outputs +; -D LED_TYPES=TYPE_SK6812_RGBW,TYPE_WS2812_RGB +; +; set default color order of your led strip +; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB ; ; set milliampere limit when using ESP power pin (or inadequate PSU) to power LEDs ; -D ABL_MILLIAMPS_DEFAULT=850 @@ -175,9 +177,6 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; ; 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 ; ; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues) ; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue # needed only for classic ESP32 rev.1 @@ -192,6 +191,22 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; -D HW_PIN_MISOSPI=9 +# ------------------------------------------------------------------------------ +# Optional: build flags for speed, instead of optimising for size. +# Example of usage: see [env:esp32S3_PSRAM_HUB75] +# ------------------------------------------------------------------------------ + +[Speed_Flags] +build_unflags = -Os ;; to disable standard optimization for small size +build_flags = + -O2 ;; optimize for speed + -free -fipa-pta ;; very useful, too + ;;-fsingle-precision-constant ;; makes all floating point literals "float" (default is "double") + ;;-funsafe-math-optimizations ;; less dangerous than -ffast-math; still allows the compiler to exploit FMA and reciprocals (up to 10% faster on -S3) + # Important: we need to explicitly switch off some "-O2" optimizations + -fno-jump-tables -fno-tree-switch-conversion ;; needed - firmware may crash otherwise + -freorder-blocks -Wwrite-strings -fstrict-volatile-bitfields ;; needed - recommended by espressif + # ------------------------------------------------------------------------------ # PRE-CONFIGURED DEVELOPMENT BOARDS AND CONTROLLERS @@ -236,14 +251,11 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=1 -D WLE lib_deps = ${esp8266.lib_deps} [env:esp32dev_qio80] +extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options) board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} -build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} #-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 @@ -251,26 +263,23 @@ board_build.flash_mode = qio ;; experimental ESP32 env using ESP-IDF V4.4.x ;; Warning: this build environment is not stable!! ;; please erase your device before installing. +extends = esp32_idf_V4 # based on newer "esp-idf V4" platform environment board = esp32dev -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_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.partitions = ${esp32.default_partitions} ;; if you get errors about "out of program space", change this to ${esp32.extended_partitions} or even ${esp32.big_partitions} board_build.f_flash = 80000000L board_build.flash_mode = dio [env:esp32s2_saola] +extends = esp32s2 board = esp32-s2-saola-1 platform = ${esp32s2.platform} platform_packages = ${esp32s2.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} ;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work -DARDUINO_USB_CDC_ON_BOOT=1 @@ -279,7 +288,7 @@ lib_deps = ${esp32s2.lib_deps} [env:esp32s3dev_8MB_PSRAM_qspi] ;; ESP32-TinyS3 development board, with 8MB FLASH and PSRAM (memory_type: qio_qspi) extends = env:esp32s3dev_8MB_PSRAM_opi -;board = um_tinys3 ; -> needs workaround from https://github.com/Aircoookie/WLED/pull/2905#issuecomment-1328049860 +;board = um_tinys3 ; -> needs workaround from https://github.com/wled-dev/WLED/pull/2905#issuecomment-1328049860 board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB @@ -307,7 +316,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB ;; NB: WLED_USE_SHOJO_PCB is not used anywhere in the source code. Not sure why its needed. lib_deps = ${esp8266.lib_deps} [env:d1_mini_debug] @@ -362,35 +371,44 @@ board_upload.flash_size = 2MB board_upload.maximum_size = 2097152 [env:wemos_shield_esp32] +extends = esp32 ;; use default esp32 platform board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} upload_speed = 460800 -build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} + -D WLED_RELEASE_NAME=\"ESP32_wemos_shield\" -D DATA_PINS=16 -D RLYPIN=19 -D BTNPIN=17 -D IRPIN=18 - -D UWLED_USE_MY_CONFIG + -UWLED_USE_MY_CONFIG -D USERMOD_DALLASTEMPERATURE -D USERMOD_FOUR_LINE_DISPLAY -D TEMPERATURE_PIN=23 - -D USERMOD_AUDIOREACTIVE lib_deps = ${esp32.lib_deps} - OneWire@~2.3.5 - olikraus/U8g2 @ ^2.28.8 - https://github.com/blazoncek/arduinoFFT.git + OneWire@~2.3.5 ;; needed for USERMOD_DALLASTEMPERATURE + olikraus/U8g2 @ ^2.28.8 ;; needed for USERMOD_FOUR_LINE_DISPLAY board_build.partitions = ${esp32.default_partitions} -[env:m5atom] -board = esp32dev -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39 +[env:esp32_pico-D4] +extends = esp32 ;; use default esp32 platform +board = pico32 ;; pico32-D4 is different from the standard esp32dev + ;; hardware details from https://github.com/srg74/WLED-ESP32-pico +build_flags = ${common.build_flags} ${esp32.build_flags} + -D WLED_RELEASE_NAME=\"pico32-D4\" -D SERVERNAME='"WLED-pico32"' + -D WLED_DISABLE_ADALIGHT ;; no serial-to-USB chip on this board - better to disable serial protocols + -D DATA_PINS=2,18 ;; LED pins + -D RLYPIN=19 -D BTNPIN=0 -D IRPIN=-1 ;; no default pin for IR + -D UM_AUDIOREACTIVE_ENABLE ;; enable AR by default + ;; Audioreactive settings for on-board microphone (ICS-43432) + -D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14 + -D SR_SQUELCH=5 -D SR_GAIN=30 lib_deps = ${esp32.lib_deps} -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} board_build.partitions = ${esp32.default_partitions} +board_build.f_flash = 80000000L + +[env:m5atom] +extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options) +build_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39 [env:sp501e] board = esp_wroom_02 @@ -413,7 +431,7 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-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 + -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 lib_deps = ${esp8266.lib_deps} [env:Athom_15w_RGBCW] ;15w bulb @@ -423,7 +441,7 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-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 + -D LED_TYPES=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 @@ -489,17 +507,15 @@ lib_deps = ${esp8266.lib_deps} # EleksTube-IPS # ------------------------------------------------------------------------------ [env:elekstube_ips] +extends = esp32 ;; use default esp32 platform board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} upload_speed = 921600 +custom_usermods = ${env:esp32dev.custom_usermods} RTC EleksTube_IPS build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED - -D USERMOD_RTC - -D USERMOD_ELEKSTUBE_IPS -D DATA_PINS=12 -D RLYPIN=27 -D BTNPIN=34 - -D DEFAULT_LED_COUNT=6 + -D PIXEL_COUNTS=6 # Display config -D ST7789_DRIVER -D TFT_WIDTH=135 @@ -513,7 +529,115 @@ build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOU -D SPI_FREQUENCY=40000000 -D USER_SETUP_LOADED monitor_filters = esp32_exception_decoder -lib_deps = - ${esp32.lib_deps} - TFT_eSPI @ ^2.3.70 + + +# ------------------------------------------------------------------------------ +# Usermod examples +# ------------------------------------------------------------------------------ + +# 433MHz RF remote example for esp32dev +[env:esp32dev_usermod_RF433] +extends = env:esp32dev +build_flags = ${env:esp32dev.build_flags} -D USERMOD_RF433 +lib_deps = ${env:esp32dev.lib_deps} + sui77/rc-switch @ 2.6.4 + +# ------------------------------------------------------------------------------ +# Hub75 examples +# ------------------------------------------------------------------------------ +# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it. + +[env:esp32dev_hub75] +board = esp32dev +upload_speed = 921600 +platform = ${esp32_idf_V4.platform} +platform_packages = +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} + -D WLED_RELEASE_NAME=\"ESP32_hub75\" + -D WLED_ENABLE_HUB75MATRIX -D NO_GFX + -D WLED_DEBUG_BUS + ; -D WLED_DEBUG + -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash + +lib_deps = ${esp32_idf_V4.lib_deps} + https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11 + +monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} +board_build.flash_mode = dio +custom_usermods = audioreactive + +[env:esp32dev_hub75_forum_pinout] +extends = env:esp32dev_hub75 +build_flags = ${common.build_flags} + -D WLED_RELEASE_NAME=\"ESP32_hub75_forum_pinout\" + -D WLED_ENABLE_HUB75MATRIX -D NO_GFX + -D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins + -D WLED_DEBUG_BUS + -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash +; -D WLED_DEBUG + + +[env:adafruit_matrixportal_esp32s3] +; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75 +board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload +;; adafruit recommends to use arduino-esp32 2.0.14 +;;platform = espressif32@ ~6.5.0 +;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14 +platform = ${esp32s3.platform} +platform_packages = +upload_speed = 921600 +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8M_qspi\" + -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + -DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power) + -D WLED_WATCHDOG_TIMEOUT=0 + -D WLED_ENABLE_HUB75MATRIX -D NO_GFX + -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips + -D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3 + -D WLED_DEBUG_BUS + -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash + + +lib_deps = ${esp32s3.lib_deps} + https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix + +board_build.partitions = ${esp32.large_partitions} ;; standard bootloader and 8MB Flash partitions +;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader +board_build.f_flash = 80000000L +board_build.flash_mode = qio +monitor_filters = esp32_exception_decoder +custom_usermods = audioreactive + +[env:esp32S3_PSRAM_HUB75] +;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and PSRAM) +board = lilygo-t7-s3 +platform = ${esp32s3.platform} +platform_packages = +upload_speed = 921600 +build_unflags = ${common.build_unflags} + ${Speed_Flags.build_unflags} ;; optional: removes "-Os" so we can override with "-O2" in build_flags +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"esp32S3_16MB_PSRAM_HUB75\" + ${Speed_Flags.build_flags} ;; optional: -O2 -> optimize for speed instead of size + -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + -DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power) + -D WLED_WATCHDOG_TIMEOUT=0 + -D WLED_ENABLE_HUB75MATRIX -D NO_GFX + -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips + -D MOONHUB_S3_PINOUT ;; HUB75 pinout + -D WLED_DEBUG_BUS + -D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75 + -D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic + +lib_deps = ${esp32s3.lib_deps} + https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix + +;;board_build.partitions = ${esp32.large_partitions} ;; for 8MB flash +board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash +board_build.f_flash = 80000000L +board_build.flash_mode = qio +monitor_filters = esp32_exception_decoder +custom_usermods = audioreactive \ No newline at end of file diff --git a/readme.md b/readme.md index 80256560af..bcdf5ab303 100644 --- a/readme.md +++ b/readme.md @@ -1,18 +1,20 @@

- - + + - +

-# Welcome to my project WLED! ✨ +# Welcome to WLED! ✨ -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! +A fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102! + +Originally created by [Aircoookie](https://github.com/Aircoookie) ## ⚙️ Features - WS2812FX library with more than 100 special effects @@ -21,7 +23,7 @@ A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control - 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 +- [Up to 10 LED outputs](https://kno.wled.ge/features/multi-strip/#esp32) per instance - Support for RGBW strips - Up to 250 user presets to save and load colors/effects easily, supports cycling through them. - Presets can be used to automatically execute API calls @@ -32,7 +34,7 @@ A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control - Filesystem-based config for easier backup of presets and settings ## 💡 Supported light control interfaces -- WLED app for [Android](https://play.google.com/store/apps/details?id=com.aircoookie.WLED) and [iOS](https://apps.apple.com/us/app/wled/id1475695033) +- WLED app for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239) - JSON and HTTP request APIs - MQTT - E1.31, Art-Net, DDP and TPM2.net @@ -63,6 +65,7 @@ See [here](https://kno.wled.ge/basics/compatible-hardware)! Licensed under the EUPL v1.2 license Credits [here](https://kno.wled.ge/about/contributors/)! +CORS proxy by [Corsfix](https://corsfix.com/) Join the Discord server to discuss everything about WLED! diff --git a/requirements.in b/requirements.in index 7c715125fc..a74bd418b9 100644 --- a/requirements.in +++ b/requirements.in @@ -1 +1 @@ -platformio +platformio>=6.1.17 diff --git a/requirements.txt b/requirements.txt index 666122aa28..fd9806ac23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,28 +1,26 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile +# pip-compile requirements.in # ajsonrpc==1.2.0 # via platformio -anyio==4.6.0 +anyio==4.8.0 # via starlette -bottle==0.13.1 +bottle==0.13.2 # via platformio -certifi==2024.8.30 +certifi==2025.1.31 # via requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.1 # via requests -click==8.1.7 +click==8.1.8 # via # platformio # uvicorn colorama==0.4.6 - # via - # click - # platformio -h11==0.14.0 + # via platformio +h11==0.16.0 # via # uvicorn # wsproto @@ -30,29 +28,31 @@ idna==3.10 # via # anyio # requests -marshmallow==3.22.0 +marshmallow==3.26.1 # via platformio -packaging==24.1 +packaging==24.2 # via marshmallow -platformio==6.1.16 +platformio==6.1.17 # via -r requirements.in -pyelftools==0.31 +pyelftools==0.32 # via platformio pyserial==3.5 # via platformio -requests==2.32.3 +requests==2.32.4 # via platformio semantic-version==2.10.0 # via platformio sniffio==1.3.1 # via anyio -starlette==0.39.1 +starlette==0.45.3 # via platformio tabulate==0.9.0 # via platformio -urllib3==2.2.3 +typing-extensions==4.12.2 + # via anyio +urllib3==2.5.0 # via requests -uvicorn==0.30.6 +uvicorn==0.34.0 # via platformio wsproto==1.2.0 # via platformio diff --git a/tools/AutoCubeMap.xlsx b/tools/AutoCubeMap.xlsx new file mode 100644 index 0000000000..b3f5cee2ad Binary files /dev/null and b/tools/AutoCubeMap.xlsx differ diff --git a/tools/WLED_ESP32_32MB.csv b/tools/WLED_ESP32_32MB.csv new file mode 100644 index 0000000000..2aa06e6f29 --- /dev/null +++ b/tools/WLED_ESP32_32MB.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, 0x300000, +app1, app, ota_1, 0x310000,0x300000, +spiffs, data, spiffs, 0x610000,0x19E0000, +coredump, data, coredump,,64K diff --git a/tools/cdata.js b/tools/cdata.js index c5d3c6aa52..5ae7088b3e 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -17,7 +17,7 @@ const fs = require("node:fs"); const path = require("path"); -const inliner = require("inliner"); +const inline = require("web-resource-inliner"); const zlib = require("node:zlib"); const CleanCSS = require("clean-css"); const minifyHtml = require("html-minifier-terser").minify; @@ -26,7 +26,7 @@ const packageJson = require("../package.json"); // Export functions for testing module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan }; -const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"] +const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_pixelforge.h", "wled00/html_settings.h", "wled00/html_other.h", "wled00/js_iro.h", "wled00/js_omggif.h"] // \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset const wledBanner = ` @@ -38,6 +38,11 @@ const wledBanner = ` \t\t\x1b[36m build script for web UI \x1b[0m`; +// Generate build timestamp as UNIX timestamp (seconds since epoch) +function generateBuildTime() { + return Math.floor(Date.now() / 1000); +} + const singleHeader = `/* * Binary array for the Web UI. * gzip is used for smaller size and improved speeds. @@ -45,6 +50,9 @@ const singleHeader = `/* * Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui * to find out how to easily modify the web UI source! */ + +// Automatically generated build time for cache busting (UNIX timestamp) +#define WEB_BUILD_TIME ${generateBuildTime()} `; @@ -89,7 +97,7 @@ function adoptVersionAndRepo(html) { repoUrl = repoUrl.replace(/^git\+/, ""); repoUrl = repoUrl.replace(/\.git$/, ""); html = html.replaceAll("https://github.com/atuline/WLED", repoUrl); - html = html.replaceAll("https://github.com/Aircoookie/WLED", repoUrl); + html = html.replaceAll("https://github.com/wled-dev/WLED", repoUrl); } let version = packageJson.version; if (version) { @@ -126,23 +134,29 @@ async function minify(str, type = "plain") { throw new Error("Unknown filter: " + type); } -async function writeHtmlGzipped(sourceFile, resultFile, page) { +async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) { console.info("Reading " + sourceFile); - new inliner(sourceFile, async function (error, html) { - if (error) throw error; - - html = adoptVersionAndRepo(html); - const originalLength = html.length; - html = await minify(html, "html-minify"); - const result = zlib.gzipSync(html, { level: zlib.constants.Z_BEST_COMPRESSION }); - console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes"); - const array = hexdump(result); - let src = singleHeader; - src += `const uint16_t PAGE_${page}_L = ${result.length};\n`; - src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`; - console.info("Writing " + resultFile); - fs.writeFileSync(resultFile, src); - }); + inline.html({ + fileContent: fs.readFileSync(sourceFile, "utf8"), + relativeTo: path.dirname(sourceFile), + strict: inlineCss, // when not inlining css, ignore errors (enables linking style.css from subfolder htm files) + stylesheets: inlineCss // when true (default), css is inlined + }, + async function (error, html) { + if (error) throw error; + + html = adoptVersionAndRepo(html); + const originalLength = html.length; + html = await minify(html, "html-minify"); + const result = zlib.gzipSync(html, { level: zlib.constants.Z_BEST_COMPRESSION }); + console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes"); + const array = hexdump(result); + let src = singleHeader; + src += `const uint16_t PAGE_${page}_length = ${result.length};\n`; + src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`; + console.info("Writing " + resultFile); + fs.writeFileSync(resultFile, src); + }); } async function specToChunk(srcDir, s) { @@ -239,8 +253,63 @@ if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.ar writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index'); writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart'); -writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal'); writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic'); +writeHtmlGzipped("wled00/data/pixelforge/pixelforge.htm", "wled00/html_pixelforge.h", 'pixelforge', false); // do not inline css +//writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit'); + +writeChunks( + "wled00/data/", + [ + { + file: "iro.js", + name: "JS_iro", + method: "gzip", + filter: "plain", // no minification, it is already minified + mangle: (s) => s.replace(/^\/\*![\s\S]*?\*\//, '') // remove license comment at the top + } + ], + "wled00/js_iro.h" +); + +writeChunks( + "wled00/data/pixelforge", + [ + { + file: "omggif.js", + name: "JS_omggif", + method: "gzip", + filter: "js-minify", + mangle: (s) => s.replace(/^\/\*![\s\S]*?\*\//, '') // remove license comment at the top + } + ], + "wled00/js_omggif.h" +); + +writeChunks( + "wled00/data", + [ + { + file: "edit.htm", + name: "PAGE_edit", + method: "gzip", + filter: "html-minify" + } + ], + "wled00/html_edit.h" +); + +writeChunks( + "wled00/data/cpal", + [ + { + file: "cpal.htm", + name: "PAGE_cpal", + method: "gzip", + filter: "html-minify" + } + ], + "wled00/html_cpal.h" +); writeChunks( "wled00/data", @@ -325,6 +394,12 @@ writeChunks( name: "PAGE_settings_pin", method: "gzip", filter: "html-minify" + }, + { + file: "settings_pininfo.htm", + name: "PAGE_settings_pininfo", + method: "gzip", + filter: "html-minify" } ], "wled00/html_settings.h" @@ -370,12 +445,6 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; name: "PAGE_update", method: "gzip", filter: "html-minify", - mangle: (str) => - str - .replace( - /function GetV().*\<\/script\>/gms, - "" - ) }, { file: "welcome.htm", diff --git a/tools/partitions-16MB_spiffs-tinyuf2.csv b/tools/partitions-16MB_spiffs-tinyuf2.csv new file mode 100644 index 0000000000..238710e909 --- /dev/null +++ b/tools/partitions-16MB_spiffs-tinyuf2.csv @@ -0,0 +1,10 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +# bootloader.bin,, 0x1000, 32K +# partition table,, 0x8000, 4K +nvs, data, nvs, 0x9000, 20K, +otadata, data, ota, 0xe000, 8K, +ota_0, app, ota_0, 0x10000, 2048K, +ota_1, app, ota_1, 0x210000, 2048K, +uf2, app, factory,0x410000, 256K, +spiffs, data, spiffs, 0x450000, 11968K, diff --git a/tools/partitions-4MB_spiffs-tinyuf2.csv b/tools/partitions-4MB_spiffs-tinyuf2.csv new file mode 100644 index 0000000000..4979c12722 --- /dev/null +++ b/tools/partitions-4MB_spiffs-tinyuf2.csv @@ -0,0 +1,11 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +# bootloader.bin,, 0x1000, 32K +# partition table, 0x8000, 4K + +nvs, data, nvs, 0x9000, 20K, +otadata, data, ota, 0xe000, 8K, +ota_0, 0, ota_0, 0x10000, 1408K, +ota_1, 0, ota_1, 0x170000, 1408K, +uf2, app, factory,0x2d0000, 256K, +spiffs, data, spiffs, 0x310000, 960K, diff --git a/tools/partitions-8MB_spiffs-tinyuf2.csv b/tools/partitions-8MB_spiffs-tinyuf2.csv new file mode 100644 index 0000000000..27ed4c2d6a --- /dev/null +++ b/tools/partitions-8MB_spiffs-tinyuf2.csv @@ -0,0 +1,10 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +# bootloader.bin,, 0x1000, 32K +# partition table,, 0x8000, 4K +nvs, data, nvs, 0x9000, 20K, +otadata, data, ota, 0xe000, 8K, +ota_0, app, ota_0, 0x10000, 2048K, +ota_1, app, ota_1, 0x210000, 2048K, +uf2, app, factory,0x410000, 256K, +spiffs, data, spiffs, 0x450000, 3776K, diff --git a/tools/stress_test.sh b/tools/stress_test.sh index d7c344c58b..d86c508642 100644 --- a/tools/stress_test.sh +++ b/tools/stress_test.sh @@ -27,6 +27,7 @@ read -a JSON_TINY_TARGETS <<< $(replicate "json/nodes") read -a JSON_SMALL_TARGETS <<< $(replicate "json/info") read -a JSON_LARGE_TARGETS <<< $(replicate "json/si") read -a JSON_LARGER_TARGETS <<< $(replicate "json/fxdata") +read -a INDEX_TARGETS <<< $(replicate "") # Expand target URLS to full arguments for curl TARGETS=(${TARGET_STR[@]}) diff --git a/tools/wled-tools b/tools/wled-tools new file mode 100755 index 0000000000..34fb6370df --- /dev/null +++ b/tools/wled-tools @@ -0,0 +1,329 @@ +#!/bin/bash + +# WLED Tools +# A utility for managing WLED devices in a local network +# https://github.com/wled/WLED + +# Color Definitions +GREEN="\e[32m" +RED="\e[31m" +BLUE="\e[34m" +YELLOW="\e[33m" +RESET="\e[0m" + +# Logging function +log() { + local category="$1" + local color="$2" + local text="$3" + + if [ "$quiet" = true ]; then + return + fi + + if [ -t 1 ]; then # Check if output is a terminal + echo -e "${color}[${category}]${RESET} ${text}" + else + echo "[${category}] ${text}" + fi +} + +# Fetch a URL to a destination file, validating status codes. +# Usage: fetch "" "" "200 404" +fetch() { + local url="$1" + local dest="$2" + local accepted="${3:-200}" + + # If no dest given, just discard body + local out + if [ -n "$dest" ]; then + # Write to ".tmp" files first, then move when success, to ensure we don't write partial files + out="${dest}.tmp" + else + out="/dev/null" + fi + + response=$(curl --connect-timeout 5 --max-time 30 -s -w "%{http_code}" -o "$out" "$url") + local curl_exit_code=$? + + if [ $curl_exit_code -ne 0 ]; then + [ -n "$dest" ] && rm -f "$out" + log "ERROR" "$RED" "Connection error during request to $url (curl exit code: $curl_exit_code)." + return 1 + fi + + for code in $accepted; do + if [ "$response" = "$code" ]; then + # Accepted; only persist body for 2xx responses + if [ -n "$dest" ]; then + if [[ "$response" =~ ^2 ]]; then + mv "$out" "$dest" + else + rm -f "$out" + fi + fi + return 0 + fi + done + + # not accepted + [ -n "$dest" ] && rm -f "$out" + log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)." + return 2 +} + + +# POST a file to a URL, validating status codes. +# Usage: post_file "" "" "200" +post_file() { + local url="$1" + local file="$2" + local accepted="${3:-200}" + + response=$(curl --connect-timeout 5 --max-time 300 -s -w "%{http_code}" -o /dev/null -X POST -F "file=@$file" "$url") + local curl_exit_code=$? + + if [ $curl_exit_code -ne 0 ]; then + log "ERROR" "$RED" "Connection error during POST to $url (curl exit code: $curl_exit_code)." + return 1 + fi + + for code in $accepted; do + if [ "$response" -eq "$code" ]; then + return 0 + fi + done + + log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)." + return 2 +} + + +# Print help message +show_help() { + cat << EOF +Usage: wled-tools.sh [OPTIONS] COMMAND [ARGS...] + +Options: + -h, --help Show this help message and exit. + -t, --target Specify a single WLED device by IP address or hostname. + -D, --discover Discover multiple WLED devices using mDNS. + -d, --directory Specify a directory for saving backups (default: working directory). + -f, --firmware Specify the firmware file for updating devices. + -q, --quiet Suppress logging output (also makes discover output hostnames only). + +Commands: + backup Backup the current state of a WLED device or multiple discovered devices. + update Update the firmware of a WLED device or multiple discovered devices. + discover Discover WLED devices using mDNS and list their IP addresses and names. + +Examples: + # Discover all WLED devices on the network + ./wled-tools discover + + # Backup a specific WLED device + ./wled-tools -t 192.168.1.100 backup + + # Backup all discovered WLED devices to a specific directory + ./wled-tools -D -d /path/to/backups backup + + # Update firmware on all discovered WLED devices + ./wled-tools -D -f /path/to/firmware.bin update + +EOF +} + +# Discover devices using mDNS +discover_devices() { + if ! command -v avahi-browse &> /dev/null; then + log "ERROR" "$RED" "'avahi-browse' is required but not installed, please install avahi-utils using your preferred package manager." + exit 1 + fi + + # Map avahi responses to strings seperated by 0x1F (unit separator) + mapfile -t raw_devices < <(avahi-browse _wled._tcp --terminate -r -p | awk -F';' '/^=/ {print $7"\x1F"$8"\x1F"$9}') + + local devices_array=() + for device in "${raw_devices[@]}"; do + IFS=$'\x1F' read -r hostname address port <<< "$device" + devices_array+=("$hostname" "$address" "$port") + done + + echo "${devices_array[@]}" +} + +# Backup one device +backup_one() { + local hostname="$1" + local address="$2" + local port="$3" + + log "INFO" "$YELLOW" "Backing up device config/presets/ir: $hostname ($address:$port)" + + mkdir -p "$backup_dir" + + local file_prefix="${backup_dir}/${hostname}" + + if ! fetch "http://$address:$port/cfg.json" "${file_prefix}.cfg.json"; then + log "ERROR" "$RED" "Failed to backup configuration for $hostname" + return 1 + fi + + if ! fetch "http://$address:$port/presets.json" "${file_prefix}.presets.json"; then + log "ERROR" "$RED" "Failed to backup presets for $hostname" + return 1 + fi + + # ir.json is optional + if ! fetch "http://$address:$port/ir.json" "${file_prefix}.ir.json" "200 404"; then + log "ERROR" "$RED" "Failed to backup ir configs for $hostname" + fi + + log "INFO" "$GREEN" "Successfully backed up config and presets for $hostname" + return 0 +} + +# Update one device +update_one() { + local hostname="$1" + local address="$2" + local port="$3" + local firmware="$4" + + log "INFO" "$YELLOW" "Starting firmware update for device: $hostname ($address:$port)" + + local url="http://$address:$port/update" + + if ! post_file "$url" "$firmware" "200"; then + log "ERROR" "$RED" "Failed to update firmware for $hostname" + return 1 + fi + + log "INFO" "$GREEN" "Successfully initiated firmware update for $hostname" + return 0 +} + +# Command-line arguments processing +command="" +target="" +discover=false +quiet=false +backup_dir="./" +firmware_file="" + +if [ $# -eq 0 ]; then + show_help + exit 0 +fi + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + -t|--target) + if [ -z "$2" ] || [[ "$2" == -* ]]; then + log "ERROR" "$RED" "The --target option requires an argument." + exit 1 + fi + target="$2" + shift 2 + ;; + -D|--discover) + discover=true + shift + ;; + -d|--directory) + if [ -z "$2" ] || [[ "$2" == -* ]]; then + log "ERROR" "$RED" "The --directory option requires an argument." + exit 1 + fi + backup_dir="$2" + shift 2 + ;; + -f|--firmware) + if [ -z "$2" ] || [[ "$2" == -* ]]; then + log "ERROR" "$RED" "The --firmware option requires an argument." + exit 1 + fi + firmware_file="$2" + shift 2 + ;; + -q|--quiet) + quiet=true + shift + ;; + backup|update|discover) + command="$1" + shift + ;; + *) + log "ERROR" "$RED" "Unknown argument: $1" + exit 1 + ;; + esac +done + +# Execute the appropriate command +case "$command" in + discover) + read -ra devices <<< "$(discover_devices)" + for ((i=0; i<${#devices[@]}; i+=3)); do + hostname="${devices[$i]}" + address="${devices[$i+1]}" + port="${devices[$i+2]}" + + if [ "$quiet" = true ]; then + echo "$hostname" + else + log "INFO" "$BLUE" "Discovered device: Hostname=$hostname, Address=$address, Port=$port" + fi + done + ;; + backup) + if [ -n "$target" ]; then + # Assume target is both the hostname and address, with port 80 + backup_one "$target" "$target" "80" + elif [ "$discover" = true ]; then + read -ra devices <<< "$(discover_devices)" + for ((i=0; i<${#devices[@]}; i+=3)); do + hostname="${devices[$i]}" + address="${devices[$i+1]}" + port="${devices[$i+2]}" + backup_one "$hostname" "$address" "$port" + done + else + log "ERROR" "$RED" "No target specified. Use --target or --discover." + exit 1 + fi + ;; + update) + # Validate firmware before proceeding + if [ -z "$firmware_file" ] || [ ! -f "$firmware_file" ]; then + log "ERROR" "$RED" "Please provide a file in --firmware that exists" + exit 1 + fi + + if [ -n "$target" ]; then + # Assume target is both the hostname and address, with port 80 + update_one "$target" "$target" "80" "$firmware_file" + elif [ "$discover" = true ]; then + read -ra devices <<< "$(discover_devices)" + for ((i=0; i<${#devices[@]}; i+=3)); do + hostname="${devices[$i]}" + address="${devices[$i+1]}" + port="${devices[$i+2]}" + update_one "$hostname" "$address" "$port" "$firmware_file" + done + else + log "ERROR" "$RED" "No target specified. Use --target or --discover." + exit 1 + fi + ;; + *) + show_help + exit 1 + ;; +esac diff --git a/usermods/ADS1115_v2/usermod_ads1115.h b/usermods/ADS1115_v2/ADS1115_v2.cpp similarity index 98% rename from usermods/ADS1115_v2/usermod_ads1115.h rename to usermods/ADS1115_v2/ADS1115_v2.cpp index 5e2b4b2703..bbf457f486 100644 --- a/usermods/ADS1115_v2/usermod_ads1115.h +++ b/usermods/ADS1115_v2/ADS1115_v2.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include #include @@ -252,4 +250,7 @@ class ADS1115Usermod : public Usermod { int16_t results = ads.getLastConversionResults(); readings[activeChannel] = ads.computeVolts(results); } -}; \ No newline at end of file +}; + +static ADS1115Usermod ads1115_v2; +REGISTER_USERMOD(ads1115_v2); \ No newline at end of file diff --git a/usermods/ADS1115_v2/library.json b/usermods/ADS1115_v2/library.json new file mode 100644 index 0000000000..5e5d7e450a --- /dev/null +++ b/usermods/ADS1115_v2/library.json @@ -0,0 +1,8 @@ +{ + "name": "ADS1115_v2", + "build": { "libArchive": false }, + "dependencies": { + "Adafruit BusIO": "https://github.com/adafruit/Adafruit_BusIO#1.13.2", + "Adafruit ADS1X15": "https://github.com/adafruit/Adafruit_ADS1X15#2.4.0" + } +} diff --git a/usermods/ADS1115_v2/readme.md b/usermods/ADS1115_v2/readme.md index 44092bc8e7..7397fc2e9a 100644 --- a/usermods/ADS1115_v2/readme.md +++ b/usermods/ADS1115_v2/readme.md @@ -6,5 +6,5 @@ Configuration is performed via the Usermod menu. There are no parameters to set ## Installation -Add the build flag `-D USERMOD_ADS1115` to your platformio environment. -Uncomment libraries with comment `#For ADS1115 sensor uncomment following` +Add 'ADS1115' to `custom_usermods` in your platformio environment. + diff --git a/usermods/AHT10_v2/usermod_aht10.h b/usermods/AHT10_v2/AHT10_v2.cpp similarity index 98% rename from usermods/AHT10_v2/usermod_aht10.h rename to usermods/AHT10_v2/AHT10_v2.cpp index b5dc1841db..f88bee1daa 100644 --- a/usermods/AHT10_v2/usermod_aht10.h +++ b/usermods/AHT10_v2/AHT10_v2.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include @@ -54,12 +52,6 @@ class UsermodAHT10 : public Usermod _lastTemperature = 0; } - ~UsermodAHT10() - { - delete _aht; - _aht = nullptr; - } - #ifndef WLED_DISABLE_MQTT void mqttInitialize() { @@ -322,6 +314,15 @@ class UsermodAHT10 : public Usermod _initDone = true; return configComplete; } + + ~UsermodAHT10() + { + delete _aht; + _aht = nullptr; + } }; -const char UsermodAHT10::_name[] PROGMEM = "AHTxx"; \ No newline at end of file +const char UsermodAHT10::_name[] PROGMEM = "AHTxx"; + +static UsermodAHT10 aht10_v2; +REGISTER_USERMOD(aht10_v2); \ No newline at end of file diff --git a/usermods/AHT10_v2/README.md b/usermods/AHT10_v2/README.md index 69fab46717..d84c1c6ad9 100644 --- a/usermods/AHT10_v2/README.md +++ b/usermods/AHT10_v2/README.md @@ -22,15 +22,9 @@ Dependencies, These must be added under `lib_deps` in your `platform.ini` (or `p # Compiling -To enable, compile with `USERMOD_AHT10` defined (e.g. in `platformio_override.ini`) +To enable, add 'AHT10' to `custom_usermods` in your platformio encrionment (e.g. in `platformio_override.ini`) ```ini [env:aht10_example] extends = env:esp32dev -build_flags = - ${common.build_flags} ${esp32.build_flags} - -D USERMOD_AHT10 - ; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal -lib_deps = - ${esp32.lib_deps} - enjoyneering/AHT10@~1.1.0 +custom_usermods = ${env:esp32dev.custom_usermods} AHT10 ``` diff --git a/usermods/AHT10_v2/library.json b/usermods/AHT10_v2/library.json new file mode 100644 index 0000000000..fa6c2a6fee --- /dev/null +++ b/usermods/AHT10_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "AHT10_v2", + "build": { "libArchive": false }, + "dependencies": { + "enjoyneering/AHT10":"~1.1.0" + } +} diff --git a/usermods/AHT10_v2/platformio_override.ini b/usermods/AHT10_v2/platformio_override.ini index 30240f2224..74dcd659bb 100644 --- a/usermods/AHT10_v2/platformio_override.ini +++ b/usermods/AHT10_v2/platformio_override.ini @@ -2,8 +2,4 @@ extends = env:esp32dev build_flags = ${common.build_flags} ${esp32.build_flags} - -D USERMOD_AHT10 ; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal -lib_deps = - ${esp32.lib_deps} - enjoyneering/AHT10@~1.1.0 \ No newline at end of file diff --git a/usermods/Analog_Clock/Analog_Clock.h b/usermods/Analog_Clock/Analog_Clock.cpp similarity index 96% rename from usermods/Analog_Clock/Analog_Clock.h rename to usermods/Analog_Clock/Analog_Clock.cpp index 9d82f7670c..3e0a1e99a6 100644 --- a/usermods/Analog_Clock/Analog_Clock.h +++ b/usermods/Analog_Clock/Analog_Clock.cpp @@ -1,10 +1,8 @@ -#pragma once #include "wled.h" /* * Usermod for analog clock */ -extern Timezone* tz; class AnalogClockUsermod : public Usermod { private: @@ -103,9 +101,9 @@ class AnalogClockUsermod : public Usermod { void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) { uint32_t ms = time.ms % 1000; uint8_t b0 = (cos8_t(ms * 64 / 1000) - 128) * 2; - setPixelColor(secondLed, gamma32(scale32(secondColor, b0))); + setPixelColor(secondLed, scale32(secondColor, b0)); uint8_t b1 = (sin8_t(ms * 64 / 1000) - 128) * 2; - setPixelColor(inc(secondLed, 1, secondsSegment), gamma32(scale32(secondColor, b1))); + setPixelColor(inc(secondLed, 1, secondsSegment), scale32(secondColor, b1)); } static inline uint32_t qadd32(uint32_t c1, uint32_t c2) { @@ -192,7 +190,7 @@ class AnalogClockUsermod : public Usermod { // 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(trailLed, scale32(secondColor, trailBright)); // } } @@ -254,3 +252,7 @@ class AnalogClockUsermod : public Usermod { return USERMOD_ID_ANALOG_CLOCK; } }; + + +static AnalogClockUsermod analog_clock; +REGISTER_USERMOD(analog_clock); \ No newline at end of file diff --git a/usermods/Analog_Clock/library.json b/usermods/Analog_Clock/library.json new file mode 100644 index 0000000000..f76cf42681 --- /dev/null +++ b/usermods/Analog_Clock/library.json @@ -0,0 +1,4 @@ +{ + "name": "Analog_Clock", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.cpp similarity index 99% rename from usermods/Animated_Staircase/Animated_Staircase.h rename to usermods/Animated_Staircase/Animated_Staircase.cpp index 54a9b3331e..2d2d27cf43 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.cpp @@ -7,7 +7,6 @@ * * See the accompanying README.md file for more info. */ -#pragma once #include "wled.h" class Animated_Staircase : public Usermod { @@ -562,3 +561,7 @@ const char Animated_Staircase::_bottomEcho_pin[] PROGMEM = "bottomEch const char Animated_Staircase::_topEchoCm[] PROGMEM = "top-dist-cm"; const char Animated_Staircase::_bottomEchoCm[] PROGMEM = "bottom-dist-cm"; const char Animated_Staircase::_togglePower[] PROGMEM = "toggle-on-off"; + + +static Animated_Staircase animated_staircase; +REGISTER_USERMOD(animated_staircase); \ No newline at end of file diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md index 2ad66b5aef..263ac8065f 100644 --- a/usermods/Animated_Staircase/README.md +++ b/usermods/Animated_Staircase/README.md @@ -1,4 +1,5 @@ # 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: @@ -11,14 +12,15 @@ The Animated Staircase can be controlled by the WLED API. Change settings such a 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://kno.wled.ge/advanced/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 `UsermodManager::add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. +Edit your environment in `platformio_override.ini` + +1. Open `platformio_override.ini` +2. add `Animated_Staircase` to the `custom_usermods` line for your environment You can configure usermod using the Usermods settings page. Please enter GPIO pins for PIR or ultrasonic sensors (trigger and echo). @@ -26,10 +28,10 @@ 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. +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 @@ -38,24 +40,23 @@ Maximum distance for ultrasonic sensor can be configured as the time needed for 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, configure a segment for each step. The lowest step of the stairs is the - lowest segment id. -2. Save your segments into a preset. -3. Ideally, add the preset in the config > LED setup menu to the "apply - preset **n** at boot" setting. + +1. In the WLED UI, configure a segment for each step. The lowest step of the stairs is the lowest segment id. +2. Save your segments into a preset. +3. Ideally, add the preset in the config > LED setup menu to the "apply 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 | +| bottom-sensor | Manually trigger a down to up animation via API | false | | top-sensor | Manually trigger an up to down animation via API | false | @@ -75,6 +76,7 @@ The staircase settings and sensor states are inside the WLED "state" element: ``` ### 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. @@ -91,6 +93,7 @@ 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, including sensor pins, delay between segment activation etc. When an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors. @@ -100,6 +103,7 @@ distances creates delays in the WLED software, _might_ introduce timing hiccups 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: @@ -116,15 +120,19 @@ 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 +Have fun with this usermod + +`www.rolfje.com` Modifications @blazoncek ## Change log + 2021-04 -* Adaptation for runtime configuration. + +- Adaptation for runtime configuration. diff --git a/usermods/Animated_Staircase/library.json b/usermods/Animated_Staircase/library.json new file mode 100644 index 0000000000..015b15cef5 --- /dev/null +++ b/usermods/Animated_Staircase/library.json @@ -0,0 +1,4 @@ +{ + "name": "Animated_Staircase", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/BH1750_v2/BH1750_v2.cpp b/usermods/BH1750_v2/BH1750_v2.cpp new file mode 100644 index 0000000000..3800e915d6 --- /dev/null +++ b/usermods/BH1750_v2/BH1750_v2.cpp @@ -0,0 +1,186 @@ +// force the compiler to show a warning to confirm that this file is included +#warning **** Included USERMOD_BH1750 **** + +#include "wled.h" +#include "BH1750_v2.h" + +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +static bool checkBoundSensor(float newValue, float prevValue, float maxDiff) +{ + return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0); +} + +void Usermod_BH1750::_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 Usermod_BH1750::_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_BRAND); + device[F("model")] = F(WLED_PRODUCT_NAME); + 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 Usermod_BH1750::setup() +{ + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } + sensorFound = lightMeter.begin(); + initDone = true; +} + +void Usermod_BH1750::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(); + + 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")); + } + } +} + + +void Usermod_BH1750::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 Usermod_BH1750::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 Usermod_BH1750::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; + +} + + +// 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"; + + +static Usermod_BH1750 bh1750_v2; +REGISTER_USERMOD(bh1750_v2); \ No newline at end of file diff --git a/usermods/BH1750_v2/BH1750_v2.h b/usermods/BH1750_v2/BH1750_v2.h new file mode 100644 index 0000000000..22f51ce9ba --- /dev/null +++ b/usermods/BH1750_v2/BH1750_v2.h @@ -0,0 +1,92 @@ + +#pragma once +#include "wled.h" +#include + +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +// 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 difference 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; + bool mqttInitialized = false; + bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages + + BH1750 lightMeter; + float lastLux = -1000; + + // set up Home Assistant discovery entries + void _mqttInitialize(); + + // 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); + +public: + void setup(); + void loop(); + inline float getIlluminance() { + return (float)lastLux; + } + + void addToJsonInfo(JsonObject &root); + + // (called from set.cpp) stores persistent properties to cfg.json + void addToConfig(JsonObject &root); + + // called before setup() to populate properties from values stored in cfg.json + bool readFromConfig(JsonObject &root); + + inline uint16_t getId() + { + return USERMOD_ID_BH1750; + } + +}; diff --git a/usermods/BH1750_v2/library.json b/usermods/BH1750_v2/library.json new file mode 100644 index 0000000000..4e32099b0c --- /dev/null +++ b/usermods/BH1750_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "BH1750_v2", + "build": { "libArchive": false }, + "dependencies": { + "claws/BH1750":"^1.2.0" + } +} diff --git a/usermods/BH1750_v2/readme.md b/usermods/BH1750_v2/readme.md index 6e6c693d45..9f5991076e 100644 --- a/usermods/BH1750_v2/readme.md +++ b/usermods/BH1750_v2/readme.md @@ -4,43 +4,40 @@ This usermod will read from an ambient light sensor like the BH1750. The luminance is displayed in both the Info section of the web UI, as well as published to the `/luminance` MQTT topic if enabled. ## Dependencies + - Libraries - `claws/BH1750 @^1.2.0` - - This must be added under `lib_deps` in your `platformio.ini` (or `platformio_override.ini`). - Data is published over MQTT - make sure you've enabled the MQTT sync interface. ## Compilation -To enable, compile with `USERMOD_BH1750` defined (e.g. in `platformio_override.ini`) -```ini -[env:usermod_BH1750_d1_mini] -extends = env:d1_mini -build_flags = - ${common.build_flags_esp8266} - -D USERMOD_BH1750 -lib_deps = - ${esp8266.lib_deps} - claws/BH1750 @ ^1.2.0 -``` +To enable, compile with `BH1750` in `custom_usermods` (e.g. in `platformio_override.ini`) ### 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 + +- `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 diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h deleted file mode 100644 index 2a2bd46370..0000000000 --- a/usermods/BH1750_v2/usermod_bh1750.h +++ /dev/null @@ -1,252 +0,0 @@ -// 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 difference 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; - 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) + " " + 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_BRAND); - device[F("model")] = F(WLED_PRODUCT_NAME); - 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/usermod_bme280.h b/usermods/BME280_v2/BME280_v2.cpp similarity index 96% rename from usermods/BME280_v2/usermod_bme280.h rename to usermods/BME280_v2/BME280_v2.cpp index 9168f42291..dd58590448 100644 --- a/usermods/BME280_v2/usermod_bme280.h +++ b/usermods/BME280_v2/BME280_v2.cpp @@ -1,17 +1,15 @@ // 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 +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + class UsermodBME280 : public Usermod { private: @@ -241,7 +239,7 @@ class UsermodBME280 : public Usermod // from the UI and values read from sensor, then publish to broker if (temperature != lastTemperature || PublishAlways) { - publishMqtt("temperature", String(temperature, TemperatureDecimals).c_str()); + publishMqtt("temperature", String(temperature, (unsigned) TemperatureDecimals).c_str()); } lastTemperature = temperature; // Update last sensor temperature for next loop @@ -254,17 +252,17 @@ class UsermodBME280 : public Usermod if (humidity != lastHumidity || PublishAlways) { - publishMqtt("humidity", String(humidity, HumidityDecimals).c_str()); + publishMqtt("humidity", String(humidity, (unsigned) HumidityDecimals).c_str()); } if (heatIndex != lastHeatIndex || PublishAlways) { - publishMqtt("heat_index", String(heatIndex, TemperatureDecimals).c_str()); + publishMqtt("heat_index", String(heatIndex, (unsigned) TemperatureDecimals).c_str()); } if (dewPoint != lastDewPoint || PublishAlways) { - publishMqtt("dew_point", String(dewPoint, TemperatureDecimals).c_str()); + publishMqtt("dew_point", String(dewPoint, (unsigned) TemperatureDecimals).c_str()); } lastHumidity = humidity; @@ -281,7 +279,7 @@ class UsermodBME280 : public Usermod if (pressure != lastPressure || PublishAlways) { - publishMqtt("pressure", String(pressure, PressureDecimals).c_str()); + publishMqtt("pressure", String(pressure, (unsigned) PressureDecimals).c_str()); } lastPressure = pressure; @@ -479,3 +477,7 @@ class UsermodBME280 : public Usermod const char UsermodBME280::_name[] PROGMEM = "BME280/BMP280"; const char UsermodBME280::_enabled[] PROGMEM = "enabled"; + + +static UsermodBME280 bme280_v2; +REGISTER_USERMOD(bme280_v2); \ No newline at end of file diff --git a/usermods/BME280_v2/README.md b/usermods/BME280_v2/README.md index a4fc229a38..0daea5825c 100644 --- a/usermods/BME280_v2/README.md +++ b/usermods/BME280_v2/README.md @@ -22,7 +22,6 @@ 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! @@ -40,17 +39,11 @@ Methods also exist to read the read/calculated values from other WLED modules th # Compiling -To enable, compile with `USERMOD_BME280` defined (e.g. in `platformio_override.ini`) +To enable, add `BME280_v2` to your `custom_usermods` (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 +custom_usermods = ${env:d1_mini.custom_usermods} BME280_v2 ``` diff --git a/usermods/BME280_v2/library.json b/usermods/BME280_v2/library.json new file mode 100644 index 0000000000..cfdfe1ba1a --- /dev/null +++ b/usermods/BME280_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "BME280_v2", + "build": { "libArchive": false }, + "dependencies": { + "finitespace/BME280":"~3.0.0" + } +} \ No newline at end of file diff --git a/usermods/BME68X_v2/BME68X_v2.cpp b/usermods/BME68X_v2/BME68X_v2.cpp new file mode 100644 index 0000000000..63bada5af0 --- /dev/null +++ b/usermods/BME68X_v2/BME68X_v2.cpp @@ -0,0 +1,1114 @@ +/** + * @file usermod_BMW68X.cpp + * @author Gabriel A. Sieben (GeoGab) + * @brief Usermod for WLED to implement the BME680/BME688 sensor + * @version 1.0.2 + * @date 28 March 2025 + */ + + #define UMOD_DEVICE "ESP32" // NOTE - Set your hardware here + #define HARDWARE_VERSION "1.0" // NOTE - Set your hardware version here + #define UMOD_BME680X_SW_VERSION "1.0.2" // NOTE - Version of the User Mod + #define CALIB_FILE_NAME "/BME680X-Calib.hex" // NOTE - Calibration file name + #define UMOD_NAME "BME680X" // NOTE - User module name + #define UMOD_DEBUG_NAME "UM-BME680X: " // NOTE - Debug print module name addon + + #define ESC "\033" + #define ESC_CSI ESC "[" + #define ESC_STYLE_RESET ESC_CSI "0m" + #define ESC_CURSOR_COLUMN(n) ESC_CSI #n "G" + + #define ESC_FGCOLOR_BLACK ESC_CSI "30m" + #define ESC_FGCOLOR_RED ESC_CSI "31m" + #define ESC_FGCOLOR_GREEN ESC_CSI "32m" + #define ESC_FGCOLOR_YELLOW ESC_CSI "33m" + #define ESC_FGCOLOR_BLUE ESC_CSI "34m" + #define ESC_FGCOLOR_MAGENTA ESC_CSI "35m" + #define ESC_FGCOLOR_CYAN ESC_CSI "36m" + #define ESC_FGCOLOR_WHITE ESC_CSI "37m" + #define ESC_FGCOLOR_DEFAULT ESC_CSI "39m" + + /* Debug Print Special Text */ + #define INFO_COLUMN ESC_CURSOR_COLUMN(60) + #define GOGAB_OK INFO_COLUMN "[" ESC_FGCOLOR_GREEN "OK" ESC_STYLE_RESET "]" + #define GOGAB_FAIL INFO_COLUMN "[" ESC_FGCOLOR_RED "FAIL" ESC_STYLE_RESET "]" + #define GOGAB_WARN INFO_COLUMN "[" ESC_FGCOLOR_YELLOW "WARN" ESC_STYLE_RESET "]" + #define GOGAB_DONE INFO_COLUMN "[" ESC_FGCOLOR_CYAN "DONE" ESC_STYLE_RESET "]" + + #include "bsec.h" // Bosch sensor library + #include "wled.h" + #include + + /* UsermodBME68X class definition */ + class UsermodBME68X : public Usermod { + + public: + /* Public: Functions */ + uint16_t getId(); + void loop(); // Loop of the user module called by wled main in loop + void setup(); // Setup of the user module called by wled main + void addToConfig(JsonObject& root); // Extends the settings/user module settings page to include the user module requirements. The settings are written from the wled core to the configuration file. + void appendConfigData(); // Adds extra info to the config page of weld + bool readFromConfig(JsonObject& root); // Reads config values + void addToJsonInfo(JsonObject& root); // Adds user module info to the weld info page + + /* Wled internal functions which can be used by the core or other user mods */ + inline float getTemperature(); // Get Temperature in the selected scale of °C or °F + inline float getHumidity(); // ... + inline float getPressure(); + inline float getGasResistance(); + inline float getAbsoluteHumidity(); + inline float getDewPoint(); + inline float getIaq(); + inline float getStaticIaq(); + inline float getCo2(); + inline float getVoc(); + inline float getGasPerc(); + inline uint8_t getIaqAccuracy(); + inline uint8_t getStaticIaqAccuracy(); + inline uint8_t getCo2Accuracy(); + inline uint8_t getVocAccuracy(); + inline uint8_t getGasPercAccuracy(); + inline bool getStabStatus(); + inline bool getRunInStatus(); + + private: + /* Private: Functions */ + void HomeAssistantDiscovery(); + void MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option = 0); + void MQTT_publish(const char* topic, const float& value, const int8_t& dig); + void onMqttConnect(bool sessionPresent); + void checkIaqSensorStatus(); + void InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit); + void InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status); + void loadState(); + void saveState(); + void getValues(); + + /*** V A R I A B L E s & C O N S T A N T s ***/ + /* Private: Settings of Usermod BME68X */ + struct settings_t { + bool enabled; // true if user module is active + byte I2cadress; // Depending on the manufacturer, the BME680 has the address 0x76 or 0x77 + uint8_t Interval; // Interval of reading sensor data in seconds + uint16_t MaxAge; // Force the publication of the value of a sensor after these defined seconds at the latest + bool pubAcc; // Publish the accuracy values + bool publishSensorState; // Publisch the sensor calibration state + bool publishAfterCalibration ; // The IAQ/CO2/VOC/GAS value are only valid after the sensor has been calibrated. If this switch is active, the values are only sent after calibration + bool PublischChange; // Publish values even when they have not changed + bool PublishIAQVerbal; // Publish Index of Air Quality (IAQ) classification Verbal + bool PublishStaticIAQVerbal; // Publish Static Index of Air Quality (Static IAQ) Verbal + byte tempScale; // 0 -> Use Celsius, 1-> Use Fahrenheit + float tempOffset; // Temperature Offset + bool HomeAssistantDiscovery; // Publish Home Assistant Device Information + bool pauseOnActiveWled ; // If this is set to true, the user mod ist not executed while wled is active + + /* Decimal Places (-1 means inactive) */ + struct decimals_t { + int8_t temperature; + int8_t humidity; + int8_t pressure; + int8_t gasResistance; + int8_t absHumidity; + int8_t drewPoint; + int8_t iaq; + int8_t staticIaq; + int8_t co2; + int8_t Voc; + int8_t gasPerc; + } decimals; + } settings; + + /* Private: Flags */ + struct flags_t { + bool InitSuccessful = false; // Initialation was un-/successful + bool MqttInitialized = false; // MQTT Initialation done flag (first MQTT Connect) + bool SaveState = false; // Save the calibration data flag + bool DeleteCaibration = false; // If set the calib file will be deleted on the next round + } flags; + + /* Private: Measurement timers */ + struct timer_t { + long actual; // Actual time stamp + long lastRun; // Last measurement time stamp + } timer; + + /* Private: Various variables */ + String stringbuff; // General string stringbuff buffer + char charbuffer[128]; // General char stringbuff buffer + String InfoPageStatusLine; // Shown on the info page of WLED + String tempScale; // °C or °F + uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE]; // Calibration data array + uint16_t stateUpdateCounter; // Save state couter + static const uint8_t bsec_config_iaq[]; // Calibration Buffer + Bsec iaqSensor; // Sensor variable + + /* Private: Sensor values */ + struct values_t { + float temperature; // Temp [°C] (Sensor-compensated) + float humidity; // Relative humidity [%] (Sensor-compensated) + float pressure; // raw pressure [hPa] + float gasResistance; // raw gas restistance [Ohm] + float absHumidity; // UserMod calculated: Absolute Humidity [g/m³] + float drewPoint; // UserMod calculated: drew point [°C/°F] + float iaq; // IAQ (Indoor Air Quallity) + float staticIaq; // Satic IAQ + float co2; // CO2 [PPM] + float Voc; // VOC in [PPM] + float gasPerc; // Gas Percentage in [%] + uint8_t iaqAccuracy; // IAQ accuracy - IAQ Accuracy = 1 means value is inaccurate, IAQ Accuracy = 2 means sensor is being calibrated, IAQ Accuracy = 3 means sensor successfully calibrated. + uint8_t staticIaqAccuracy; // Static IAQ accuracy + uint8_t co2Accuracy; // co2 accuracy + uint8_t VocAccuracy; // voc accuracy + uint8_t gasPercAccuracy; // Gas percentage accuracy + bool stabStatus; // Indicates if the sensor is undergoing initial stabilization during its first use after production + bool runInStatus; // Indicates when the sensor is ready after after switch-on + } valuesA, valuesB, *ValuesPtr, *PrevValuesPtr, *swap; // Data Scructur A, Data Structur B, Pointers to switch between data channel A & B + + struct cvalues_t { + String iaqVerbal; // IAQ verbal + String staticIaqVerbal; // Static IAQ verbal + + } cvalues; + + /* Private: Sensor settings */ + bsec_virtual_sensor_t sensorList[13] = { + BSEC_OUTPUT_IAQ, // Index for Air Quality estimate [0-500] Index for Air Quality (IAQ) gives an indication of the relative change in ambient TVOCs detected by BME680. + BSEC_OUTPUT_STATIC_IAQ, // Unscaled Index for Air Quality estimate + BSEC_OUTPUT_CO2_EQUIVALENT, // CO2 equivalent estimate [ppm] + BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, // Breath VOC concentration estimate [ppm] + BSEC_OUTPUT_RAW_TEMPERATURE, // Temperature sensor signal [degrees Celsius] Temperature directly measured by BME680 in degree Celsius. This value is cross-influenced by the sensor heating and device specific heating. + BSEC_OUTPUT_RAW_PRESSURE, // Pressure sensor signal [Pa] Pressure directly measured by the BME680 in Pa. + BSEC_OUTPUT_RAW_HUMIDITY, // Relative humidity sensor signal [%] Relative humidity directly measured by the BME680 in %. This value is cross-influenced by the sensor heating and device specific heating. + BSEC_OUTPUT_RAW_GAS, // Gas sensor signal [Ohm] Gas resistance measured directly by the BME680 in Ohm.The resistance value changes due to varying VOC concentrations (the higher the concentration of reducing VOCs, the lower the resistance and vice versa). + BSEC_OUTPUT_STABILIZATION_STATUS, // Gas sensor stabilization status [boolean] Indicates initial stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1). + BSEC_OUTPUT_RUN_IN_STATUS, // Gas sensor run-in status [boolean] Indicates power-on stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1) + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, // Sensor heat compensated temperature [degrees Celsius] Temperature measured by BME680 which is compensated for the influence of sensor (heater) in degree Celsius. The self heating introduced by the heater is depending on the sensor operation mode and the sensor supply voltage. + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, // Sensor heat compensated humidity [%] Relative measured by BME680 which is compensated for the influence of sensor (heater) in %. It converts the ::BSEC_INPUT_HUMIDITY from temperature ::BSEC_INPUT_TEMPERATURE to temperature ::BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE. + BSEC_OUTPUT_GAS_PERCENTAGE // Percentage of min and max filtered gas value [%] + }; + + /*** V A R I A B L E s & C O N S T A N T s ***/ + /* Public: strings to reduce flash memory usage (used more than twice) */ + static const char _enabled[]; + static const char _hadtopic[]; + + /* Public: Settings Strings*/ + static const char _nameI2CAdr[]; + static const char _nameInterval[]; + static const char _nameMaxAge[]; + static const char _namePubAc[]; + static const char _namePubSenState[]; + static const char _namePubAfterCalib[]; + static const char _namePublishChange[]; + static const char _nameTempScale[]; + static const char _nameTempOffset[]; + static const char _nameHADisc[]; + static const char _nameDelCalib[]; + + /* Public: Sensor names / Sensor short names */ + static const char _nameTemp[]; + static const char _nameHum[]; + static const char _namePress[]; + static const char _nameGasRes[]; + static const char _nameAHum[]; + static const char _nameDrewP[]; + static const char _nameIaq[]; + static const char _nameIaqAc[]; + static const char _nameIaqVerb[]; + static const char _nameStaticIaq[]; + static const char _nameStaticIaqVerb[]; + static const char _nameStaticIaqAc[]; + static const char _nameCo2[]; + static const char _nameCo2Ac[]; + static const char _nameVoc[]; + static const char _nameVocAc[]; + static const char _nameComGasAc[]; + static const char _nameGasPer[]; + static const char _nameGasPerAc[]; + static const char _namePauseOnActWL[]; + + static const char _nameStabStatus[]; + static const char _nameRunInStatus[]; + + /* Public: Sensor Units */ + static const char _unitTemp[]; + static const char _unitHum[]; + static const char _unitPress[]; + static const char _unitGasres[]; + static const char _unitAHum[]; + static const char _unitDrewp[]; + static const char _unitIaq[]; + static const char _unitStaticIaq[]; + static const char _unitCo2[]; + static const char _unitVoc[]; + static const char _unitGasPer[]; + static const char _unitNone[]; + + static const char _unitCelsius[]; + static const char _unitFahrenheit[]; + }; // UsermodBME68X class definition End + + /*** Setting C O N S T A N T S ***/ + /* Private: Settings Strings*/ + const char UsermodBME68X::_enabled[] PROGMEM = "Enabled"; + const char UsermodBME68X::_hadtopic[] PROGMEM = "homeassistant/sensor/"; + + const char UsermodBME68X::_nameI2CAdr[] PROGMEM = "i2C Address"; + const char UsermodBME68X::_nameInterval[] PROGMEM = "Interval"; + const char UsermodBME68X::_nameMaxAge[] PROGMEM = "Max Age"; + const char UsermodBME68X::_namePublishChange[] PROGMEM = "Pub changes only"; + const char UsermodBME68X::_namePubAc[] PROGMEM = "Pub Accuracy"; + const char UsermodBME68X::_namePubSenState[] PROGMEM = "Pub Calib State"; + const char UsermodBME68X::_namePubAfterCalib[] PROGMEM = "Pub After Calib"; + const char UsermodBME68X::_nameTempScale[] PROGMEM = "Temp Scale"; + const char UsermodBME68X::_nameTempOffset[] PROGMEM = "Temp Offset"; + const char UsermodBME68X::_nameHADisc[] PROGMEM = "HA Discovery"; + const char UsermodBME68X::_nameDelCalib[] PROGMEM = "Del Calibration Hist"; + const char UsermodBME68X::_namePauseOnActWL[] PROGMEM = "Pause while WLED active"; + + /* Private: Sensor names / Sensor short name */ + const char UsermodBME68X::_nameTemp[] PROGMEM = "Temperature"; + const char UsermodBME68X::_nameHum[] PROGMEM = "Humidity"; + const char UsermodBME68X::_namePress[] PROGMEM = "Pressure"; + const char UsermodBME68X::_nameGasRes[] PROGMEM = "Gas-Resistance"; + const char UsermodBME68X::_nameAHum[] PROGMEM = "Absolute-Humidity"; + const char UsermodBME68X::_nameDrewP[] PROGMEM = "Drew-Point"; + const char UsermodBME68X::_nameIaq[] PROGMEM = "IAQ"; + const char UsermodBME68X::_nameIaqVerb[] PROGMEM = "IAQ-Verbal"; + const char UsermodBME68X::_nameStaticIaq[] PROGMEM = "Static-IAQ"; + const char UsermodBME68X::_nameStaticIaqVerb[] PROGMEM = "Static-IAQ-Verbal"; + const char UsermodBME68X::_nameCo2[] PROGMEM = "CO2"; + const char UsermodBME68X::_nameVoc[] PROGMEM = "VOC"; + const char UsermodBME68X::_nameGasPer[] PROGMEM = "Gas-Percentage"; + const char UsermodBME68X::_nameIaqAc[] PROGMEM = "IAQ-Accuracy"; + const char UsermodBME68X::_nameStaticIaqAc[] PROGMEM = "Static-IAQ-Accuracy"; + const char UsermodBME68X::_nameCo2Ac[] PROGMEM = "CO2-Accuracy"; + const char UsermodBME68X::_nameVocAc[] PROGMEM = "VOC-Accuracy"; + const char UsermodBME68X::_nameGasPerAc[] PROGMEM = "Gas-Percentage-Accuracy"; + const char UsermodBME68X::_nameStabStatus[] PROGMEM = "Stab-Status"; + const char UsermodBME68X::_nameRunInStatus[] PROGMEM = "Run-In-Status"; + + /* Private Units */ + const char UsermodBME68X::_unitTemp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit + const char UsermodBME68X::_unitHum[] PROGMEM = "%"; + const char UsermodBME68X::_unitPress[] PROGMEM = "hPa"; + const char UsermodBME68X::_unitGasres[] PROGMEM = "kΩ"; + const char UsermodBME68X::_unitAHum[] PROGMEM = "g/m³"; + const char UsermodBME68X::_unitDrewp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit + const char UsermodBME68X::_unitIaq[] PROGMEM = " "; // No unit + const char UsermodBME68X::_unitStaticIaq[] PROGMEM = " "; // No unit + const char UsermodBME68X::_unitCo2[] PROGMEM = "ppm"; + const char UsermodBME68X::_unitVoc[] PROGMEM = "ppm"; + const char UsermodBME68X::_unitGasPer[] PROGMEM = "%"; + const char UsermodBME68X::_unitNone[] PROGMEM = ""; + + const char UsermodBME68X::_unitCelsius[] PROGMEM = "°C"; // Symbol for Celsius + const char UsermodBME68X::_unitFahrenheit[] PROGMEM = "°F"; // Symbol for Fahrenheit + + /* Load Sensor Settings */ + const uint8_t UsermodBME68X::bsec_config_iaq[] = { + #include "config/generic_33v_3s_28d/bsec_iaq.txt" // Allow 28 days for calibration because the WLED module normally stays in the same place anyway + }; + + + /************************************************************************************************************/ + /********************************************* M A I N C O D E *********************************************/ + /************************************************************************************************************/ + + /** + * @brief Called by WLED: Setup of the usermod + */ + void UsermodBME68X::setup() { + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Initialize" ESC_STYLE_RESET)); + + /* Check, if i2c is activated */ + if (i2c_scl < 0 || i2c_sda < 0) { + settings.enabled = false; // Disable usermod once i2c is not running + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "I2C is not activated. Please activate I2C first." GOGAB_FAIL)); + return; + } + + flags.InitSuccessful = true; // Will be set to false on need + + /* Set data structure pointers */ + ValuesPtr = &valuesA; + PrevValuesPtr = &valuesB; + + /* Init Library*/ + iaqSensor.begin(settings.I2cadress, Wire); // BME68X_I2C_ADDR_LOW + stringbuff = "BSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix); + DEBUG_PRINT(F(UMOD_NAME)); + DEBUG_PRINTLN(F(stringbuff.c_str())); + + /* Init Sensor*/ + iaqSensor.setConfig(bsec_config_iaq); + iaqSensor.updateSubscription(sensorList, 13, BSEC_SAMPLE_RATE_LP); + iaqSensor.setTPH(BME68X_OS_2X, BME68X_OS_16X, BME68X_OS_1X); // Set the temperature, Pressure and Humidity over-sampling + iaqSensor.setTemperatureOffset(settings.tempOffset); // set the temperature offset in degree Celsius + loadState(); // Load the old calibration data + checkIaqSensorStatus(); // Check the sensor status + // HomeAssistantDiscovery(); + DEBUG_PRINTLN(F(INFO_COLUMN GOGAB_DONE)); + } + + /** + * @brief Called by WLED: Main loop called by WLED + * + */ + void UsermodBME68X::loop() { + if (!settings.enabled || strip.isUpdating() || !flags.InitSuccessful) return; // Leave if not enabled or string is updating or init failed + + if (settings.pauseOnActiveWled && strip.getBrightness()) return; // Workarround Known Issue: handing led update - Leave once pause on activ wled is active and wled is active + + timer.actual = millis(); // Timer to fetch new temperature, humidity and pressure data at intervals + + if (timer.actual - timer.lastRun >= settings.Interval * 1000) { + timer.lastRun = timer.actual; + + /* Get the sonsor measurments and publish them */ + if (iaqSensor.run()) { // iaqSensor.run() + getValues(); // Get the new values + + if (ValuesPtr->temperature != PrevValuesPtr->temperature || !settings.PublischChange) { // NOTE - negative dig means inactive + MQTT_publish(_nameTemp, ValuesPtr->temperature, settings.decimals.temperature); + } + if (ValuesPtr->humidity != PrevValuesPtr->humidity || !settings.PublischChange) { + MQTT_publish(_nameHum, ValuesPtr->humidity, settings.decimals.humidity); + } + if (ValuesPtr->pressure != PrevValuesPtr->pressure || !settings.PublischChange) { + MQTT_publish(_namePress, ValuesPtr->pressure, settings.decimals.humidity); + } + if (ValuesPtr->gasResistance != PrevValuesPtr->gasResistance || !settings.PublischChange) { + MQTT_publish(_nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance); + } + if (ValuesPtr->absHumidity != PrevValuesPtr->absHumidity || !settings.PublischChange) { + MQTT_publish(_nameAHum, PrevValuesPtr->absHumidity, settings.decimals.absHumidity); + } + if (ValuesPtr->drewPoint != PrevValuesPtr->drewPoint || !settings.PublischChange) { + MQTT_publish(_nameDrewP, PrevValuesPtr->drewPoint, settings.decimals.drewPoint); + } + if (ValuesPtr->iaq != PrevValuesPtr->iaq || !settings.PublischChange) { + MQTT_publish(_nameIaq, ValuesPtr->iaq, settings.decimals.iaq); + if (settings.pubAcc) MQTT_publish(_nameIaqAc, ValuesPtr->iaqAccuracy, 0); + if (settings.decimals.iaq>-1) { + if (settings.PublishIAQVerbal) { + if (ValuesPtr->iaq <= 50) cvalues.iaqVerbal = F("Excellent"); + else if (ValuesPtr->iaq <= 100) cvalues.iaqVerbal = F("Good"); + else if (ValuesPtr->iaq <= 150) cvalues.iaqVerbal = F("Lightly polluted"); + else if (ValuesPtr->iaq <= 200) cvalues.iaqVerbal = F("Moderately polluted"); + else if (ValuesPtr->iaq <= 250) cvalues.iaqVerbal = F("Heavily polluted"); + else if (ValuesPtr->iaq <= 350) cvalues.iaqVerbal = F("Severely polluted"); + else cvalues.iaqVerbal = F("Extremely polluted"); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameIaqVerb); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.iaqVerbal.c_str()); + } + } + } + if (ValuesPtr->staticIaq != PrevValuesPtr->staticIaq || !settings.PublischChange) { + MQTT_publish(_nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq); + if (settings.pubAcc) MQTT_publish(_nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0); + if (settings.decimals.staticIaq>-1) { + if (settings.PublishIAQVerbal) { + if (ValuesPtr->staticIaq <= 50) cvalues.staticIaqVerbal = F("Excellent"); + else if (ValuesPtr->staticIaq <= 100) cvalues.staticIaqVerbal = F("Good"); + else if (ValuesPtr->staticIaq <= 150) cvalues.staticIaqVerbal = F("Lightly polluted"); + else if (ValuesPtr->staticIaq <= 200) cvalues.staticIaqVerbal = F("Moderately polluted"); + else if (ValuesPtr->staticIaq <= 250) cvalues.staticIaqVerbal = F("Heavily polluted"); + else if (ValuesPtr->staticIaq <= 350) cvalues.staticIaqVerbal = F("Severely polluted"); + else cvalues.staticIaqVerbal = F("Extremely polluted"); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameStaticIaqVerb); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.staticIaqVerbal.c_str()); + } + } + } + if (ValuesPtr->co2 != PrevValuesPtr->co2 || !settings.PublischChange) { + MQTT_publish(_nameCo2, ValuesPtr->co2, settings.decimals.co2); + if (settings.pubAcc) MQTT_publish(_nameCo2Ac, ValuesPtr->co2Accuracy, 0); + } + if (ValuesPtr->Voc != PrevValuesPtr->Voc || !settings.PublischChange) { + MQTT_publish(_nameVoc, ValuesPtr->Voc, settings.decimals.Voc); + if (settings.pubAcc) MQTT_publish(_nameVocAc, ValuesPtr->VocAccuracy, 0); + } + if (ValuesPtr->gasPerc != PrevValuesPtr->gasPerc || !settings.PublischChange) { + MQTT_publish(_nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc); + if (settings.pubAcc) MQTT_publish(_nameGasPerAc, ValuesPtr->gasPercAccuracy, 0); + } + + /**** Publish Sensor State Entrys *****/ + if ((ValuesPtr->stabStatus != PrevValuesPtr->stabStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameStabStatus, ValuesPtr->stabStatus, 0); + if ((ValuesPtr->runInStatus != PrevValuesPtr->runInStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameRunInStatus, ValuesPtr->runInStatus, 0); + + /* Check accuracies - if accurasy level 3 is reached -> save calibration data */ + if ((ValuesPtr->iaqAccuracy != PrevValuesPtr->iaqAccuracy) && ValuesPtr->iaqAccuracy == 3) flags.SaveState = true; // Save after calibration / recalibration + if ((ValuesPtr->staticIaqAccuracy != PrevValuesPtr->staticIaqAccuracy) && ValuesPtr->staticIaqAccuracy == 3) flags.SaveState = true; + if ((ValuesPtr->co2Accuracy != PrevValuesPtr->co2Accuracy) && ValuesPtr->co2Accuracy == 3) flags.SaveState = true; + if ((ValuesPtr->VocAccuracy != PrevValuesPtr->VocAccuracy) && ValuesPtr->VocAccuracy == 3) flags.SaveState = true; + if ((ValuesPtr->gasPercAccuracy != PrevValuesPtr->gasPercAccuracy) && ValuesPtr->gasPercAccuracy == 3) flags.SaveState = true; + + if (flags.SaveState) saveState(); // Save if the save state flag is set + } + } + } + + /** + * @brief Retrieves the sensor data and truncates it to the requested decimal places + * + */ + void UsermodBME68X::getValues() { + /* Swap the point to the data structures */ + swap = PrevValuesPtr; + PrevValuesPtr = ValuesPtr; + ValuesPtr = swap; + + /* Float Values */ + ValuesPtr->temperature = roundf(iaqSensor.temperature * powf(10, settings.decimals.temperature)) / powf(10, settings.decimals.temperature); + ValuesPtr->humidity = roundf(iaqSensor.humidity * powf(10, settings.decimals.humidity)) / powf(10, settings.decimals.humidity); + ValuesPtr->pressure = roundf(iaqSensor.pressure * powf(10, settings.decimals.pressure)) / powf(10, settings.decimals.pressure) /100; // Pa 2 hPa + ValuesPtr->gasResistance = roundf(iaqSensor.gasResistance * powf(10, settings.decimals.gasResistance)) /powf(10, settings.decimals.gasResistance) /1000; // Ohm 2 KOhm + ValuesPtr->iaq = roundf(iaqSensor.iaq * powf(10, settings.decimals.iaq)) / powf(10, settings.decimals.iaq); + ValuesPtr->staticIaq = roundf(iaqSensor.staticIaq * powf(10, settings.decimals.staticIaq)) / powf(10, settings.decimals.staticIaq); + ValuesPtr->co2 = roundf(iaqSensor.co2Equivalent * powf(10, settings.decimals.co2)) / powf(10, settings.decimals.co2); + ValuesPtr->Voc = roundf(iaqSensor.breathVocEquivalent * powf(10, settings.decimals.Voc)) / powf(10, settings.decimals.Voc); + ValuesPtr->gasPerc = roundf(iaqSensor.gasPercentage * powf(10, settings.decimals.gasPerc)) / powf(10, settings.decimals.gasPerc); + + /* Calculate Absolute Humidity [g/m³] */ + if (settings.decimals.absHumidity>-1) { + const float mw = 18.01534; // molar mass of water g/mol + const float r = 8.31447215; // Universal gas constant J/mol/K + ValuesPtr->absHumidity = (6.112 * powf(2.718281828, (17.67 * ValuesPtr->temperature) / (ValuesPtr->temperature + 243.5)) * ValuesPtr->humidity * mw) / ((273.15 + ValuesPtr->temperature) * r); // in ppm + } + /* Calculate Drew Point (C°) */ + if (settings.decimals.drewPoint>-1) { + ValuesPtr->drewPoint = (243.5 * (log( ValuesPtr->humidity / 100) + ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature))) / (17.67 - log(ValuesPtr->humidity / 100) - ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature)))); + } + + /* Convert to Fahrenheit when selected */ + if (settings.tempScale) { // settings.tempScale = 0 => Celsius, = 1 => Fahrenheit + ValuesPtr->temperature = ValuesPtr->temperature * 1.8 + 32; // Value stored in Fahrenheit + ValuesPtr->drewPoint = ValuesPtr->drewPoint * 1.8 + 32; + } + + /* Integer Values */ + ValuesPtr->iaqAccuracy = iaqSensor.iaqAccuracy; + ValuesPtr->staticIaqAccuracy = iaqSensor.staticIaqAccuracy; + ValuesPtr->co2Accuracy = iaqSensor.co2Accuracy; + ValuesPtr->VocAccuracy = iaqSensor.breathVocAccuracy; + ValuesPtr->gasPercAccuracy = iaqSensor.gasPercentageAccuracy; + ValuesPtr->stabStatus = iaqSensor.stabStatus; + ValuesPtr->runInStatus = iaqSensor.runInStatus; + } + + + /** + * @brief Sends the current sensor data via MQTT + * @param topic Suptopic of the sensor as const char + * @param value Current sensor value as float + */ + void UsermodBME68X::MQTT_publish(const char* topic, const float& value, const int8_t& dig) { + if (dig<0) return; + if (WLED_MQTT_CONNECTED) { + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); + mqtt->publish(charbuffer, 0, false, String(value, dig).c_str()); + } + } + + /** + * @brief Called by WLED: Initialize the MQTT parts when the connection to the MQTT server is established. + * @param bool Session Present + */ + void UsermodBME68X::onMqttConnect(bool sessionPresent) { + DEBUG_PRINTLN(UMOD_DEBUG_NAME "OnMQTTConnect event fired"); + HomeAssistantDiscovery(); + + if (!flags.MqttInitialized) { + flags.MqttInitialized=true; + DEBUG_PRINTLN(UMOD_DEBUG_NAME "MQTT first connect"); + } + } + + + /** + * @brief MQTT initialization to generate the mqtt topic strings. This initialization also creates the HomeAssistat device configuration (HA Discovery), which home assinstant automatically evaluates to create a device. + */ + void UsermodBME68X::HomeAssistantDiscovery() { + if (!settings.HomeAssistantDiscovery || !flags.InitSuccessful || !settings.enabled) return; // Leave once HomeAssistant Discovery is inactive + + DEBUG_PRINTLN(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Creating HomeAssistant Discovery Mqtt-Entrys" ESC_STYLE_RESET); + + /* Sensor Values */ + MQTT_PublishHASensor(_nameTemp, "TEMPERATURE", tempScale.c_str(), settings.decimals.temperature ); // Temperature + MQTT_PublishHASensor(_namePress, "ATMOSPHERIC_PRESSURE", _unitPress, settings.decimals.pressure ); // Pressure + MQTT_PublishHASensor(_nameHum, "HUMIDITY", _unitHum, settings.decimals.humidity ); // Humidity + MQTT_PublishHASensor(_nameGasRes, "GAS", _unitGasres, settings.decimals.gasResistance ); // There is no device class for resistance in HA yet: https://developers.home-assistant.io/docs/core/entity/sensor/ + MQTT_PublishHASensor(_nameAHum, "HUMIDITY", _unitAHum, settings.decimals.absHumidity ); // Absolute Humidity + MQTT_PublishHASensor(_nameDrewP, "TEMPERATURE", tempScale.c_str(), settings.decimals.drewPoint ); // Drew Point + MQTT_PublishHASensor(_nameIaq, "AQI", _unitIaq, settings.decimals.iaq ); // IAQ + MQTT_PublishHASensor(_nameIaqVerb, "", _unitNone, settings.PublishIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor) + MQTT_PublishHASensor(_nameStaticIaq, "AQI", _unitNone, settings.decimals.staticIaq ); // Static IAQ + MQTT_PublishHASensor(_nameStaticIaqVerb, "", _unitNone, settings.PublishStaticIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor + MQTT_PublishHASensor(_nameCo2, "CO2", _unitCo2, settings.decimals.co2 ); // CO2 + MQTT_PublishHASensor(_nameVoc, "VOLATILE_ORGANIC_COMPOUNDS", _unitVoc, settings.decimals.Voc ); // VOC + MQTT_PublishHASensor(_nameGasPer, "AQI", _unitGasPer, settings.decimals.gasPerc ); // Gas % + + /* Accuracys - switched off once publishAccuracy=0 or the main value is switched of by digs set to a negative number */ + MQTT_PublishHASensor(_nameIaqAc, "AQI", _unitNone, settings.pubAcc - 1 + settings.decimals.iaq * settings.pubAcc, 1); // Option 1: Diagnostics Sektion + MQTT_PublishHASensor(_nameStaticIaqAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.staticIaq * settings.pubAcc, 1); + MQTT_PublishHASensor(_nameCo2Ac, "", _unitNone, settings.pubAcc - 1 + settings.decimals.co2 * settings.pubAcc, 1); + MQTT_PublishHASensor(_nameVocAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.Voc * settings.pubAcc, 1); + MQTT_PublishHASensor(_nameGasPerAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.gasPerc * settings.pubAcc, 1); + + MQTT_PublishHASensor(_nameStabStatus, "", _unitNone, settings.publishSensorState - 1, 1); + MQTT_PublishHASensor(_nameRunInStatus, "", _unitNone, settings.publishSensorState - 1, 1); + + DEBUG_PRINTLN(UMOD_DEBUG_NAME GOGAB_DONE); + } + + /** + * @brief These MQTT entries are responsible for the Home Assistant Discovery of the sensors. HA is shown here where to look for the sensor data. This entry therefore only needs to be sent once. + * Important note: In order to find everything that is sent from this device to Home Assistant via MQTT under the same device name, the "device/identifiers" entry must be the same. + * I use the MQTT device name here. If other user mods also use the HA Discovery, it is recommended to set the identifier the same. Otherwise you would have several devices, + * even though it is one device. I therefore only use the MQTT client name set in WLED here. + * @param name Name of the sensor + * @param topic Topic of the live sensor data + * @param unitOfMeasurement Unit of the measurment + * @param digs Number of decimal places + * @param option Set to true if the sensor is part of diagnostics (dafault 0) + */ + void UsermodBME68X::MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option) { + DEBUG_PRINT(UMOD_DEBUG_NAME "\t" + name); + + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, name.c_str()); // Current values will be posted here + String basetopic = String(_hadtopic) + mqttClientID + F("/") + name + F("/config"); // This is the place where Home Assinstant Discovery will check for new devices + + if (digs < 0) { // if digs are set to -1 -> entry deactivated + /* Delete MQTT Entry */ + if (WLED_MQTT_CONNECTED) { + mqtt->publish(basetopic.c_str(), 0, true, ""); // Send emty entry to delete + DEBUG_PRINTLN(INFO_COLUMN "deleted"); + } + } else { + /* Create all the necessary HAD MQTT entrys - see: https://www.home-assistant.io/integrations/sensor.mqtt/#configuration-variables */ + DynamicJsonDocument jdoc(700); // json document + // See: https://www.home-assistant.io/integrations/mqtt/ + JsonObject avail = jdoc.createNestedObject(F("avty")); // 'avty': 'availability' + avail[F("topic")] = mqttDeviceTopic + String("/status"); // An MQTT topic subscribed to receive availability (online/offline) updates. + avail[F("payload_available")] = "online"; + avail[F("payload_not_available")] = "offline"; + JsonObject device = jdoc.createNestedObject(F("device")); // Information about the device this sensor is a part of to tie it into the device registry. Only works when unique_id is set. At least one of identifiers or connections must be present to identify the device. + device[F("name")] = serverDescription; + device[F("identifiers")] = String(mqttClientID); + device[F("manufacturer")] = F("WLED"); + device[F("model")] = UMOD_DEVICE; + device[F("sw_version")] = versionString; + device[F("hw_version")] = F(HARDWARE_VERSION); + + if (deviceClass != "") jdoc[F("device_class")] = deviceClass; // The type/class of the sensor to set the icon in the frontend. The device_class can be null + if (option == 1) jdoc[F("entity_category")] = "diagnostic"; // Option 1: The category of the entity | When set, the entity category must be diagnostic for sensors. + if (option == 2) jdoc[F("mode")] = "text"; // Option 2: Set text mode | + jdoc[F("expire_after")] = 1800; // If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. Default the sensors state never expires. + jdoc[F("name")] = name; // The name of the MQTT sensor. Without server/module/device name. The device name will be added by HomeAssinstant anyhow + if (unitOfMeasurement != "") jdoc[F("state_class")] = "measurement"; // NOTE: This entry is missing in some other usermods. But it is very important. Because only with this entry, you can use statistics (such as statistical graphs). + jdoc[F("state_topic")] = charbuffer; // The MQTT topic subscribed to receive sensor values. If device_class, state_class, unit_of_measurement or suggested_display_precision is set, and a numeric value is expected, an empty value '' will be ignored and will not update the state, a 'null' value will set the sensor to an unknown state. The device_class can be null. + jdoc[F("unique_id")] = String(mqttClientID) + "-" + name; // An ID that uniquely identifies this sensor. If two sensors have the same unique ID, Home Assistant will raise an exception. + if (unitOfMeasurement != "") jdoc[F("unit_of_measurement")] = unitOfMeasurement; // Defines the units of measurement of the sensor, if any. The unit_of_measurement can be null. + + DEBUG_PRINTF(" (%d bytes)", jdoc.memoryUsage()); + + stringbuff = ""; // clear string buffer + serializeJson(jdoc, stringbuff); // JSON to String + + if (WLED_MQTT_CONNECTED) { // Check if MQTT Connected, otherwise it will crash the 8266 + mqtt->publish(basetopic.c_str(), 0, true, stringbuff.c_str()); // Publish the HA discovery sensor entry + DEBUG_PRINTLN(INFO_COLUMN "published"); + } + } + } + + /** + * @brief Called by WLED: Publish Sensor Information to Info Page + * @param JsonObject Pointer + */ + void UsermodBME68X::addToJsonInfo(JsonObject& root) { + //DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Add to info event")); + JsonObject user = root[F("u")]; + + if (user.isNull()) + user = root.createNestedObject(F("u")); + + if (!flags.InitSuccessful) { + // Init was not seccessful - let the user know + JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); + temperature_json.add(F("not found")); + JsonArray humidity_json = user.createNestedArray(F("BMW68x Reason")); + humidity_json.add(InfoPageStatusLine); + } + else if (!settings.enabled) { + JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); + temperature_json.add(F("disabled")); + } + else { + InfoHelper(user, _nameTemp, ValuesPtr->temperature, settings.decimals.temperature, tempScale.c_str()); + InfoHelper(user, _nameHum, ValuesPtr->humidity, settings.decimals.humidity, _unitHum); + InfoHelper(user, _namePress, ValuesPtr->pressure, settings.decimals.pressure, _unitPress); + InfoHelper(user, _nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance, _unitGasres); + InfoHelper(user, _nameAHum, ValuesPtr->absHumidity, settings.decimals.absHumidity, _unitAHum); + InfoHelper(user, _nameDrewP, ValuesPtr->drewPoint, settings.decimals.drewPoint, tempScale.c_str()); + InfoHelper(user, _nameIaq, ValuesPtr->iaq, settings.decimals.iaq, _unitIaq); + InfoHelper(user, _nameIaqVerb, cvalues.iaqVerbal, settings.PublishIAQVerbal); + InfoHelper(user, _nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq, _unitStaticIaq); + InfoHelper(user, _nameStaticIaqVerb,cvalues.staticIaqVerbal, settings.PublishStaticIAQVerbal); + InfoHelper(user, _nameCo2, ValuesPtr->co2, settings.decimals.co2, _unitCo2); + InfoHelper(user, _nameVoc, ValuesPtr->Voc, settings.decimals.Voc, _unitVoc); + InfoHelper(user, _nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc, _unitGasPer); + + if (settings.pubAcc) { + if (settings.decimals.iaq >= 0) InfoHelper(user, _nameIaqAc, ValuesPtr->iaqAccuracy, 0, " "); + if (settings.decimals.staticIaq >= 0) InfoHelper(user, _nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0, " "); + if (settings.decimals.co2 >= 0) InfoHelper(user, _nameCo2Ac, ValuesPtr->co2Accuracy, 0, " "); + if (settings.decimals.Voc >= 0) InfoHelper(user, _nameVocAc, ValuesPtr->VocAccuracy, 0, " "); + if (settings.decimals.gasPerc >= 0) InfoHelper(user, _nameGasPerAc, ValuesPtr->gasPercAccuracy, 0, " "); + } + + if (settings.publishSensorState) { + InfoHelper(user, _nameStabStatus, ValuesPtr->stabStatus, 0, " "); + InfoHelper(user, _nameRunInStatus, ValuesPtr->runInStatus, 0, " "); + } + } + } + + /** + * @brief Info Page helper function + * @param root JSON object + * @param name Name of the sensor as char + * @param sensorvalue Value of the sensor as float + * @param decimals Decimal places of the value + * @param unit Unit of the sensor + */ + void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit) { + if (decimals > -1) { + JsonArray sub_json = root.createNestedArray(name); + sub_json.add(roundf(sensorvalue * powf(10, decimals)) / powf(10, decimals)); + sub_json.add(unit); + } + } + + /** + * @brief Info Page helper function (overload) + * @param root JSON object + * @param name Name of the sensor + * @param sensorvalue Value of the sensor as string + * @param status Status of the value (active/inactive) + */ + void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status) { + if (status) { + JsonArray sub_json = root.createNestedArray(name); + sub_json.add(sensorvalue); + } + } + + /** + * @brief Called by WLED: Adds the usermodul neends on the config page for user modules + * @param JsonObject Pointer + * + * @see Usermod::addToConfig() + * @see UsermodManager::addToConfig() + */ + void UsermodBME68X::addToConfig(JsonObject& root) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Creating configuration pages content: ")); + + JsonObject top = root.createNestedObject(FPSTR(UMOD_NAME)); + /* general settings */ + top[FPSTR(_enabled)] = settings.enabled; + top[FPSTR(_nameI2CAdr)] = settings.I2cadress; + top[FPSTR(_nameInterval)] = settings.Interval; + top[FPSTR(_namePublishChange)] = settings.PublischChange; + top[FPSTR(_namePubAc)] = settings.pubAcc; + top[FPSTR(_namePubSenState)] = settings.publishSensorState; + top[FPSTR(_nameTempScale)] = settings.tempScale; + top[FPSTR(_nameTempOffset)] = settings.tempOffset; + top[FPSTR(_nameHADisc)] = settings.HomeAssistantDiscovery; + top[FPSTR(_namePauseOnActWL)] = settings.pauseOnActiveWled; + top[FPSTR(_nameDelCalib)] = flags.DeleteCaibration; + + /* Digs */ + JsonObject sensors_json = top.createNestedObject("Sensors"); + sensors_json[FPSTR(_nameTemp)] = settings.decimals.temperature; + sensors_json[FPSTR(_nameHum)] = settings.decimals.humidity; + sensors_json[FPSTR(_namePress)] = settings.decimals.pressure; + sensors_json[FPSTR(_nameGasRes)] = settings.decimals.gasResistance; + sensors_json[FPSTR(_nameAHum)] = settings.decimals.absHumidity; + sensors_json[FPSTR(_nameDrewP)] = settings.decimals.drewPoint; + sensors_json[FPSTR(_nameIaq)] = settings.decimals.iaq; + sensors_json[FPSTR(_nameIaqVerb)] = settings.PublishIAQVerbal; + sensors_json[FPSTR(_nameStaticIaq)] = settings.decimals.staticIaq; + sensors_json[FPSTR(_nameStaticIaqVerb)] = settings.PublishStaticIAQVerbal; + sensors_json[FPSTR(_nameCo2)] = settings.decimals.co2; + sensors_json[FPSTR(_nameVoc)] = settings.decimals.Voc; + sensors_json[FPSTR(_nameGasPer)] = settings.decimals.gasPerc; + + DEBUG_PRINTLN(F(GOGAB_OK)); + } + + /** + * @brief Called by WLED: Add dropdown and additional infos / structure + * @see Usermod::appendConfigData() + * @see UsermodManager::appendConfigData() + */ + void UsermodBME68X::appendConfigData() { + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'read interval [seconds]');"), UMOD_NAME, _nameInterval); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'only if value changes');"), UMOD_NAME, _namePublishChange); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'maximum age of a message in seconds');"), UMOD_NAME, _nameMaxAge); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'Gas related values are only published after the gas sensor has been calibrated');"), UMOD_NAME, _namePubAfterCalib); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'*) Set to minus to deactivate (all sensors)');"), UMOD_NAME, _nameTemp); oappend(charbuffer); + + /* Dropdown for Celsius/Fahrenheit*/ + oappend(F("dd=addDropdown('")); + oappend(UMOD_NAME); + oappend(F("','")); + oappend(_nameTempScale); + oappend(F("');")); + oappend(F("addOption(dd,'Celsius',0);")); + oappend(F("addOption(dd,'Fahrenheit',1);")); + + /* i²C Address*/ + oappend(F("dd=addDropdown('")); + oappend(UMOD_NAME); + oappend(F("','")); + oappend(_nameI2CAdr); + oappend(F("');")); + oappend(F("addOption(dd,'0x76',0x76);")); + oappend(F("addOption(dd,'0x77',0x77);")); + } + + /** + * @brief Called by WLED: Read Usermod Config Settings 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) + * 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. + * NOTE: Here are the default settings of the user module + * @param JsonObject Pointer + * @return bool + * @see Usermod::readFromConfig() + * @see UsermodManager::readFromConfig() + */ + bool UsermodBME68X::readFromConfig(JsonObject& root) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Reading configuration: ")); + + JsonObject top = root[FPSTR(UMOD_NAME)]; + bool configComplete = !top.isNull(); + + /* general settings */ /* DEFAULTS */ + configComplete &= getJsonValue(top[FPSTR(_enabled)], settings.enabled, 1 ); // Usermod enabled per default + configComplete &= getJsonValue(top[FPSTR(_nameI2CAdr)], settings.I2cadress, 0x77 ); // Defalut IC2 adress set to 0x77 (some modules are set to 0x76) + configComplete &= getJsonValue(top[FPSTR(_nameInterval)], settings.Interval, 1 ); // Executed every second + configComplete &= getJsonValue(top[FPSTR(_namePublishChange)], settings.PublischChange, false ); // Publish changed values only + configComplete &= getJsonValue(top[FPSTR(_nameTempScale)], settings.tempScale, 0 ); // Temp sale set to Celsius (1=Fahrenheit) + configComplete &= getJsonValue(top[FPSTR(_nameTempOffset)], settings.tempOffset, 0 ); // Temp offset is set to 0 (Celsius) + configComplete &= getJsonValue(top[FPSTR(_namePubSenState)], settings.publishSensorState, 1 ); // Publish the sensor states + configComplete &= getJsonValue(top[FPSTR(_namePubAc)], settings.pubAcc, 1 ); // Publish accuracy values + configComplete &= getJsonValue(top[FPSTR(_nameHADisc)], settings.HomeAssistantDiscovery, true ); // Activate HomeAssistant Discovery (this Module will be shown as MQTT device in HA) + configComplete &= getJsonValue(top[FPSTR(_namePauseOnActWL)], settings.pauseOnActiveWled, false ); // Pause on active WLED not activated per default + configComplete &= getJsonValue(top[FPSTR(_nameDelCalib)], flags.DeleteCaibration, false ); // IF checked the calibration file will be delete when the save button is pressed + + /* Decimal places */ /* no of digs / -1 means deactivated */ + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameTemp)], settings.decimals.temperature, 1 ); // One decimal places + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameHum)], settings.decimals.humidity, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_namePress)], settings.decimals.pressure, 0 ); // Zero decimal places + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasRes)], settings.decimals.gasResistance, -1 ); // deavtivated + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameDrewP)], settings.decimals.drewPoint, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameAHum)], settings.decimals.absHumidity, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaq)], settings.decimals.iaq, 0 ); // Index for Air Quality Number is active + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaqVerb)], settings.PublishIAQVerbal, -1 ); // deactivated - Index for Air Quality (IAQ) verbal classification + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaq)], settings.decimals.staticIaq, 0 ); // activated - Static IAQ is better than IAQ for devices that are not moved + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaqVerb)], settings.PublishStaticIAQVerbal, 0 ); // activated + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameCo2)], settings.decimals.co2, 0 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameVoc)], settings.decimals.Voc, 0 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasPer)], settings.decimals.gasPerc, 0 ); + + DEBUG_PRINTLN(F(GOGAB_OK)); + + /* Set the selected temperature unit */ + if (settings.tempScale) { + tempScale = F(_unitFahrenheit); + } + else { + tempScale = F(_unitCelsius); + } + + if (flags.DeleteCaibration) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Deleting Calibration File")); + flags.DeleteCaibration = false; + if (WLED_FS.remove(CALIB_FILE_NAME)) { + DEBUG_PRINTLN(F(GOGAB_OK)); + } + else { + DEBUG_PRINTLN(F(GOGAB_FAIL)); + } + } + + if (settings.Interval < 1) settings.Interval = 1; // Correct interval on need (A number less than 1 is not permitted) + iaqSensor.setTemperatureOffset(settings.tempOffset); // Set Temp Offset + + return configComplete; + } + + /** + * @brief Called by WLED: Retunrs the user modul id number + * + * @return uint16_t User module number + */ + uint16_t UsermodBME68X::getId() { + return USERMOD_ID_BME68X; + } + + + /** + * @brief Returns the current temperature in the scale which is choosen in settings + * @return Temperature value (°C or °F as choosen in settings) + */ + inline float UsermodBME68X::getTemperature() { + return ValuesPtr->temperature; + } + + /** + * @brief Returns the current humidity + * @return Humididty value (%) + */ + inline float UsermodBME68X::getHumidity() { + return ValuesPtr->humidity; + } + + /** + * @brief Returns the current pressure + * @return Pressure value (hPa) + */ + inline float UsermodBME68X::getPressure() { + return ValuesPtr->pressure; + } + + /** + * @brief Returns the current gas resistance + * @return Gas resistance value (kΩ) + */ + inline float UsermodBME68X::getGasResistance() { + return ValuesPtr->gasResistance; + } + + /** + * @brief Returns the current absolute humidity + * @return Absolute humidity value (g/m³) + */ + inline float UsermodBME68X::getAbsoluteHumidity() { + return ValuesPtr->absHumidity; + } + + /** + * @brief Returns the current dew point + * @return Dew point (°C or °F as choosen in settings) + */ + inline float UsermodBME68X::getDewPoint() { + return ValuesPtr->drewPoint; + } + + /** + * @brief Returns the current iaq (Indoor Air Quallity) + * @return Iaq value (0-500) + */ + inline float UsermodBME68X::getIaq() { + return ValuesPtr->iaq; + } + + /** + * @brief Returns the current static iaq (Indoor Air Quallity) (NOTE: Static iaq is the better choice than iaq for fixed devices such as the wled module) + * @return Static iaq value (float) + */ + inline float UsermodBME68X::getStaticIaq() { + return ValuesPtr->staticIaq; + } + + /** + * @brief Returns the current co2 + * @return Co2 value (ppm) + */ + inline float UsermodBME68X::getCo2() { + return ValuesPtr->co2; + } + + /** + * @brief Returns the current voc (Breath VOC concentration estimate [ppm]) + * @return Voc value (ppm) + */ + inline float UsermodBME68X::getVoc() { + return ValuesPtr->Voc; + } + + /** + * @brief Returns the current gas percentage + * @return Gas percentage value (%) + */ + inline float UsermodBME68X::getGasPerc() { + return ValuesPtr->gasPerc; + } + + /** + * @brief Returns the current iaq accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Iaq accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getIaqAccuracy() { + return ValuesPtr->iaqAccuracy ; + } + + /** + * @brief Returns the current static iaq accuracy accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Static iaq accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getStaticIaqAccuracy() { + return ValuesPtr->staticIaqAccuracy; + } + + /** + * @brief Returns the current co2 accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Co2 accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getCo2Accuracy() { + return ValuesPtr->co2Accuracy; + } + + /** + * @brief Returns the current voc accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Voc accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getVocAccuracy() { + return ValuesPtr->VocAccuracy; + } + + /** + * @brief Returns the current gas percentage accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Gas percentage accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getGasPercAccuracy() { + return ValuesPtr->gasPercAccuracy; + } + + /** + * @brief Returns the current stab status. + * Indicates when the sensor is ready after after switch-on + * @return stab status value (0 = switched on / 1 = stabilized) + */ + inline bool UsermodBME68X::getStabStatus() { + return ValuesPtr->stabStatus; + } + + /** + * @brief Returns the current run in status. + * Indicates if the sensor is undergoing initial stabilization during its first use after production + * @return Tun status accuracy value (0 = switched on first time / 1 = stabilized) + */ + inline bool UsermodBME68X::getRunInStatus() { + return ValuesPtr->runInStatus; + } + + + /** + * @brief Checks whether the library and the sensor are running. + */ + void UsermodBME68X::checkIaqSensorStatus() { + + if (iaqSensor.bsecStatus != BSEC_OK) { + InfoPageStatusLine = "BSEC Library "; + DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); + flags.InitSuccessful = false; + if (iaqSensor.bsecStatus < BSEC_OK) { + InfoPageStatusLine += " Error Code : " + String(iaqSensor.bsecStatus); + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + InfoPageStatusLine += " Warning Code : " + String(iaqSensor.bsecStatus); + DEBUG_PRINTLN(GOGAB_WARN); + } + } + else { + InfoPageStatusLine = "Sensor BME68X "; + DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); + + if (iaqSensor.bme68xStatus != BME68X_OK) { + flags.InitSuccessful = false; + if (iaqSensor.bme68xStatus < BME68X_OK) { + InfoPageStatusLine += "error code: " + String(iaqSensor.bme68xStatus); + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + InfoPageStatusLine += "warning code: " + String(iaqSensor.bme68xStatus); + DEBUG_PRINTLN(GOGAB_WARN); + } + } + else { + InfoPageStatusLine += F("OK"); + DEBUG_PRINTLN(GOGAB_OK); + } + } + } + + /** + * @brief Loads the calibration data from the file system of the device + */ + void UsermodBME68X::loadState() { + if (WLED_FS.exists(CALIB_FILE_NAME)) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Read the calibration file: ")); + File file = WLED_FS.open(CALIB_FILE_NAME, FILE_READ); + if (!file) { + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + file.read(bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + DEBUG_PRINTLN(GOGAB_OK); + iaqSensor.setState(bsecState); + } + } + else { + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Calibration file not found.")); + } + } + + /** + * @brief Saves the calibration data from the file system of the device + */ + void UsermodBME68X::saveState() { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Write the calibration file ")); + File file = WLED_FS.open(CALIB_FILE_NAME, FILE_WRITE); + if (!file) { + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + iaqSensor.getState(bsecState); + file.write(bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + stateUpdateCounter++; + DEBUG_PRINTF("(saved %d times)" GOGAB_OK "\n", stateUpdateCounter); + flags.SaveState = false; // Clear save state flag + + char contbuffer[30]; + + /* Timestamp */ + time_t curr_time; + tm* curr_tm; + time(&curr_time); + curr_tm = localtime(&curr_time); + + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Last Run"); + strftime(contbuffer, 30, "%d %B %Y - %T", curr_tm); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); + + snprintf(contbuffer, 30, "%d", stateUpdateCounter); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Count"); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); + } + } + + + static UsermodBME68X bme68x_v2; + REGISTER_USERMOD(bme68x_v2); \ No newline at end of file diff --git a/usermods/BME68X_v2/README.md b/usermods/BME68X_v2/README.md index 72ae25a57e..ee2670aa90 100644 --- a/usermods/BME68X_v2/README.md +++ b/usermods/BME68X_v2/README.md @@ -1,65 +1,70 @@ # Usermod BME68X -This usermod was developed for a BME680/BME68X sensor. The BME68X is not compatible with the BME280/BMP280 chip. It has its own library. The original 'BSEC Software Library' from Bosch was used to develop the code. The measured values are displayed on the WLED info page. + +This usermod was developed for a BME680/BME68X sensor. The BME68X is not compatible with the BME280/BMP280 chip. It has its own library. The original 'BSEC Software Library' from Bosch was used to develop the code. The measured values are displayed on the WLED info page.

In addition, the values are published on MQTT if this is active. The topic used for this is: 'wled/[MQTT Client ID]'. The Client ID is set in the WLED MQTT settings. +

If you use HomeAssistance discovery, the device tree for HomeAssistance is created. This is published under the topic 'homeassistant/sensor/[MQTT Client ID]' via MQTT. +

A device with the following sensors appears in HomeAssistant. Please note that MQTT must be activated in HomeAssistant. -

+

## Features + Raw sensor types - Sensor Accuracy Scale Range - -------------------------------------------------------------------------------------------------- - Temperature +/- 1.0 °C/°F -40 to 85 °C - Humidity +/- 3 % 0 to 100 % - Pressure +/- 1 hPa 300 to 1100 hPa - Gas Resistance Ohm +Sensor Accuracy Scale Range +----------------------------- +Temperature +/- 1.0 °C/°F -40 to 85 °C +Humidity +/- 3 % 0 to 100 % +Pressure +/- 1 hPa 300 to 1100 hPa +Gas Resistance Ohm The BSEC Library calculates the following values via the gas resistance - Sensor Accuracy Scale Range - -------------------------------------------------------------------------------------------------- - IAQ value between 0 and 500 - Static IAQ same as IAQ but for permanently installed devices - CO2 PPM - VOC PPM - Gas-Percentage % - +Sensor Accuracy Scale Range +----------------------------- +IAQ value between 0 and 500 +Static IAQ same as IAQ but for permanently installed devices +CO2 PPM +VOC PPM +Gas-Percentage % In addition the usermod calculates - Sensor Accuracy Scale Range - -------------------------------------------------------------------------------------------------- - Absolute humidity g/m³ - Dew point °C/°F +Sensor Accuracy Scale Range +----------------------------- + +Absolute humidity g/m³ +Dew point °C/°F ### IAQ (Indoor Air Quality) -The IAQ is divided into the following value groups. + +The IAQ is divided into the following value groups. +

For more detailed information, please consult the enclosed Bosch product description (BME680.pdf). - ## Calibration of the device -The gas sensor of the BME68X must be calibrated. This differs from the BME280, which does not require any calibration. +The gas sensor of the BME68X must be calibrated. This differs from the BME280, which does not require any calibration. There is a range of additional information for this, which the driver also provides. These values can be found in HomeAssistant under Diagnostics. - **STABILIZATION_STATUS**: Gas sensor stabilization status [boolean] Indicates initial stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1). - **RUN_IN_STATUS**: Gas sensor run-in status [boolean] Indicates power-on stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1) -Furthermore, all GAS based values have their own accuracy value. These have the following meaning: +Furthermore, all GAS based values have their own accuracy value. These have the following meaning: -- **Accuracy = 0** means the sensor is being stabilized (this can take a while on the first run) -- **Accuracy = 1** means that the previous measured values show too few differences and cannot be used for calibration. If the sensor is at accuracy 1 for too long, you must ensure that the ambient air is chaning. Opening the windows is fine. Or sometimes it is sufficient to breathe on the sensor for approx. 5 minutes. +- **Accuracy = 0** means the sensor is being stabilized (this can take a while on the first run) +- **Accuracy = 1** means that the previous measured values show too few differences and cannot be used for calibration. If the sensor is at accuracy 1 for too long, you must ensure that the ambient air is chaning. Opening the windows is fine. Or sometimes it is sufficient to breathe on the sensor for approx. 5 minutes. - **Accuracy = 2** means the sensor is currently calibrating. - **Accuracy = 3** means that the sensor has been successfully calibrated. Once accuracy 3 is reached, the calibration data is automatically written to the file system. This calibration data will be used again at the next start and will speed up the calibration. @@ -67,28 +72,29 @@ The IAQ index is therefore only meaningful if IAQ Accuracy = 3. In addition to t Reasonably reliable values are therefore only achieved when accuracy displays the value 3. +## Settings +The settings of the usermods are set in the usermod section of wled. -## Settings -The settings of the usermods are set in the usermod section of wled.

The possible settings are - **Enable:** Enables / disables the usermod - **I2C address:** I2C address of the sensor. You can choose between 0X77 & 0X76. The default is 0x77. -- **Interval:** Specifies the interval of seconds at which the usermod should be executed. The default is every second. -- **Pub Chages Only:** If this item is active, the values are only published if they have changed since the last publication. -- **Pub Accuracy:** The Accuracy values associated with the gas values are also published. -- **Pub Calib State:** If this item is active, STABILIZATION_STATUS& RUN_IN_STATUS are also published. +- **Interval:** Specifies the interval of seconds at which the usermod should be executed. The default is every second. +- **Pub Chages Only:** If this item is active, the values are only published if they have changed since the last publication. +- **Pub Accuracy:** The Accuracy values associated with the gas values are also published. +- **Pub Calib State:** If this item is active, STABILIZATION_STATUS& RUN_IN_STATUS are also published. - **Temp Scale:** Here you can choose between °C and °F. -- **Temp Offset:** The temperature offset is always set in °C. It must be converted for Fahrenheit. -- **HA Discovery:** If this item is active, the HomeAssistant sensor tree is created. +- **Temp Offset:** The temperature offset is always set in °C. It must be converted for Fahrenheit. +- **HA Discovery:** If this item is active, the HomeAssistant sensor tree is created. - **Pause While WLED Active:** If WLED has many LEDs to calculate, the computing power may no longer be sufficient to calculate the LEDs and read the sensor data. The LEDs then hang for a few microseconds, which can be seen. If this point is active, no sensor data is fetched as long as WLED is running. -- **Del Calibration Hist:** If a check mark is set here, the calibration file saved in the file system is deleted when the settings are saved. +- **Del Calibration Hist:** If a check mark is set here, the calibration file saved in the file system is deleted when the settings are saved. ### Sensors -Applies to all sensors. The number of decimal places is set here. If the sensor is set to -1, it will no longer be published. In addition, the IAQ values can be activated here in verbal form. + +Applies to all sensors. The number of decimal places is set here. If the sensor is set to -1, it will no longer be published. In addition, the IAQ values can be activated here in verbal form. It is recommended to use the Static IAQ for the IAQ values. This is recommended by Bosch for statically placed devices. @@ -99,8 +105,9 @@ Data is published over MQTT - make sure you've enabled the MQTT sync interface. 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. + - getTemperature(); The scale °C/°F is depended to the settings -- getHumidity(); +- getHumidity(); - getPressure(); - getGasResistance(); - getAbsoluteHumidity(); @@ -118,32 +125,36 @@ Methods also exist to read the read/calculated values from other WLED modules th - getStabStatus(); - getRunInStatus(); +## Compilation -## Compiling +To enable, compile with `BME68X` in `custom_usermods` (e.g. in `platformio_override.ini`) -To enable, compile with `USERMOD_BME68X` defined (e.g. in `platformio_override.ini`) and add the `BSEC Software Library` to the lib_deps. +Example: -``` -[env:esp32-BME680] -board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} -lib_deps = ${esp32.lib_deps} - boschsensortec/BSEC Software Library @ ^1.8.1492 ; USERMOD: BME680 -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} - -D USERMOD_BME68X ; USERMOD: BME680 +```[env:esp32_mySpecial] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} BME68X ``` ## Revision History + ### Version 1.0.0 + - First version of the BME68X_v user module + ### Version 1.0.1 + - Rebased to WELD Version 0.15 - Reworked some default settings - A problem with the default settings has been fixed +### Version 1.0.2 + +* Rebased to WELD Version 0.16 +* Fixed: Solved compilation problems related to some macro naming interferences. + ## Known problems + - MQTT goes online at device start. Shortly afterwards it goes offline and takes quite a while until it goes online again. The problem does not come from this user module, but from the WLED core. - If you save the settings often, WLED can get stuck. - If many LEDS are connected to WLED, reading the sensor can cause a small but noticeable hang. The "Pause While WLED Active" option was introduced as a workaround. diff --git a/usermods/BME68X_v2/library.json b/usermods/BME68X_v2/library.json new file mode 100644 index 0000000000..b315aa5d4b --- /dev/null +++ b/usermods/BME68X_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "BME68X", + "build": { "libArchive": false }, + "dependencies": { + "boschsensortec/BSEC Software Library":"^1.8.1492" + } +} diff --git a/usermods/BME68X_v2/usermod_bme68x.h b/usermods/BME68X_v2/usermod_bme68x.h deleted file mode 100644 index aca24d0a29..0000000000 --- a/usermods/BME68X_v2/usermod_bme68x.h +++ /dev/null @@ -1,1114 +0,0 @@ -/** - * @file usermod_BMW68X.h - * @author Gabriel A. Sieben (GeoGab) - * @brief Usermod for WLED to implement the BME680/BME688 sensor - * @version 1.0.0 - * @date 19 Feb 2024 - */ - -#pragma once -#warning ********************Included USERMOD_BME68X ******************** - -#define UMOD_DEVICE "ESP32" // NOTE - Set your hardware here -#define HARDWARE_VERSION "1.0" // NOTE - Set your hardware version here -#define UMOD_BME680X_SW_VERSION "1.0.1" // NOTE - Version of the User Mod -#define CALIB_FILE_NAME "/BME680X-Calib.hex" // NOTE - Calibration file name -#define UMOD_NAME "BME680X" // NOTE - User module name -#define UMOD_DEBUG_NAME "UM-BME680X: " // NOTE - Debug print module name addon - -/* Debug Print Text Coloring */ -#define ESC "\033" -#define ESC_CSI ESC "[" -#define ESC_STYLE_RESET ESC_CSI "0m" -#define ESC_CURSOR_COLUMN(n) ESC_CSI #n "G" - -#define ESC_FGCOLOR_BLACK ESC_CSI "30m" -#define ESC_FGCOLOR_RED ESC_CSI "31m" -#define ESC_FGCOLOR_GREEN ESC_CSI "32m" -#define ESC_FGCOLOR_YELLOW ESC_CSI "33m" -#define ESC_FGCOLOR_BLUE ESC_CSI "34m" -#define ESC_FGCOLOR_MAGENTA ESC_CSI "35m" -#define ESC_FGCOLOR_CYAN ESC_CSI "36m" -#define ESC_FGCOLOR_WHITE ESC_CSI "37m" -#define ESC_FGCOLOR_DEFAULT ESC_CSI "39m" - -/* Debug Print Special Text */ -#define INFO_COLUMN ESC_CURSOR_COLUMN(60) -#define OK INFO_COLUMN "[" ESC_FGCOLOR_GREEN "OK" ESC_STYLE_RESET "]" -#define FAIL INFO_COLUMN "[" ESC_FGCOLOR_RED "FAIL" ESC_STYLE_RESET "]" -#define WARN INFO_COLUMN "[" ESC_FGCOLOR_YELLOW "WARN" ESC_STYLE_RESET "]" -#define DONE INFO_COLUMN "[" ESC_FGCOLOR_CYAN "DONE" ESC_STYLE_RESET "]" - -#include "bsec.h" // Bosch sensor library -#include "wled.h" -#include - -/* UsermodBME68X class definition */ -class UsermodBME68X : public Usermod { - - public: - /* Public: Functions */ - uint16_t getId(); - void loop(); // Loop of the user module called by wled main in loop - void setup(); // Setup of the user module called by wled main - void addToConfig(JsonObject& root); // Extends the settings/user module settings page to include the user module requirements. The settings are written from the wled core to the configuration file. - void appendConfigData(); // Adds extra info to the config page of weld - bool readFromConfig(JsonObject& root); // Reads config values - void addToJsonInfo(JsonObject& root); // Adds user module info to the weld info page - - /* Wled internal functions which can be used by the core or other user mods */ - inline float getTemperature(); // Get Temperature in the selected scale of °C or °F - inline float getHumidity(); // ... - inline float getPressure(); - inline float getGasResistance(); - inline float getAbsoluteHumidity(); - inline float getDewPoint(); - inline float getIaq(); - inline float getStaticIaq(); - inline float getCo2(); - inline float getVoc(); - inline float getGasPerc(); - inline uint8_t getIaqAccuracy(); - inline uint8_t getStaticIaqAccuracy(); - inline uint8_t getCo2Accuracy(); - inline uint8_t getVocAccuracy(); - inline uint8_t getGasPercAccuracy(); - inline bool getStabStatus(); - inline bool getRunInStatus(); - - private: - /* Private: Functions */ - void HomeAssistantDiscovery(); - void MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option = 0); - void MQTT_publish(const char* topic, const float& value, const int8_t& dig); - void onMqttConnect(bool sessionPresent); - void checkIaqSensorStatus(); - void InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit); - void InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status); - void loadState(); - void saveState(); - void getValues(); - - /*** V A R I A B L E s & C O N S T A N T s ***/ - /* Private: Settings of Usermod BME68X */ - struct settings_t { - bool enabled; // true if user module is active - byte I2cadress; // Depending on the manufacturer, the BME680 has the address 0x76 or 0x77 - uint8_t Interval; // Interval of reading sensor data in seconds - uint16_t MaxAge; // Force the publication of the value of a sensor after these defined seconds at the latest - bool pubAcc; // Publish the accuracy values - bool publishSensorState; // Publisch the sensor calibration state - bool publishAfterCalibration ; // The IAQ/CO2/VOC/GAS value are only valid after the sensor has been calibrated. If this switch is active, the values are only sent after calibration - bool PublischChange; // Publish values even when they have not changed - bool PublishIAQVerbal; // Publish Index of Air Quality (IAQ) classification Verbal - bool PublishStaticIAQVerbal; // Publish Static Index of Air Quality (Static IAQ) Verbal - byte tempScale; // 0 -> Use Celsius, 1-> Use Fahrenheit - float tempOffset; // Temperature Offset - bool HomeAssistantDiscovery; // Publish Home Assistant Device Information - bool pauseOnActiveWled ; // If this is set to true, the user mod ist not executed while wled is active - - /* Decimal Places (-1 means inactive) */ - struct decimals_t { - int8_t temperature; - int8_t humidity; - int8_t pressure; - int8_t gasResistance; - int8_t absHumidity; - int8_t drewPoint; - int8_t iaq; - int8_t staticIaq; - int8_t co2; - int8_t Voc; - int8_t gasPerc; - } decimals; - } settings; - - /* Private: Flags */ - struct flags_t { - bool InitSuccessful = false; // Initialation was un-/successful - bool MqttInitialized = false; // MQTT Initialation done flag (first MQTT Connect) - bool SaveState = false; // Save the calibration data flag - bool DeleteCaibration = false; // If set the calib file will be deleted on the next round - } flags; - - /* Private: Measurement timers */ - struct timer_t { - long actual; // Actual time stamp - long lastRun; // Last measurement time stamp - } timer; - - /* Private: Various variables */ - String stringbuff; // General string stringbuff buffer - char charbuffer[128]; // General char stringbuff buffer - String InfoPageStatusLine; // Shown on the info page of WLED - String tempScale; // °C or °F - uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE]; // Calibration data array - uint16_t stateUpdateCounter; // Save state couter - static const uint8_t bsec_config_iaq[]; // Calibration Buffer - Bsec iaqSensor; // Sensor variable - - /* Private: Sensor values */ - struct values_t { - float temperature; // Temp [°C] (Sensor-compensated) - float humidity; // Relative humidity [%] (Sensor-compensated) - float pressure; // raw pressure [hPa] - float gasResistance; // raw gas restistance [Ohm] - float absHumidity; // UserMod calculated: Absolute Humidity [g/m³] - float drewPoint; // UserMod calculated: drew point [°C/°F] - float iaq; // IAQ (Indoor Air Quallity) - float staticIaq; // Satic IAQ - float co2; // CO2 [PPM] - float Voc; // VOC in [PPM] - float gasPerc; // Gas Percentage in [%] - uint8_t iaqAccuracy; // IAQ accuracy - IAQ Accuracy = 1 means value is inaccurate, IAQ Accuracy = 2 means sensor is being calibrated, IAQ Accuracy = 3 means sensor successfully calibrated. - uint8_t staticIaqAccuracy; // Static IAQ accuracy - uint8_t co2Accuracy; // co2 accuracy - uint8_t VocAccuracy; // voc accuracy - uint8_t gasPercAccuracy; // Gas percentage accuracy - bool stabStatus; // Indicates if the sensor is undergoing initial stabilization during its first use after production - bool runInStatus; // Indicates when the sensor is ready after after switch-on - } valuesA, valuesB, *ValuesPtr, *PrevValuesPtr, *swap; // Data Scructur A, Data Structur B, Pointers to switch between data channel A & B - - struct cvalues_t { - String iaqVerbal; // IAQ verbal - String staticIaqVerbal; // Static IAQ verbal - - } cvalues; - - /* Private: Sensor settings */ - bsec_virtual_sensor_t sensorList[13] = { - BSEC_OUTPUT_IAQ, // Index for Air Quality estimate [0-500] Index for Air Quality (IAQ) gives an indication of the relative change in ambient TVOCs detected by BME680. - BSEC_OUTPUT_STATIC_IAQ, // Unscaled Index for Air Quality estimate - BSEC_OUTPUT_CO2_EQUIVALENT, // CO2 equivalent estimate [ppm] - BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, // Breath VOC concentration estimate [ppm] - BSEC_OUTPUT_RAW_TEMPERATURE, // Temperature sensor signal [degrees Celsius] Temperature directly measured by BME680 in degree Celsius. This value is cross-influenced by the sensor heating and device specific heating. - BSEC_OUTPUT_RAW_PRESSURE, // Pressure sensor signal [Pa] Pressure directly measured by the BME680 in Pa. - BSEC_OUTPUT_RAW_HUMIDITY, // Relative humidity sensor signal [%] Relative humidity directly measured by the BME680 in %. This value is cross-influenced by the sensor heating and device specific heating. - BSEC_OUTPUT_RAW_GAS, // Gas sensor signal [Ohm] Gas resistance measured directly by the BME680 in Ohm.The resistance value changes due to varying VOC concentrations (the higher the concentration of reducing VOCs, the lower the resistance and vice versa). - BSEC_OUTPUT_STABILIZATION_STATUS, // Gas sensor stabilization status [boolean] Indicates initial stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1). - BSEC_OUTPUT_RUN_IN_STATUS, // Gas sensor run-in status [boolean] Indicates power-on stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1) - BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, // Sensor heat compensated temperature [degrees Celsius] Temperature measured by BME680 which is compensated for the influence of sensor (heater) in degree Celsius. The self heating introduced by the heater is depending on the sensor operation mode and the sensor supply voltage. - BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, // Sensor heat compensated humidity [%] Relative measured by BME680 which is compensated for the influence of sensor (heater) in %. It converts the ::BSEC_INPUT_HUMIDITY from temperature ::BSEC_INPUT_TEMPERATURE to temperature ::BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE. - BSEC_OUTPUT_GAS_PERCENTAGE // Percentage of min and max filtered gas value [%] - }; - - /*** V A R I A B L E s & C O N S T A N T s ***/ - /* Public: strings to reduce flash memory usage (used more than twice) */ - static const char _enabled[]; - static const char _hadtopic[]; - - /* Public: Settings Strings*/ - static const char _nameI2CAdr[]; - static const char _nameInterval[]; - static const char _nameMaxAge[]; - static const char _namePubAc[]; - static const char _namePubSenState[]; - static const char _namePubAfterCalib[]; - static const char _namePublishChange[]; - static const char _nameTempScale[]; - static const char _nameTempOffset[]; - static const char _nameHADisc[]; - static const char _nameDelCalib[]; - - /* Public: Sensor names / Sensor short names */ - static const char _nameTemp[]; - static const char _nameHum[]; - static const char _namePress[]; - static const char _nameGasRes[]; - static const char _nameAHum[]; - static const char _nameDrewP[]; - static const char _nameIaq[]; - static const char _nameIaqAc[]; - static const char _nameIaqVerb[]; - static const char _nameStaticIaq[]; - static const char _nameStaticIaqVerb[]; - static const char _nameStaticIaqAc[]; - static const char _nameCo2[]; - static const char _nameCo2Ac[]; - static const char _nameVoc[]; - static const char _nameVocAc[]; - static const char _nameComGasAc[]; - static const char _nameGasPer[]; - static const char _nameGasPerAc[]; - static const char _namePauseOnActWL[]; - - static const char _nameStabStatus[]; - static const char _nameRunInStatus[]; - - /* Public: Sensor Units */ - static const char _unitTemp[]; - static const char _unitHum[]; - static const char _unitPress[]; - static const char _unitGasres[]; - static const char _unitAHum[]; - static const char _unitDrewp[]; - static const char _unitIaq[]; - static const char _unitStaticIaq[]; - static const char _unitCo2[]; - static const char _unitVoc[]; - static const char _unitGasPer[]; - static const char _unitNone[]; - - static const char _unitCelsius[]; - static const char _unitFahrenheit[]; -}; // UsermodBME68X class definition End - -/*** Setting C O N S T A N T S ***/ -/* Private: Settings Strings*/ -const char UsermodBME68X::_enabled[] PROGMEM = "Enabled"; -const char UsermodBME68X::_hadtopic[] PROGMEM = "homeassistant/sensor/"; - -const char UsermodBME68X::_nameI2CAdr[] PROGMEM = "i2C Address"; -const char UsermodBME68X::_nameInterval[] PROGMEM = "Interval"; -const char UsermodBME68X::_nameMaxAge[] PROGMEM = "Max Age"; -const char UsermodBME68X::_namePublishChange[] PROGMEM = "Pub changes only"; -const char UsermodBME68X::_namePubAc[] PROGMEM = "Pub Accuracy"; -const char UsermodBME68X::_namePubSenState[] PROGMEM = "Pub Calib State"; -const char UsermodBME68X::_namePubAfterCalib[] PROGMEM = "Pub After Calib"; -const char UsermodBME68X::_nameTempScale[] PROGMEM = "Temp Scale"; -const char UsermodBME68X::_nameTempOffset[] PROGMEM = "Temp Offset"; -const char UsermodBME68X::_nameHADisc[] PROGMEM = "HA Discovery"; -const char UsermodBME68X::_nameDelCalib[] PROGMEM = "Del Calibration Hist"; -const char UsermodBME68X::_namePauseOnActWL[] PROGMEM = "Pause while WLED active"; - -/* Private: Sensor names / Sensor short name */ -const char UsermodBME68X::_nameTemp[] PROGMEM = "Temperature"; -const char UsermodBME68X::_nameHum[] PROGMEM = "Humidity"; -const char UsermodBME68X::_namePress[] PROGMEM = "Pressure"; -const char UsermodBME68X::_nameGasRes[] PROGMEM = "Gas-Resistance"; -const char UsermodBME68X::_nameAHum[] PROGMEM = "Absolute-Humidity"; -const char UsermodBME68X::_nameDrewP[] PROGMEM = "Drew-Point"; -const char UsermodBME68X::_nameIaq[] PROGMEM = "IAQ"; -const char UsermodBME68X::_nameIaqVerb[] PROGMEM = "IAQ-Verbal"; -const char UsermodBME68X::_nameStaticIaq[] PROGMEM = "Static-IAQ"; -const char UsermodBME68X::_nameStaticIaqVerb[] PROGMEM = "Static-IAQ-Verbal"; -const char UsermodBME68X::_nameCo2[] PROGMEM = "CO2"; -const char UsermodBME68X::_nameVoc[] PROGMEM = "VOC"; -const char UsermodBME68X::_nameGasPer[] PROGMEM = "Gas-Percentage"; -const char UsermodBME68X::_nameIaqAc[] PROGMEM = "IAQ-Accuracy"; -const char UsermodBME68X::_nameStaticIaqAc[] PROGMEM = "Static-IAQ-Accuracy"; -const char UsermodBME68X::_nameCo2Ac[] PROGMEM = "CO2-Accuracy"; -const char UsermodBME68X::_nameVocAc[] PROGMEM = "VOC-Accuracy"; -const char UsermodBME68X::_nameGasPerAc[] PROGMEM = "Gas-Percentage-Accuracy"; -const char UsermodBME68X::_nameStabStatus[] PROGMEM = "Stab-Status"; -const char UsermodBME68X::_nameRunInStatus[] PROGMEM = "Run-In-Status"; - -/* Private Units */ -const char UsermodBME68X::_unitTemp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit -const char UsermodBME68X::_unitHum[] PROGMEM = "%"; -const char UsermodBME68X::_unitPress[] PROGMEM = "hPa"; -const char UsermodBME68X::_unitGasres[] PROGMEM = "kΩ"; -const char UsermodBME68X::_unitAHum[] PROGMEM = "g/m³"; -const char UsermodBME68X::_unitDrewp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit -const char UsermodBME68X::_unitIaq[] PROGMEM = " "; // No unit -const char UsermodBME68X::_unitStaticIaq[] PROGMEM = " "; // No unit -const char UsermodBME68X::_unitCo2[] PROGMEM = "ppm"; -const char UsermodBME68X::_unitVoc[] PROGMEM = "ppm"; -const char UsermodBME68X::_unitGasPer[] PROGMEM = "%"; -const char UsermodBME68X::_unitNone[] PROGMEM = ""; - -const char UsermodBME68X::_unitCelsius[] PROGMEM = "°C"; // Symbol for Celsius -const char UsermodBME68X::_unitFahrenheit[] PROGMEM = "°F"; // Symbol for Fahrenheit - -/* Load Sensor Settings */ -const uint8_t UsermodBME68X::bsec_config_iaq[] = { - #include "config/generic_33v_3s_28d/bsec_iaq.txt" // Allow 28 days for calibration because the WLED module normally stays in the same place anyway -}; - - -/************************************************************************************************************/ -/********************************************* M A I N C O D E *********************************************/ -/************************************************************************************************************/ - -/** - * @brief Called by WLED: Setup of the usermod - */ -void UsermodBME68X::setup() { - DEBUG_PRINTLN(F(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Initialize" ESC_STYLE_RESET)); - - /* Check, if i2c is activated */ - if (i2c_scl < 0 || i2c_sda < 0) { - settings.enabled = false; // Disable usermod once i2c is not running - DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "I2C is not activated. Please activate I2C first." FAIL)); - return; - } - - flags.InitSuccessful = true; // Will be set to false on need - - /* Set data structure pointers */ - ValuesPtr = &valuesA; - PrevValuesPtr = &valuesB; - - /* Init Library*/ - iaqSensor.begin(settings.I2cadress, Wire); // BME68X_I2C_ADDR_LOW - stringbuff = "BSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix); - DEBUG_PRINT(F(UMOD_NAME)); - DEBUG_PRINTLN(F(stringbuff.c_str())); - - /* Init Sensor*/ - iaqSensor.setConfig(bsec_config_iaq); - iaqSensor.updateSubscription(sensorList, 13, BSEC_SAMPLE_RATE_LP); - iaqSensor.setTPH(BME68X_OS_2X, BME68X_OS_16X, BME68X_OS_1X); // Set the temperature, Pressure and Humidity over-sampling - iaqSensor.setTemperatureOffset(settings.tempOffset); // set the temperature offset in degree Celsius - loadState(); // Load the old calibration data - checkIaqSensorStatus(); // Check the sensor status - // HomeAssistantDiscovery(); - DEBUG_PRINTLN(F(INFO_COLUMN DONE)); -} - -/** - * @brief Called by WLED: Main loop called by WLED - * - */ -void UsermodBME68X::loop() { - if (!settings.enabled || strip.isUpdating() || !flags.InitSuccessful) return; // Leave if not enabled or string is updating or init failed - - if (settings.pauseOnActiveWled && strip.getBrightness()) return; // Workarround Known Issue: handing led update - Leave once pause on activ wled is active and wled is active - - timer.actual = millis(); // Timer to fetch new temperature, humidity and pressure data at intervals - - if (timer.actual - timer.lastRun >= settings.Interval * 1000) { - timer.lastRun = timer.actual; - - /* Get the sonsor measurments and publish them */ - if (iaqSensor.run()) { // iaqSensor.run() - getValues(); // Get the new values - - if (ValuesPtr->temperature != PrevValuesPtr->temperature || !settings.PublischChange) { // NOTE - negative dig means inactive - MQTT_publish(_nameTemp, ValuesPtr->temperature, settings.decimals.temperature); - } - if (ValuesPtr->humidity != PrevValuesPtr->humidity || !settings.PublischChange) { - MQTT_publish(_nameHum, ValuesPtr->humidity, settings.decimals.humidity); - } - if (ValuesPtr->pressure != PrevValuesPtr->pressure || !settings.PublischChange) { - MQTT_publish(_namePress, ValuesPtr->pressure, settings.decimals.humidity); - } - if (ValuesPtr->gasResistance != PrevValuesPtr->gasResistance || !settings.PublischChange) { - MQTT_publish(_nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance); - } - if (ValuesPtr->absHumidity != PrevValuesPtr->absHumidity || !settings.PublischChange) { - MQTT_publish(_nameAHum, PrevValuesPtr->absHumidity, settings.decimals.absHumidity); - } - if (ValuesPtr->drewPoint != PrevValuesPtr->drewPoint || !settings.PublischChange) { - MQTT_publish(_nameDrewP, PrevValuesPtr->drewPoint, settings.decimals.drewPoint); - } - if (ValuesPtr->iaq != PrevValuesPtr->iaq || !settings.PublischChange) { - MQTT_publish(_nameIaq, ValuesPtr->iaq, settings.decimals.iaq); - if (settings.pubAcc) MQTT_publish(_nameIaqAc, ValuesPtr->iaqAccuracy, 0); - if (settings.decimals.iaq>-1) { - if (settings.PublishIAQVerbal) { - if (ValuesPtr->iaq <= 50) cvalues.iaqVerbal = F("Excellent"); - else if (ValuesPtr->iaq <= 100) cvalues.iaqVerbal = F("Good"); - else if (ValuesPtr->iaq <= 150) cvalues.iaqVerbal = F("Lightly polluted"); - else if (ValuesPtr->iaq <= 200) cvalues.iaqVerbal = F("Moderately polluted"); - else if (ValuesPtr->iaq <= 250) cvalues.iaqVerbal = F("Heavily polluted"); - else if (ValuesPtr->iaq <= 350) cvalues.iaqVerbal = F("Severely polluted"); - else cvalues.iaqVerbal = F("Extremely polluted"); - snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameIaqVerb); - if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.iaqVerbal.c_str()); - } - } - } - if (ValuesPtr->staticIaq != PrevValuesPtr->staticIaq || !settings.PublischChange) { - MQTT_publish(_nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq); - if (settings.pubAcc) MQTT_publish(_nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0); - if (settings.decimals.staticIaq>-1) { - if (settings.PublishIAQVerbal) { - if (ValuesPtr->staticIaq <= 50) cvalues.staticIaqVerbal = F("Excellent"); - else if (ValuesPtr->staticIaq <= 100) cvalues.staticIaqVerbal = F("Good"); - else if (ValuesPtr->staticIaq <= 150) cvalues.staticIaqVerbal = F("Lightly polluted"); - else if (ValuesPtr->staticIaq <= 200) cvalues.staticIaqVerbal = F("Moderately polluted"); - else if (ValuesPtr->staticIaq <= 250) cvalues.staticIaqVerbal = F("Heavily polluted"); - else if (ValuesPtr->staticIaq <= 350) cvalues.staticIaqVerbal = F("Severely polluted"); - else cvalues.staticIaqVerbal = F("Extremely polluted"); - snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameStaticIaqVerb); - if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.staticIaqVerbal.c_str()); - } - } - } - if (ValuesPtr->co2 != PrevValuesPtr->co2 || !settings.PublischChange) { - MQTT_publish(_nameCo2, ValuesPtr->co2, settings.decimals.co2); - if (settings.pubAcc) MQTT_publish(_nameCo2Ac, ValuesPtr->co2Accuracy, 0); - } - if (ValuesPtr->Voc != PrevValuesPtr->Voc || !settings.PublischChange) { - MQTT_publish(_nameVoc, ValuesPtr->Voc, settings.decimals.Voc); - if (settings.pubAcc) MQTT_publish(_nameVocAc, ValuesPtr->VocAccuracy, 0); - } - if (ValuesPtr->gasPerc != PrevValuesPtr->gasPerc || !settings.PublischChange) { - MQTT_publish(_nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc); - if (settings.pubAcc) MQTT_publish(_nameGasPerAc, ValuesPtr->gasPercAccuracy, 0); - } - - /**** Publish Sensor State Entrys *****/ - if ((ValuesPtr->stabStatus != PrevValuesPtr->stabStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameStabStatus, ValuesPtr->stabStatus, 0); - if ((ValuesPtr->runInStatus != PrevValuesPtr->runInStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameRunInStatus, ValuesPtr->runInStatus, 0); - - /* Check accuracies - if accurasy level 3 is reached -> save calibration data */ - if ((ValuesPtr->iaqAccuracy != PrevValuesPtr->iaqAccuracy) && ValuesPtr->iaqAccuracy == 3) flags.SaveState = true; // Save after calibration / recalibration - if ((ValuesPtr->staticIaqAccuracy != PrevValuesPtr->staticIaqAccuracy) && ValuesPtr->staticIaqAccuracy == 3) flags.SaveState = true; - if ((ValuesPtr->co2Accuracy != PrevValuesPtr->co2Accuracy) && ValuesPtr->co2Accuracy == 3) flags.SaveState = true; - if ((ValuesPtr->VocAccuracy != PrevValuesPtr->VocAccuracy) && ValuesPtr->VocAccuracy == 3) flags.SaveState = true; - if ((ValuesPtr->gasPercAccuracy != PrevValuesPtr->gasPercAccuracy) && ValuesPtr->gasPercAccuracy == 3) flags.SaveState = true; - - if (flags.SaveState) saveState(); // Save if the save state flag is set - } - } -} - -/** - * @brief Retrieves the sensor data and truncates it to the requested decimal places - * - */ -void UsermodBME68X::getValues() { - /* Swap the point to the data structures */ - swap = PrevValuesPtr; - PrevValuesPtr = ValuesPtr; - ValuesPtr = swap; - - /* Float Values */ - ValuesPtr->temperature = roundf(iaqSensor.temperature * powf(10, settings.decimals.temperature)) / powf(10, settings.decimals.temperature); - ValuesPtr->humidity = roundf(iaqSensor.humidity * powf(10, settings.decimals.humidity)) / powf(10, settings.decimals.humidity); - ValuesPtr->pressure = roundf(iaqSensor.pressure * powf(10, settings.decimals.pressure)) / powf(10, settings.decimals.pressure) /100; // Pa 2 hPa - ValuesPtr->gasResistance = roundf(iaqSensor.gasResistance * powf(10, settings.decimals.gasResistance)) /powf(10, settings.decimals.gasResistance) /1000; // Ohm 2 KOhm - ValuesPtr->iaq = roundf(iaqSensor.iaq * powf(10, settings.decimals.iaq)) / powf(10, settings.decimals.iaq); - ValuesPtr->staticIaq = roundf(iaqSensor.staticIaq * powf(10, settings.decimals.staticIaq)) / powf(10, settings.decimals.staticIaq); - ValuesPtr->co2 = roundf(iaqSensor.co2Equivalent * powf(10, settings.decimals.co2)) / powf(10, settings.decimals.co2); - ValuesPtr->Voc = roundf(iaqSensor.breathVocEquivalent * powf(10, settings.decimals.Voc)) / powf(10, settings.decimals.Voc); - ValuesPtr->gasPerc = roundf(iaqSensor.gasPercentage * powf(10, settings.decimals.gasPerc)) / powf(10, settings.decimals.gasPerc); - - /* Calculate Absolute Humidity [g/m³] */ - if (settings.decimals.absHumidity>-1) { - const float mw = 18.01534; // molar mass of water g/mol - const float r = 8.31447215; // Universal gas constant J/mol/K - ValuesPtr->absHumidity = (6.112 * powf(2.718281828, (17.67 * ValuesPtr->temperature) / (ValuesPtr->temperature + 243.5)) * ValuesPtr->humidity * mw) / ((273.15 + ValuesPtr->temperature) * r); // in ppm - } - /* Calculate Drew Point (C°) */ - if (settings.decimals.drewPoint>-1) { - ValuesPtr->drewPoint = (243.5 * (log( ValuesPtr->humidity / 100) + ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature))) / (17.67 - log(ValuesPtr->humidity / 100) - ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature)))); - } - - /* Convert to Fahrenheit when selected */ - if (settings.tempScale) { // settings.tempScale = 0 => Celsius, = 1 => Fahrenheit - ValuesPtr->temperature = ValuesPtr->temperature * 1.8 + 32; // Value stored in Fahrenheit - ValuesPtr->drewPoint = ValuesPtr->drewPoint * 1.8 + 32; - } - - /* Integer Values */ - ValuesPtr->iaqAccuracy = iaqSensor.iaqAccuracy; - ValuesPtr->staticIaqAccuracy = iaqSensor.staticIaqAccuracy; - ValuesPtr->co2Accuracy = iaqSensor.co2Accuracy; - ValuesPtr->VocAccuracy = iaqSensor.breathVocAccuracy; - ValuesPtr->gasPercAccuracy = iaqSensor.gasPercentageAccuracy; - ValuesPtr->stabStatus = iaqSensor.stabStatus; - ValuesPtr->runInStatus = iaqSensor.runInStatus; -} - - -/** - * @brief Sends the current sensor data via MQTT - * @param topic Suptopic of the sensor as const char - * @param value Current sensor value as float - */ -void UsermodBME68X::MQTT_publish(const char* topic, const float& value, const int8_t& dig) { - if (dig<0) return; - if (WLED_MQTT_CONNECTED) { - snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); - mqtt->publish(charbuffer, 0, false, String(value, dig).c_str()); - } -} - -/** - * @brief Called by WLED: Initialize the MQTT parts when the connection to the MQTT server is established. - * @param bool Session Present - */ -void UsermodBME68X::onMqttConnect(bool sessionPresent) { - DEBUG_PRINTLN(UMOD_DEBUG_NAME "OnMQTTConnect event fired"); - HomeAssistantDiscovery(); - - if (!flags.MqttInitialized) { - flags.MqttInitialized=true; - DEBUG_PRINTLN(UMOD_DEBUG_NAME "MQTT first connect"); - } -} - - -/** - * @brief MQTT initialization to generate the mqtt topic strings. This initialization also creates the HomeAssistat device configuration (HA Discovery), which home assinstant automatically evaluates to create a device. - */ -void UsermodBME68X::HomeAssistantDiscovery() { - if (!settings.HomeAssistantDiscovery || !flags.InitSuccessful || !settings.enabled) return; // Leave once HomeAssistant Discovery is inactive - - DEBUG_PRINTLN(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Creating HomeAssistant Discovery Mqtt-Entrys" ESC_STYLE_RESET); - - /* Sensor Values */ - MQTT_PublishHASensor(_nameTemp, "TEMPERATURE", tempScale.c_str(), settings.decimals.temperature ); // Temperature - MQTT_PublishHASensor(_namePress, "ATMOSPHERIC_PRESSURE", _unitPress, settings.decimals.pressure ); // Pressure - MQTT_PublishHASensor(_nameHum, "HUMIDITY", _unitHum, settings.decimals.humidity ); // Humidity - MQTT_PublishHASensor(_nameGasRes, "GAS", _unitGasres, settings.decimals.gasResistance ); // There is no device class for resistance in HA yet: https://developers.home-assistant.io/docs/core/entity/sensor/ - MQTT_PublishHASensor(_nameAHum, "HUMIDITY", _unitAHum, settings.decimals.absHumidity ); // Absolute Humidity - MQTT_PublishHASensor(_nameDrewP, "TEMPERATURE", tempScale.c_str(), settings.decimals.drewPoint ); // Drew Point - MQTT_PublishHASensor(_nameIaq, "AQI", _unitIaq, settings.decimals.iaq ); // IAQ - MQTT_PublishHASensor(_nameIaqVerb, "", _unitNone, settings.PublishIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor) - MQTT_PublishHASensor(_nameStaticIaq, "AQI", _unitNone, settings.decimals.staticIaq ); // Static IAQ - MQTT_PublishHASensor(_nameStaticIaqVerb, "", _unitNone, settings.PublishStaticIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor - MQTT_PublishHASensor(_nameCo2, "CO2", _unitCo2, settings.decimals.co2 ); // CO2 - MQTT_PublishHASensor(_nameVoc, "VOLATILE_ORGANIC_COMPOUNDS", _unitVoc, settings.decimals.Voc ); // VOC - MQTT_PublishHASensor(_nameGasPer, "AQI", _unitGasPer, settings.decimals.gasPerc ); // Gas % - - /* Accuracys - switched off once publishAccuracy=0 or the main value is switched of by digs set to a negative number */ - MQTT_PublishHASensor(_nameIaqAc, "AQI", _unitNone, settings.pubAcc - 1 + settings.decimals.iaq * settings.pubAcc, 1); // Option 1: Diagnostics Sektion - MQTT_PublishHASensor(_nameStaticIaqAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.staticIaq * settings.pubAcc, 1); - MQTT_PublishHASensor(_nameCo2Ac, "", _unitNone, settings.pubAcc - 1 + settings.decimals.co2 * settings.pubAcc, 1); - MQTT_PublishHASensor(_nameVocAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.Voc * settings.pubAcc, 1); - MQTT_PublishHASensor(_nameGasPerAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.gasPerc * settings.pubAcc, 1); - - MQTT_PublishHASensor(_nameStabStatus, "", _unitNone, settings.publishSensorState - 1, 1); - MQTT_PublishHASensor(_nameRunInStatus, "", _unitNone, settings.publishSensorState - 1, 1); - - DEBUG_PRINTLN(UMOD_DEBUG_NAME DONE); -} - -/** - * @brief These MQTT entries are responsible for the Home Assistant Discovery of the sensors. HA is shown here where to look for the sensor data. This entry therefore only needs to be sent once. - * Important note: In order to find everything that is sent from this device to Home Assistant via MQTT under the same device name, the "device/identifiers" entry must be the same. - * I use the MQTT device name here. If other user mods also use the HA Discovery, it is recommended to set the identifier the same. Otherwise you would have several devices, - * even though it is one device. I therefore only use the MQTT client name set in WLED here. - * @param name Name of the sensor - * @param topic Topic of the live sensor data - * @param unitOfMeasurement Unit of the measurment - * @param digs Number of decimal places - * @param option Set to true if the sensor is part of diagnostics (dafault 0) - */ -void UsermodBME68X::MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option) { - DEBUG_PRINT(UMOD_DEBUG_NAME "\t" + name); - - snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, name.c_str()); // Current values will be posted here - String basetopic = String(_hadtopic) + mqttClientID + F("/") + name + F("/config"); // This is the place where Home Assinstant Discovery will check for new devices - - if (digs < 0) { // if digs are set to -1 -> entry deactivated - /* Delete MQTT Entry */ - if (WLED_MQTT_CONNECTED) { - mqtt->publish(basetopic.c_str(), 0, true, ""); // Send emty entry to delete - DEBUG_PRINTLN(INFO_COLUMN "deleted"); - } - } else { - /* Create all the necessary HAD MQTT entrys - see: https://www.home-assistant.io/integrations/sensor.mqtt/#configuration-variables */ - DynamicJsonDocument jdoc(700); // json document - // See: https://www.home-assistant.io/integrations/mqtt/ - JsonObject avail = jdoc.createNestedObject(F("avty")); // 'avty': 'availability' - avail[F("topic")] = mqttDeviceTopic + String("/status"); // An MQTT topic subscribed to receive availability (online/offline) updates. - avail[F("payload_available")] = "online"; - avail[F("payload_not_available")] = "offline"; - JsonObject device = jdoc.createNestedObject(F("device")); // Information about the device this sensor is a part of to tie it into the device registry. Only works when unique_id is set. At least one of identifiers or connections must be present to identify the device. - device[F("name")] = serverDescription; - device[F("identifiers")] = String(mqttClientID); - device[F("manufacturer")] = F("WLED"); - device[F("model")] = UMOD_DEVICE; - device[F("sw_version")] = versionString; - device[F("hw_version")] = F(HARDWARE_VERSION); - - if (deviceClass != "") jdoc[F("device_class")] = deviceClass; // The type/class of the sensor to set the icon in the frontend. The device_class can be null - if (option == 1) jdoc[F("entity_category")] = "diagnostic"; // Option 1: The category of the entity | When set, the entity category must be diagnostic for sensors. - if (option == 2) jdoc[F("mode")] = "text"; // Option 2: Set text mode | - jdoc[F("expire_after")] = 1800; // If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. Default the sensors state never expires. - jdoc[F("name")] = name; // The name of the MQTT sensor. Without server/module/device name. The device name will be added by HomeAssinstant anyhow - if (unitOfMeasurement != "") jdoc[F("state_class")] = "measurement"; // NOTE: This entry is missing in some other usermods. But it is very important. Because only with this entry, you can use statistics (such as statistical graphs). - jdoc[F("state_topic")] = charbuffer; // The MQTT topic subscribed to receive sensor values. If device_class, state_class, unit_of_measurement or suggested_display_precision is set, and a numeric value is expected, an empty value '' will be ignored and will not update the state, a 'null' value will set the sensor to an unknown state. The device_class can be null. - jdoc[F("unique_id")] = String(mqttClientID) + "-" + name; // An ID that uniquely identifies this sensor. If two sensors have the same unique ID, Home Assistant will raise an exception. - if (unitOfMeasurement != "") jdoc[F("unit_of_measurement")] = unitOfMeasurement; // Defines the units of measurement of the sensor, if any. The unit_of_measurement can be null. - - DEBUG_PRINTF(" (%d bytes)", jdoc.memoryUsage()); - - stringbuff = ""; // clear string buffer - serializeJson(jdoc, stringbuff); // JSON to String - - if (WLED_MQTT_CONNECTED) { // Check if MQTT Connected, otherwise it will crash the 8266 - mqtt->publish(basetopic.c_str(), 0, true, stringbuff.c_str()); // Publish the HA discovery sensor entry - DEBUG_PRINTLN(INFO_COLUMN "published"); - } - } -} - -/** - * @brief Called by WLED: Publish Sensor Information to Info Page - * @param JsonObject Pointer - */ -void UsermodBME68X::addToJsonInfo(JsonObject& root) { - //DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Add to info event")); - JsonObject user = root[F("u")]; - - if (user.isNull()) - user = root.createNestedObject(F("u")); - - if (!flags.InitSuccessful) { - // Init was not seccessful - let the user know - JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); - temperature_json.add(F("not found")); - JsonArray humidity_json = user.createNestedArray(F("BMW68x Reason")); - humidity_json.add(InfoPageStatusLine); - } - else if (!settings.enabled) { - JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); - temperature_json.add(F("disabled")); - } - else { - InfoHelper(user, _nameTemp, ValuesPtr->temperature, settings.decimals.temperature, tempScale.c_str()); - InfoHelper(user, _nameHum, ValuesPtr->humidity, settings.decimals.humidity, _unitHum); - InfoHelper(user, _namePress, ValuesPtr->pressure, settings.decimals.pressure, _unitPress); - InfoHelper(user, _nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance, _unitGasres); - InfoHelper(user, _nameAHum, ValuesPtr->absHumidity, settings.decimals.absHumidity, _unitAHum); - InfoHelper(user, _nameDrewP, ValuesPtr->drewPoint, settings.decimals.drewPoint, tempScale.c_str()); - InfoHelper(user, _nameIaq, ValuesPtr->iaq, settings.decimals.iaq, _unitIaq); - InfoHelper(user, _nameIaqVerb, cvalues.iaqVerbal, settings.PublishIAQVerbal); - InfoHelper(user, _nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq, _unitStaticIaq); - InfoHelper(user, _nameStaticIaqVerb,cvalues.staticIaqVerbal, settings.PublishStaticIAQVerbal); - InfoHelper(user, _nameCo2, ValuesPtr->co2, settings.decimals.co2, _unitCo2); - InfoHelper(user, _nameVoc, ValuesPtr->Voc, settings.decimals.Voc, _unitVoc); - InfoHelper(user, _nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc, _unitGasPer); - - if (settings.pubAcc) { - if (settings.decimals.iaq >= 0) InfoHelper(user, _nameIaqAc, ValuesPtr->iaqAccuracy, 0, " "); - if (settings.decimals.staticIaq >= 0) InfoHelper(user, _nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0, " "); - if (settings.decimals.co2 >= 0) InfoHelper(user, _nameCo2Ac, ValuesPtr->co2Accuracy, 0, " "); - if (settings.decimals.Voc >= 0) InfoHelper(user, _nameVocAc, ValuesPtr->VocAccuracy, 0, " "); - if (settings.decimals.gasPerc >= 0) InfoHelper(user, _nameGasPerAc, ValuesPtr->gasPercAccuracy, 0, " "); - } - - if (settings.publishSensorState) { - InfoHelper(user, _nameStabStatus, ValuesPtr->stabStatus, 0, " "); - InfoHelper(user, _nameRunInStatus, ValuesPtr->runInStatus, 0, " "); - } - } -} - -/** - * @brief Info Page helper function - * @param root JSON object - * @param name Name of the sensor as char - * @param sensorvalue Value of the sensor as float - * @param decimals Decimal places of the value - * @param unit Unit of the sensor - */ -void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit) { - if (decimals > -1) { - JsonArray sub_json = root.createNestedArray(name); - sub_json.add(roundf(sensorvalue * powf(10, decimals)) / powf(10, decimals)); - sub_json.add(unit); - } -} - -/** - * @brief Info Page helper function (overload) - * @param root JSON object - * @param name Name of the sensor - * @param sensorvalue Value of the sensor as string - * @param status Status of the value (active/inactive) - */ -void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status) { - if (status) { - JsonArray sub_json = root.createNestedArray(name); - sub_json.add(sensorvalue); - } -} - -/** - * @brief Called by WLED: Adds the usermodul neends on the config page for user modules - * @param JsonObject Pointer - * - * @see Usermod::addToConfig() - * @see UsermodManager::addToConfig() - */ -void UsermodBME68X::addToConfig(JsonObject& root) { - DEBUG_PRINT(F(UMOD_DEBUG_NAME "Creating configuration pages content: ")); - - JsonObject top = root.createNestedObject(FPSTR(UMOD_NAME)); - /* general settings */ - top[FPSTR(_enabled)] = settings.enabled; - top[FPSTR(_nameI2CAdr)] = settings.I2cadress; - top[FPSTR(_nameInterval)] = settings.Interval; - top[FPSTR(_namePublishChange)] = settings.PublischChange; - top[FPSTR(_namePubAc)] = settings.pubAcc; - top[FPSTR(_namePubSenState)] = settings.publishSensorState; - top[FPSTR(_nameTempScale)] = settings.tempScale; - top[FPSTR(_nameTempOffset)] = settings.tempOffset; - top[FPSTR(_nameHADisc)] = settings.HomeAssistantDiscovery; - top[FPSTR(_namePauseOnActWL)] = settings.pauseOnActiveWled; - top[FPSTR(_nameDelCalib)] = flags.DeleteCaibration; - - /* Digs */ - JsonObject sensors_json = top.createNestedObject("Sensors"); - sensors_json[FPSTR(_nameTemp)] = settings.decimals.temperature; - sensors_json[FPSTR(_nameHum)] = settings.decimals.humidity; - sensors_json[FPSTR(_namePress)] = settings.decimals.pressure; - sensors_json[FPSTR(_nameGasRes)] = settings.decimals.gasResistance; - sensors_json[FPSTR(_nameAHum)] = settings.decimals.absHumidity; - sensors_json[FPSTR(_nameDrewP)] = settings.decimals.drewPoint; - sensors_json[FPSTR(_nameIaq)] = settings.decimals.iaq; - sensors_json[FPSTR(_nameIaqVerb)] = settings.PublishIAQVerbal; - sensors_json[FPSTR(_nameStaticIaq)] = settings.decimals.staticIaq; - sensors_json[FPSTR(_nameStaticIaqVerb)] = settings.PublishStaticIAQVerbal; - sensors_json[FPSTR(_nameCo2)] = settings.decimals.co2; - sensors_json[FPSTR(_nameVoc)] = settings.decimals.Voc; - sensors_json[FPSTR(_nameGasPer)] = settings.decimals.gasPerc; - - DEBUG_PRINTLN(F(OK)); -} - -/** - * @brief Called by WLED: Add dropdown and additional infos / structure - * @see Usermod::appendConfigData() - * @see UsermodManager::appendConfigData() - */ -void UsermodBME68X::appendConfigData() { - // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'read interval [seconds]');"), UMOD_NAME, _nameInterval); oappend(charbuffer); - // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'only if value changes');"), UMOD_NAME, _namePublishChange); oappend(charbuffer); - // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'maximum age of a message in seconds');"), UMOD_NAME, _nameMaxAge); oappend(charbuffer); - // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'Gas related values are only published after the gas sensor has been calibrated');"), UMOD_NAME, _namePubAfterCalib); oappend(charbuffer); - // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'*) Set to minus to deactivate (all sensors)');"), UMOD_NAME, _nameTemp); oappend(charbuffer); - - /* Dropdown for Celsius/Fahrenheit*/ - oappend(F("dd=addDropdown('")); - oappend(UMOD_NAME); - oappend(F("','")); - oappend(_nameTempScale); - oappend(F("');")); - oappend(F("addOption(dd,'Celsius',0);")); - oappend(F("addOption(dd,'Fahrenheit',1);")); - - /* i²C Address*/ - oappend(F("dd=addDropdown('")); - oappend(UMOD_NAME); - oappend(F("','")); - oappend(_nameI2CAdr); - oappend(F("');")); - oappend(F("addOption(dd,'0x76',0x76);")); - oappend(F("addOption(dd,'0x77',0x77);")); -} - -/** - * @brief Called by WLED: Read Usermod Config Settings 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) - * 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. - * NOTE: Here are the default settings of the user module - * @param JsonObject Pointer - * @return bool - * @see Usermod::readFromConfig() - * @see UsermodManager::readFromConfig() - */ -bool UsermodBME68X::readFromConfig(JsonObject& root) { - DEBUG_PRINT(F(UMOD_DEBUG_NAME "Reading configuration: ")); - - JsonObject top = root[FPSTR(UMOD_NAME)]; - bool configComplete = !top.isNull(); - - /* general settings */ /* DEFAULTS */ - configComplete &= getJsonValue(top[FPSTR(_enabled)], settings.enabled, 1 ); // Usermod enabled per default - configComplete &= getJsonValue(top[FPSTR(_nameI2CAdr)], settings.I2cadress, 0x77 ); // Defalut IC2 adress set to 0x77 (some modules are set to 0x76) - configComplete &= getJsonValue(top[FPSTR(_nameInterval)], settings.Interval, 1 ); // Executed every second - configComplete &= getJsonValue(top[FPSTR(_namePublishChange)], settings.PublischChange, false ); // Publish changed values only - configComplete &= getJsonValue(top[FPSTR(_nameTempScale)], settings.tempScale, 0 ); // Temp sale set to Celsius (1=Fahrenheit) - configComplete &= getJsonValue(top[FPSTR(_nameTempOffset)], settings.tempOffset, 0 ); // Temp offset is set to 0 (Celsius) - configComplete &= getJsonValue(top[FPSTR(_namePubSenState)], settings.publishSensorState, 1 ); // Publish the sensor states - configComplete &= getJsonValue(top[FPSTR(_namePubAc)], settings.pubAcc, 1 ); // Publish accuracy values - configComplete &= getJsonValue(top[FPSTR(_nameHADisc)], settings.HomeAssistantDiscovery, true ); // Activate HomeAssistant Discovery (this Module will be shown as MQTT device in HA) - configComplete &= getJsonValue(top[FPSTR(_namePauseOnActWL)], settings.pauseOnActiveWled, false ); // Pause on active WLED not activated per default - configComplete &= getJsonValue(top[FPSTR(_nameDelCalib)], flags.DeleteCaibration, false ); // IF checked the calibration file will be delete when the save button is pressed - - /* Decimal places */ /* no of digs / -1 means deactivated */ - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameTemp)], settings.decimals.temperature, 1 ); // One decimal places - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameHum)], settings.decimals.humidity, 1 ); - configComplete &= getJsonValue(top["Sensors"][FPSTR(_namePress)], settings.decimals.pressure, 0 ); // Zero decimal places - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasRes)], settings.decimals.gasResistance, -1 ); // deavtivated - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameDrewP)], settings.decimals.drewPoint, 1 ); - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameAHum)], settings.decimals.absHumidity, 1 ); - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaq)], settings.decimals.iaq, 0 ); // Index for Air Quality Number is active - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaqVerb)], settings.PublishIAQVerbal, -1 ); // deactivated - Index for Air Quality (IAQ) verbal classification - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaq)], settings.decimals.staticIaq, 0 ); // activated - Static IAQ is better than IAQ for devices that are not moved - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaqVerb)], settings.PublishStaticIAQVerbal, 0 ); // activated - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameCo2)], settings.decimals.co2, 0 ); - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameVoc)], settings.decimals.Voc, 0 ); - configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasPer)], settings.decimals.gasPerc, 0 ); - - DEBUG_PRINTLN(F(OK)); - - /* Set the selected temperature unit */ - if (settings.tempScale) { - tempScale = F(_unitFahrenheit); - } - else { - tempScale = F(_unitCelsius); - } - - if (flags.DeleteCaibration) { - DEBUG_PRINT(F(UMOD_DEBUG_NAME "Deleting Calibration File")); - flags.DeleteCaibration = false; - if (WLED_FS.remove(CALIB_FILE_NAME)) { - DEBUG_PRINTLN(F(OK)); - } - else { - DEBUG_PRINTLN(F(FAIL)); - } - } - - if (settings.Interval < 1) settings.Interval = 1; // Correct interval on need (A number less than 1 is not permitted) - iaqSensor.setTemperatureOffset(settings.tempOffset); // Set Temp Offset - - return configComplete; -} - -/** - * @brief Called by WLED: Retunrs the user modul id number - * - * @return uint16_t User module number - */ -uint16_t UsermodBME68X::getId() { - return USERMOD_ID_BME68X; -} - - -/** - * @brief Returns the current temperature in the scale which is choosen in settings - * @return Temperature value (°C or °F as choosen in settings) -*/ -inline float UsermodBME68X::getTemperature() { - return ValuesPtr->temperature; -} - -/** - * @brief Returns the current humidity - * @return Humididty value (%) -*/ -inline float UsermodBME68X::getHumidity() { - return ValuesPtr->humidity; -} - -/** - * @brief Returns the current pressure - * @return Pressure value (hPa) -*/ -inline float UsermodBME68X::getPressure() { - return ValuesPtr->pressure; -} - -/** - * @brief Returns the current gas resistance - * @return Gas resistance value (kΩ) -*/ -inline float UsermodBME68X::getGasResistance() { - return ValuesPtr->gasResistance; -} - -/** - * @brief Returns the current absolute humidity - * @return Absolute humidity value (g/m³) -*/ -inline float UsermodBME68X::getAbsoluteHumidity() { - return ValuesPtr->absHumidity; -} - -/** - * @brief Returns the current dew point - * @return Dew point (°C or °F as choosen in settings) -*/ -inline float UsermodBME68X::getDewPoint() { - return ValuesPtr->drewPoint; -} - -/** - * @brief Returns the current iaq (Indoor Air Quallity) - * @return Iaq value (0-500) -*/ -inline float UsermodBME68X::getIaq() { - return ValuesPtr->iaq; -} - -/** - * @brief Returns the current static iaq (Indoor Air Quallity) (NOTE: Static iaq is the better choice than iaq for fixed devices such as the wled module) - * @return Static iaq value (float) -*/ -inline float UsermodBME68X::getStaticIaq() { - return ValuesPtr->staticIaq; -} - -/** - * @brief Returns the current co2 - * @return Co2 value (ppm) -*/ -inline float UsermodBME68X::getCo2() { - return ValuesPtr->co2; -} - -/** - * @brief Returns the current voc (Breath VOC concentration estimate [ppm]) - * @return Voc value (ppm) -*/ -inline float UsermodBME68X::getVoc() { - return ValuesPtr->Voc; -} - -/** - * @brief Returns the current gas percentage - * @return Gas percentage value (%) -*/ -inline float UsermodBME68X::getGasPerc() { - return ValuesPtr->gasPerc; -} - -/** - * @brief Returns the current iaq accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) - * @return Iaq accuracy value (0-3) -*/ -inline uint8_t UsermodBME68X::getIaqAccuracy() { - return ValuesPtr->iaqAccuracy ; -} - -/** - * @brief Returns the current static iaq accuracy accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) - * @return Static iaq accuracy value (0-3) -*/ -inline uint8_t UsermodBME68X::getStaticIaqAccuracy() { - return ValuesPtr->staticIaqAccuracy; -} - -/** - * @brief Returns the current co2 accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) - * @return Co2 accuracy value (0-3) -*/ -inline uint8_t UsermodBME68X::getCo2Accuracy() { - return ValuesPtr->co2Accuracy; -} - -/** - * @brief Returns the current voc accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) - * @return Voc accuracy value (0-3) -*/ -inline uint8_t UsermodBME68X::getVocAccuracy() { - return ValuesPtr->VocAccuracy; -} - -/** - * @brief Returns the current gas percentage accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) - * @return Gas percentage accuracy value (0-3) -*/ -inline uint8_t UsermodBME68X::getGasPercAccuracy() { - return ValuesPtr->gasPercAccuracy; -} - -/** - * @brief Returns the current stab status. - * Indicates when the sensor is ready after after switch-on - * @return stab status value (0 = switched on / 1 = stabilized) -*/ -inline bool UsermodBME68X::getStabStatus() { - return ValuesPtr->stabStatus; -} - -/** - * @brief Returns the current run in status. - * Indicates if the sensor is undergoing initial stabilization during its first use after production - * @return Tun status accuracy value (0 = switched on first time / 1 = stabilized) -*/ -inline bool UsermodBME68X::getRunInStatus() { - return ValuesPtr->runInStatus; -} - - -/** - * @brief Checks whether the library and the sensor are running. - */ -void UsermodBME68X::checkIaqSensorStatus() { - - if (iaqSensor.bsecStatus != BSEC_OK) { - InfoPageStatusLine = "BSEC Library "; - DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); - flags.InitSuccessful = false; - if (iaqSensor.bsecStatus < BSEC_OK) { - InfoPageStatusLine += " Error Code : " + String(iaqSensor.bsecStatus); - DEBUG_PRINTLN(FAIL); - } - else { - InfoPageStatusLine += " Warning Code : " + String(iaqSensor.bsecStatus); - DEBUG_PRINTLN(WARN); - } - } - else { - InfoPageStatusLine = "Sensor BME68X "; - DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); - - if (iaqSensor.bme68xStatus != BME68X_OK) { - flags.InitSuccessful = false; - if (iaqSensor.bme68xStatus < BME68X_OK) { - InfoPageStatusLine += "error code: " + String(iaqSensor.bme68xStatus); - DEBUG_PRINTLN(FAIL); - } - else { - InfoPageStatusLine += "warning code: " + String(iaqSensor.bme68xStatus); - DEBUG_PRINTLN(WARN); - } - } - else { - InfoPageStatusLine += F("OK"); - DEBUG_PRINTLN(OK); - } - } -} - -/** - * @brief Loads the calibration data from the file system of the device - */ -void UsermodBME68X::loadState() { - if (WLED_FS.exists(CALIB_FILE_NAME)) { - DEBUG_PRINT(F(UMOD_DEBUG_NAME "Read the calibration file: ")); - File file = WLED_FS.open(CALIB_FILE_NAME, FILE_READ); - if (!file) { - DEBUG_PRINTLN(FAIL); - } - else { - file.read(bsecState, BSEC_MAX_STATE_BLOB_SIZE); - file.close(); - DEBUG_PRINTLN(OK); - iaqSensor.setState(bsecState); - } - } - else { - DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Calibration file not found.")); - } -} - -/** - * @brief Saves the calibration data from the file system of the device - */ -void UsermodBME68X::saveState() { - DEBUG_PRINT(F(UMOD_DEBUG_NAME "Write the calibration file ")); - File file = WLED_FS.open(CALIB_FILE_NAME, FILE_WRITE); - if (!file) { - DEBUG_PRINTLN(FAIL); - } - else { - iaqSensor.getState(bsecState); - file.write(bsecState, BSEC_MAX_STATE_BLOB_SIZE); - file.close(); - stateUpdateCounter++; - DEBUG_PRINTF("(saved %d times)" OK "\n", stateUpdateCounter); - flags.SaveState = false; // Clear save state flag - - char contbuffer[30]; - - /* Timestamp */ - time_t curr_time; - tm* curr_tm; - time(&curr_time); - curr_tm = localtime(&curr_time); - - snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Last Run"); - strftime(contbuffer, 30, "%d %B %Y - %T", curr_tm); - if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); - - snprintf(contbuffer, 30, "%d", stateUpdateCounter); - snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Count"); - if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); - } -} diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/Battery.cpp similarity index 99% rename from usermods/Battery/usermod_v2_Battery.h rename to usermods/Battery/Battery.cpp index b36c5f4d60..5572f55024 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/Battery.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include "battery_defaults.h" #include "UMBattery.h" @@ -857,3 +855,7 @@ const char UsermodBattery::_preset[] PROGMEM = "preset"; const char UsermodBattery::_duration[] PROGMEM = "duration"; const char UsermodBattery::_init[] PROGMEM = "init"; const char UsermodBattery::_haDiscovery[] PROGMEM = "HA-discovery"; + + +static UsermodBattery battery; +REGISTER_USERMOD(battery); \ No newline at end of file diff --git a/usermods/Battery/library.json b/usermods/Battery/library.json new file mode 100644 index 0000000000..8e71c60a77 --- /dev/null +++ b/usermods/Battery/library.json @@ -0,0 +1,4 @@ +{ + "name": "Battery", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/Battery/readme.md b/usermods/Battery/readme.md index c3d3d8bf47..0e203f3a2b 100644 --- a/usermods/Battery/readme.md +++ b/usermods/Battery/readme.md @@ -23,9 +23,7 @@ Enables battery level monitoring of your project. ## 🎈 Installation -| **Option 1** | **Option 2** | -|--------------|--------------| -| In `wled00/my_config.h`
Add the line: `#define USERMOD_BATTERY`

[Example: my_config.h](assets/installation_my_config_h.png) | In `platformio_override.ini` (or `platformio.ini`)
Under: `build_flags =`, add the line: `-D USERMOD_BATTERY`

[Example: platformio_override.ini](assets/installation_platformio_override_ini.png) | +In `platformio_override.ini` (or `platformio.ini`)
Under: `custom_usermods =`, add the line: `Battery`

[Example: platformio_override.ini](assets/installation_platformio_override_ini.png) |

diff --git a/usermods/Cronixie/usermod_cronixie.h b/usermods/Cronixie/Cronixie.cpp similarity index 98% rename from usermods/Cronixie/usermod_cronixie.h rename to usermods/Cronixie/Cronixie.cpp index 671c5d134d..e0a3bbee7a 100644 --- a/usermods/Cronixie/usermod_cronixie.h +++ b/usermods/Cronixie/Cronixie.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" class UsermodCronixie : public Usermod { @@ -249,7 +247,7 @@ class UsermodCronixie : public Usermod { if (backlight && _digitOut[i] <11) { - uint32_t col = gamma32(strip.getSegment(0).colors[1]); + uint32_t col = strip.getSegment(0).colors[1]; for (uint16_t j=o; j< o+10; j++) { if (j != excl) strip.setPixelColor(j, col); } @@ -299,4 +297,7 @@ class UsermodCronixie : public Usermod { { return USERMOD_ID_CRONIXIE; } -}; \ No newline at end of file +}; + +static UsermodCronixie cronixie; +REGISTER_USERMOD(cronixie); \ No newline at end of file diff --git a/usermods/Cronixie/library.json b/usermods/Cronixie/library.json new file mode 100644 index 0000000000..4a1b6988e2 --- /dev/null +++ b/usermods/Cronixie/library.json @@ -0,0 +1,4 @@ +{ + "name": "Cronixie", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/Cronixie/readme.md b/usermods/Cronixie/readme.md index 1eeac8ed03..38efdbab55 100644 --- a/usermods/Cronixie/readme.md +++ b/usermods/Cronixie/readme.md @@ -4,5 +4,5 @@ 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. +Compile and upload after adding `Cronixie` to `custom_usermods` 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/DHT/usermod_dht.h b/usermods/DHT/DHT.cpp similarity index 98% rename from usermods/DHT/usermod_dht.h rename to usermods/DHT/DHT.cpp index 05a7267b5f..2ed3dd0ace 100644 --- a/usermods/DHT/usermod_dht.h +++ b/usermods/DHT/DHT.cpp @@ -1,7 +1,5 @@ -#pragma once - #include "wled.h" -#ifndef WLED_ENABLE_MQTT +#ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif @@ -245,3 +243,7 @@ class UsermodDHT : public Usermod { } }; + + +static UsermodDHT dht; +REGISTER_USERMOD(dht); \ No newline at end of file diff --git a/usermods/DHT/library.json b/usermods/DHT/library.json new file mode 100644 index 0000000000..7b0dc36187 --- /dev/null +++ b/usermods/DHT/library.json @@ -0,0 +1,7 @@ +{ + "name": "DHT", + "build": { "libArchive": false}, + "dependencies": { + "DHT_nonblocking":"https://github.com/alwynallan/DHT_nonblocking" + } +} diff --git a/usermods/DHT/platformio_override.ini b/usermods/DHT/platformio_override.ini index d192f0434e..6ec2fb9992 100644 --- a/usermods/DHT/platformio_override.ini +++ b/usermods/DHT/platformio_override.ini @@ -1,6 +1,5 @@ ; 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 @@ -11,13 +10,11 @@ [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 +custom_usermods = ${env:d1_mini.custom_usermods} DHT +build_flags = ${env:d1_mini.build_flags} -D USERMOD_DHT_CELSIUS [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 +custom_usermods = ${env:custom32_LEDPIN_16.custom_usermods} DHT +build_flags = ${env:custom32_LEDPIN_16.build_flags} -D USERMOD_DHT_CELSIUS -D USERMOD_DHT_STATS diff --git a/usermods/DHT/readme.md b/usermods/DHT/readme.md index 6089ffbf88..9080b9b200 100644 --- a/usermods/DHT/readme.md +++ b/usermods/DHT/readme.md @@ -15,7 +15,6 @@ Copy the example `platformio_override.ini` to the root directory. This file sho ### 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 diff --git a/usermods/EXAMPLE/library.json b/usermods/EXAMPLE/library.json new file mode 100644 index 0000000000..d0dc2f88e6 --- /dev/null +++ b/usermods/EXAMPLE/library.json @@ -0,0 +1,5 @@ +{ + "name": "EXAMPLE", + "build": { "libArchive": false }, + "dependencies": {} +} diff --git a/usermods/EXAMPLE_v2/readme.md b/usermods/EXAMPLE/readme.md similarity index 64% rename from usermods/EXAMPLE_v2/readme.md rename to usermods/EXAMPLE/readme.md index 8917a1fba3..ee8a2282a0 100644 --- a/usermods/EXAMPLE_v2/readme.md +++ b/usermods/EXAMPLE/readme.md @@ -4,7 +4,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.cpp` and compile! +Add `EXAMPLE` to `custom_usermods` in your PlatformIO environment 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/usermod_v2_example.cpp similarity index 97% rename from usermods/EXAMPLE_v2/usermod_v2_example.h rename to usermods/EXAMPLE/usermod_v2_example.cpp index df05f3e3dc..02e399fe08 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE/usermod_v2_example.cpp @@ -1,10 +1,8 @@ -#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 + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is an example for a v2 usermod. * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. @@ -315,11 +313,11 @@ class MyExampleUsermod : public Usermod { yield(); // ignore certain button types as they may have other consequences if (!enabled - || buttonType[b] == BTN_TYPE_NONE - || buttonType[b] == BTN_TYPE_RESERVED - || buttonType[b] == BTN_TYPE_PIR_SENSOR - || buttonType[b] == BTN_TYPE_ANALOG - || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { + || buttons[b].type == BTN_TYPE_NONE + || buttons[b].type == BTN_TYPE_RESERVED + || buttons[b].type == BTN_TYPE_PIR_SENSOR + || buttons[b].type == BTN_TYPE_ANALOG + || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { return false; } @@ -404,3 +402,6 @@ void MyExampleUsermod::publishMqtt(const char* state, bool retain) } #endif } + +static MyExampleUsermod example_usermod; +REGISTER_USERMOD(example_usermod); diff --git a/usermods/EleksTube_IPS/usermod_elekstube_ips.h b/usermods/EleksTube_IPS/EleksTube_IPS.cpp similarity index 98% rename from usermods/EleksTube_IPS/usermod_elekstube_ips.h rename to usermods/EleksTube_IPS/EleksTube_IPS.cpp index 0f7d92e7e9..e8637b0fe8 100644 --- a/usermods/EleksTube_IPS/usermod_elekstube_ips.h +++ b/usermods/EleksTube_IPS/EleksTube_IPS.cpp @@ -1,4 +1,3 @@ -#pragma once #include "TFTs.h" #include "wled.h" @@ -156,3 +155,7 @@ class ElekstubeIPSUsermod : public Usermod { const char ElekstubeIPSUsermod::_name[] PROGMEM = "EleksTubeIPS"; const char ElekstubeIPSUsermod::_tubeSeg[] PROGMEM = "tubeSegment"; const char ElekstubeIPSUsermod::_digitOffset[] PROGMEM = "digitOffset"; + + +static ElekstubeIPSUsermod elekstube_ips; +REGISTER_USERMOD(elekstube_ips); \ No newline at end of file diff --git a/usermods/EleksTube_IPS/library.json.disabled b/usermods/EleksTube_IPS/library.json.disabled new file mode 100644 index 0000000000..d143638e47 --- /dev/null +++ b/usermods/EleksTube_IPS/library.json.disabled @@ -0,0 +1,8 @@ +{ + "name:": "EleksTube_IPS", + "build": { "libArchive": false }, + "dependencies": { + "TFT_eSPI" : "2.5.33" + } +} +# Seems to add 300kb to the RAM requirement??? diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp b/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp index 823ad74724..d51ddb57cf 100644 --- a/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp +++ b/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp @@ -1,11 +1,12 @@ -#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/ #include //Dallastemperature sensor + +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + //The SCL and SDA pins are defined here. //Lolin32 boards use SCL=5 SDA=4 #define U8X8_PIN_SCL 5 diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp b/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp index 29a4332dbd..ce3c659e8f 100644 --- a/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp +++ b/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp @@ -1,13 +1,13 @@ -#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/ #include #include //BME280 sensor +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + void UpdateBME280Data(); #define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit diff --git a/usermods/Fix_unreachable_netservices_v2/library.json b/usermods/Fix_unreachable_netservices_v2/library.json new file mode 100644 index 0000000000..4d1dbfc8e4 --- /dev/null +++ b/usermods/Fix_unreachable_netservices_v2/library.json @@ -0,0 +1,4 @@ +{ + "name": "Fix_unreachable_netservices_v2", + "platforms": ["espressif8266"] +} diff --git a/usermods/Fix_unreachable_netservices_v2/readme.md b/usermods/Fix_unreachable_netservices_v2/readme.md index 07d64bc673..9f3889ebbf 100644 --- a/usermods/Fix_unreachable_netservices_v2/readme.md +++ b/usermods/Fix_unreachable_netservices_v2/readme.md @@ -30,41 +30,6 @@ The usermod supports the following state changes: ## Installation -1. Copy the file `usermod_Fix_unreachable_netservices.h` to the `wled00` directory. -2. Register the usermod by adding `#include "usermod_Fix_unreachable_netservices.h"` in the top and `registerUsermod(new FixUnreachableNetServices());` in the bottom of `usermods_list.cpp`. - -Example **usermods_list.cpp**: - -```cpp -#include "wled.h" -/* - * Register your v2 usermods here! - * (for v1 usermods using just usermod.cpp, you can ignore this file) - */ - -/* - * Add/uncomment your usermod filename here (and once more below) - * || || || - * \/ \/ \/ - */ -//#include "usermod_v2_example.h" -//#include "usermod_temperature.h" -//#include "usermod_v2_empty.h" -#include "usermod_Fix_unreachable_netservices.h" - -void registerUsermods() -{ - /* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //UsermodManager::add(new MyExampleUsermod()); - //UsermodManager::add(new UsermodTemperature()); - //UsermodManager::add(new UsermodRenameMe()); - UsermodManager::add(new FixUnreachableNetServices()); - -} -``` +1. Add `Fix_unreachable_netservices` to `custom_usermods` in your PlatformIO environment. Hopefully I can help someone with that - @gegu diff --git a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp similarity index 96% rename from usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h rename to usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp index 3d441e59d7..7fb8e97982 100644 --- a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h +++ b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp @@ -1,12 +1,4 @@ -#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 @@ -16,7 +8,7 @@ class FixUnreachableNetServices : public Usermod * By this procedure the net services of WLED remains accessible in some problematic WLAN environments. * * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/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. @@ -168,4 +160,11 @@ Delay @@ -210,12 +208,6 @@ class UsermodINA226 : public Usermod } } - ~UsermodINA226() - { - delete _ina226; - _ina226 = nullptr; - } - #ifndef WLED_DISABLE_MQTT void mqttInitialize() { @@ -551,6 +543,17 @@ class UsermodINA226 : public Usermod _initDone = true; return configComplete; } + + ~UsermodINA226() + { + delete _ina226; + _ina226 = nullptr; + } + }; const char UsermodINA226::_name[] PROGMEM = "INA226"; + + +static UsermodINA226 ina226_v2; +REGISTER_USERMOD(ina226_v2); \ No newline at end of file diff --git a/usermods/INA226_v2/README.md b/usermods/INA226_v2/README.md index b1e6916184..dfa9fc003b 100644 --- a/usermods/INA226_v2/README.md +++ b/usermods/INA226_v2/README.md @@ -22,13 +22,6 @@ The following settings can be configured in the Usermod Menu: - **MqttPublishAlways**: Publish always, regardless if there is a change. - **MqttHomeAssistantDiscovery**: Enable Home Assistant discovery. -## Dependencies - -These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). - -- Libraries - - `wollewald/INA226_WE@~1.2.9` (by [wollewald](https://registry.platformio.org/libraries/wollewald/INA226_WE)) - - `Wire` ## Understanding Samples and Conversion Times @@ -62,16 +55,12 @@ For detailed programming information and register configurations, refer to the [ ## Compiling -To enable, compile with `USERMOD_INA226` defined (e.g. in `platformio_override.ini`). +To enable, compile with `INA226` in `custom_usermods` (e.g. in `platformio_override.ini`). ```ini [env:ina226_example] extends = env:esp32dev -build_flags = - ${common.build_flags} ${esp32.build_flags} - -D USERMOD_INA226 +custom_usermods = ${env:esp32dev.custom_usermods} INA226 +build_flags = ${env:esp32dev.build_flags} ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal -lib_deps = - ${esp32.lib_deps} - wollewald/INA226_WE@~1.2.9 ``` \ No newline at end of file diff --git a/usermods/INA226_v2/library.json b/usermods/INA226_v2/library.json new file mode 100644 index 0000000000..34fcd36830 --- /dev/null +++ b/usermods/INA226_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "INA226_v2", + "build": { "libArchive": false }, + "dependencies": { + "wollewald/INA226_WE":"~1.2.9" + } +} diff --git a/usermods/INA226_v2/platformio_override.ini b/usermods/INA226_v2/platformio_override.ini index 885b2dd1e4..9968cbf721 100644 --- a/usermods/INA226_v2/platformio_override.ini +++ b/usermods/INA226_v2/platformio_override.ini @@ -1,9 +1,6 @@ [env:ina226_example] extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2 build_flags = - ${common.build_flags} ${esp32.build_flags} - -D USERMOD_INA226 + ${env:esp32dev.build_flags} ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal -lib_deps = - ${esp32.lib_deps} - wollewald/INA226_WE@~1.2.9 \ No newline at end of file diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp similarity index 97% rename from usermods/Internal_Temperature_v2/usermod_internal_temperature.h rename to usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp index c24b4c6288..7c30985eea 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" class InternalTemperatureUsermod : public Usermod @@ -50,7 +48,7 @@ class InternalTemperatureUsermod : public Usermod #else // ESP32 ESP32S3 and ESP32C3 temperature = roundf(temperatureRead() * 10) / 10; #endif - + if(presetToActivate != 0){ // Check if temperature has exceeded the activation threshold if (temperature >= activationThreshold) { // Update the state flag if not already set @@ -58,7 +56,7 @@ class InternalTemperatureUsermod : public Usermod isAboveThreshold = true; } // Check if a 'high temperature' preset is configured and it's not already active - if (presetToActivate != 0 && currentPreset != presetToActivate) { + if (currentPreset != presetToActivate) { // If a playlist is active, store it for reactivation later if (currentPlaylist > 0) { previousPlaylist = currentPlaylist; @@ -101,6 +99,7 @@ class InternalTemperatureUsermod : public Usermod } } } + } #ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) @@ -192,4 +191,7 @@ void InternalTemperatureUsermod::publishMqtt(const char *state, bool retain) mqtt->publish(subuf, 0, retain, state); } #endif -} \ No newline at end of file +} + +static InternalTemperatureUsermod internal_temperature_v2; +REGISTER_USERMOD(internal_temperature_v2); \ No newline at end of file diff --git a/usermods/Internal_Temperature_v2/library.json b/usermods/Internal_Temperature_v2/library.json new file mode 100644 index 0000000000..b1826ab458 --- /dev/null +++ b/usermods/Internal_Temperature_v2/library.json @@ -0,0 +1,4 @@ +{ + "name": "Internal_Temperature_v2", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/Internal_Temperature_v2/readme.md b/usermods/Internal_Temperature_v2/readme.md index d574f3abff..68bac8b027 100644 --- a/usermods/Internal_Temperature_v2/readme.md +++ b/usermods/Internal_Temperature_v2/readme.md @@ -23,8 +23,7 @@ ## Installation -- Add a build flag `-D USERMOD_INTERNAL_TEMPERATURE` to your `platformio.ini` (or `platformio_override.ini`). - +- Add `Internal_Temperature` to `custom_usermods` in your `platformio.ini` (or `platformio_override.ini`). ## 📝 Change Log diff --git a/usermods/LD2410_v2/usermod_ld2410.h b/usermods/LD2410_v2/LD2410_v2.cpp similarity index 98% rename from usermods/LD2410_v2/usermod_ld2410.h rename to usermods/LD2410_v2/LD2410_v2.cpp index 4d96b32fff..095da12f25 100644 --- a/usermods/LD2410_v2/usermod_ld2410.h +++ b/usermods/LD2410_v2/LD2410_v2.cpp @@ -1,14 +1,10 @@ -#warning **** Included USERMOD_LD2410 **** +#include "wled.h" +#include -#ifndef WLED_ENABLE_MQTT +#ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif -#pragma once - -#include "wled.h" -#include - class LD2410Usermod : public Usermod { private: @@ -235,3 +231,7 @@ void LD2410Usermod::publishMqtt(const char* topic, const char* state, bool retai } #endif } + + +static LD2410Usermod ld2410_v2; +REGISTER_USERMOD(ld2410_v2); \ No newline at end of file diff --git a/usermods/LD2410_v2/library.json b/usermods/LD2410_v2/library.json new file mode 100644 index 0000000000..757ec40477 --- /dev/null +++ b/usermods/LD2410_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "LD2410_v2", + "build": { "libArchive": false }, + "dependencies": { + "ncmreynolds/ld2410":"^0.1.3" + } +} \ No newline at end of file diff --git a/usermods/LD2410_v2/readme.md b/usermods/LD2410_v2/readme.md index 40463d4b0e..25b1cbbcc3 100644 --- a/usermods/LD2410_v2/readme.md +++ b/usermods/LD2410_v2/readme.md @@ -10,21 +10,15 @@ The movement and presence state are displayed in both the Info section of the we ## Dependencies - Libraries - `ncmreynolds/ld2410@^0.1.3` - - This must be added under `lib_deps` in your `platformio.ini` (or `platformio_override.ini`). - Data is published over MQTT - make sure you've enabled the MQTT sync interface. ## Compilation -To enable, compile with `USERMOD_LD2410` defined (e.g. in `platformio_override.ini`) +To enable, compile with `LD2140` in `custom_usermods` (e.g. in `platformio_override.ini`) ```ini [env:usermod_USERMOD_LD2410_esp32dev] extends = env:esp32dev -build_flags = - ${common.build_flags_esp32} - -D USERMOD_LD2410 -lib_deps = - ${esp32.lib_deps} - ncmreynolds/ld2410@^0.1.3 +custom_usermods = ${env:esp32dev.custom_usermods} LD2140_v2 ``` ### Configuration Options diff --git a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h b/usermods/LDR_Dusk_Dawn_v2/LDR_Dusk_Dawn_v2.cpp similarity index 98% rename from usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h rename to usermods/LDR_Dusk_Dawn_v2/LDR_Dusk_Dawn_v2.cpp index 03f4c078a4..9c5c835e9a 100644 --- a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h +++ b/usermods/LDR_Dusk_Dawn_v2/LDR_Dusk_Dawn_v2.cpp @@ -1,4 +1,3 @@ -#pragma once #include "wled.h" #ifndef ARDUINO_ARCH_ESP32 @@ -151,3 +150,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { }; const char LDR_Dusk_Dawn_v2::_name[] PROGMEM = "LDR_Dusk_Dawn_v2"; + + +static LDR_Dusk_Dawn_v2 ldr_dusk_dawn_v2; +REGISTER_USERMOD(ldr_dusk_dawn_v2); \ No newline at end of file diff --git a/usermods/LDR_Dusk_Dawn_v2/README.md b/usermods/LDR_Dusk_Dawn_v2/README.md index 5e33518a9d..8fe86a3697 100644 --- a/usermods/LDR_Dusk_Dawn_v2/README.md +++ b/usermods/LDR_Dusk_Dawn_v2/README.md @@ -2,13 +2,14 @@ This usermod will obtain readings from a Light Dependent Resistor (LDR) and will turn on/off specific presets based on those readings. This is useful for exterior lighting situations where you want the lights to only be on when it is dark out. # Installation -Add "-D USERMOD_LDR_DUSK_DAWN" to your platformio.ini [common] build_flags and build. +Add "LDR_Dusk_Dawn" to your platformio.ini environment's custom_usermods and build. Example: ``` -[common] -build_flags = - -D USERMOD_LDR_DUSK_DAWN # Enable LDR Dusk Dawn Usermod +[env:usermod_LDR_Dusk_Dawn_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + LDR_Dusk_Dawn # Enable LDR Dusk Dawn Usermod ``` # Usermod Settings diff --git a/usermods/LDR_Dusk_Dawn_v2/library.json b/usermods/LDR_Dusk_Dawn_v2/library.json new file mode 100644 index 0000000000..709967ea70 --- /dev/null +++ b/usermods/LDR_Dusk_Dawn_v2/library.json @@ -0,0 +1,4 @@ +{ + "name": "LDR_Dusk_Dawn_v2", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/MAX17048_v2/usermod_max17048.h b/usermods/MAX17048_v2/MAX17048_v2.cpp similarity index 97% rename from usermods/MAX17048_v2/usermod_max17048.h rename to usermods/MAX17048_v2/MAX17048_v2.cpp index c3a2664ab1..520f1a7b35 100644 --- a/usermods/MAX17048_v2/usermod_max17048.h +++ b/usermods/MAX17048_v2/MAX17048_v2.cpp @@ -1,8 +1,6 @@ // force the compiler to show a warning to confirm that this file is included #warning **** Included USERMOD_MAX17048 V2.0 **** -#pragma once - #include "wled.h" #include "Adafruit_MAX1704X.h" @@ -37,8 +35,8 @@ class Usermod_MAX17048 : public Usermod { unsigned long lastSend = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT); - uint8_t VoltageDecimals = 3; // Number of decimal places in published voltage values - uint8_t PercentDecimals = 1; // Number of decimal places in published percent values + unsigned VoltageDecimals = 3; // Number of decimal places in published voltage values + unsigned PercentDecimals = 1; // Number of decimal places in published percent values // string that are used multiple time (this will save some flash memory) static const char _name[]; @@ -279,3 +277,7 @@ const char Usermod_MAX17048::_enabled[] PROGMEM = "enabled"; const char Usermod_MAX17048::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; const char Usermod_MAX17048::_minReadInterval[] PROGMEM = "min-read-interval-ms"; const char Usermod_MAX17048::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscovery"; + + +static Usermod_MAX17048 max17048_v2; +REGISTER_USERMOD(max17048_v2); \ No newline at end of file diff --git a/usermods/MAX17048_v2/library.json b/usermods/MAX17048_v2/library.json new file mode 100644 index 0000000000..a9ae1543fe --- /dev/null +++ b/usermods/MAX17048_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "MAX17048_v2", + "build": { "libArchive": false}, + "dependencies": { + "Adafruit_MAX1704X":"https://github.com/adafruit/Adafruit_MAX1704X#1.0.2" + } +} diff --git a/usermods/MAX17048_v2/readme.md b/usermods/MAX17048_v2/readme.md index 958e6def2e..df42f989a9 100644 --- a/usermods/MAX17048_v2/readme.md +++ b/usermods/MAX17048_v2/readme.md @@ -5,26 +5,16 @@ This usermod reads information from an Adafruit MAX17048 and outputs the follow ## Dependencies -Libraries: -- `Adafruit_BusIO@~1.14.5` (by [adafruit](https://github.com/adafruit/Adafruit_BusIO)) -- `Adafruit_MAX1704X@~1.0.2` (by [adafruit](https://github.com/adafruit/Adafruit_MAX1704X)) - -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. ## Compilation +Add "MAX17048_v2" to your platformio.ini environment's custom_usermods and build. To enable, compile with `USERMOD_MAX17048` define in the build_flags (e.g. in `platformio.ini` or `platformio_override.ini`) such as in the example below: ```ini [env:usermod_max17048_d1_mini] extends = env:d1_mini -build_flags = - ${common.build_flags_esp8266} - -D USERMOD_MAX17048 -lib_deps = - ${esp8266.lib_deps} - https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 - https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 +custom_usermods = ${env:d1_mini.custom_usermods} MAX17048_v2 ``` ### Configuration Options diff --git a/usermods/MY9291/usermode_MY9291.h b/usermods/MY9291/MY9291.cpp similarity index 94% rename from usermods/MY9291/usermode_MY9291.h rename to usermods/MY9291/MY9291.cpp index 66bbc34cbc..3881ffd071 100644 --- a/usermods/MY9291/usermode_MY9291.h +++ b/usermods/MY9291/MY9291.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include "MY92xx.h" @@ -42,4 +40,7 @@ class MY9291Usermod : public Usermod { uint16_t getId() { return USERMOD_ID_MY9291; } -}; \ No newline at end of file +}; + +static MY9291Usermod my9291; +REGISTER_USERMOD(my9291); \ No newline at end of file diff --git a/usermods/MY9291/library.json b/usermods/MY9291/library.json new file mode 100644 index 0000000000..9c3a33d43e --- /dev/null +++ b/usermods/MY9291/library.json @@ -0,0 +1,5 @@ +{ + "name": "MY9291", + "build": { "libArchive": false }, + "platforms": ["espressif8266"] +} \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/PIR_Highlight_Standby b/usermods/PIR_sensor_switch/PIR_Highlight_Standby index 152388e8b9..4ca32bf4ef 100644 --- a/usermods/PIR_sensor_switch/PIR_Highlight_Standby +++ b/usermods/PIR_sensor_switch/PIR_Highlight_Standby @@ -42,7 +42,7 @@ * * * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/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. diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/PIR_sensor_switch.cpp similarity index 96% rename from usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h rename to usermods/PIR_sensor_switch/PIR_sensor_switch.cpp index 0deda181c2..6f09ce5be0 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/PIR_sensor_switch.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #ifndef PIR_SENSOR_PIN @@ -26,7 +24,7 @@ * Maintained by: @blazoncek * * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/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. @@ -571,3 +569,7 @@ bool PIRsensorSwitch::readFromConfig(JsonObject &root) // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !(pins.isNull() || pins.size() != PIR_SENSOR_MAX_SENSORS); } + + +static PIRsensorSwitch pir_sensor_switch; +REGISTER_USERMOD(pir_sensor_switch); \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/library.json b/usermods/PIR_sensor_switch/library.json new file mode 100644 index 0000000000..b3cbcbbff6 --- /dev/null +++ b/usermods/PIR_sensor_switch/library.json @@ -0,0 +1,4 @@ +{ + "name": "PIR_sensor_switch", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index fac5419f00..2b88974815 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -5,7 +5,7 @@ This usermod-v2 modification allows the connection of a PIR sensor to switch on _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. +The LED strip is switched [using a relay](https://kno.wled.ge/features/relay-control/) to keep the power consumption low when it is switched off. ## Web interface @@ -25,7 +25,7 @@ You can also use usermod's off timer instead of sensor's. In such case rotate th **NOTE:** Usermod has been included in master branch of WLED so it can be compiled in directly just by defining `-D USERMOD_PIRSWITCH` and optionally `-D PIR_SENSOR_PIN=16` to override default pin. You can also change the default off time by adding `-D PIR_SENSOR_OFF_SEC=30`. -## API to enable/disable the PIR sensor from outside. For example from another usermod: +## API to enable/disable the PIR sensor from outside. For example from another usermod To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. @@ -33,15 +33,16 @@ When the PIR sensor state changes an MQTT message is broadcasted with topic `wle Usermod can also be configured to send just the MQTT message but not change WLED state using settings page as well as responding to motion only at night (assuming NTP and latitude/longitude are set to determine sunrise/sunset times). -### There are two options to get access to the usermod instance: +### 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' +_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. +_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" @@ -79,25 +80,30 @@ Usermod can be configured via the Usermods settings page. * `override` - override PIR input when WLED state is changed using UI * `domoticz-idx` - Domoticz virtual switch ID (used with MQTT `domoticz/in`) - 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/disable usermod from WLED UI (Info dialog) 2022-11 + * Added compile time option for off timer. * Added Home Assistant autodiscovery MQTT broadcast. * Updated info on compiling. 2023-?? + * Override option * Domoticz virtual switch ID (used with MQTT `domoticz/in`) 2024-02 -* Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3` \ No newline at end of file + +* Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3` diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/PWM_fan.cpp similarity index 98% rename from usermods/PWM_fan/usermod_PWM_fan.h rename to usermods/PWM_fan/PWM_fan.cpp index c3ef24fe41..a0939f0854 100644 --- a/usermods/PWM_fan/usermod_PWM_fan.h +++ b/usermods/PWM_fan/PWM_fan.cpp @@ -1,10 +1,14 @@ -#pragma once +#include "wled.h" -#if !defined(USERMOD_DALLASTEMPERATURE) && !defined(USERMOD_SHT) +#if defined(USERMOD_DALLASTEMPERATURE) +#include "UsermodTemperature.h" +#elif defined(USERMOD_SHT) +#include "ShtUsermod.h" +#else #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 @@ -397,3 +401,7 @@ const char PWMFanUsermod::_maxPWMValuePct[] PROGMEM = "max-PWM-percent"; const char PWMFanUsermod::_IRQperRotation[] PROGMEM = "IRQs-per-rotation"; const char PWMFanUsermod::_speed[] PROGMEM = "speed"; const char PWMFanUsermod::_lock[] PROGMEM = "lock"; + + +static PWMFanUsermod pwm_fan; +REGISTER_USERMOD(pwm_fan); \ No newline at end of file diff --git a/usermods/PWM_fan/library.json b/usermods/PWM_fan/library.json new file mode 100644 index 0000000000..8ae3d7fd6e --- /dev/null +++ b/usermods/PWM_fan/library.json @@ -0,0 +1,7 @@ +{ + "name": "PWM_fan", + "build": { + "libArchive": false, + "extraScript": "setup_deps.py" + } +} \ No newline at end of file diff --git a/usermods/PWM_fan/readme.md b/usermods/PWM_fan/readme.md index 6a44acf3b3..872bbd9b9f 100644 --- a/usermods/PWM_fan/readme.md +++ b/usermods/PWM_fan/readme.md @@ -11,8 +11,8 @@ If the _tachometer_ is supported, the current speed (in RPM) will be displayed o ## 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`. +Add the `PWM_fan` to `custom_usermods` in your `platformio.ini` (or `platformio_override.ini`) +You will also need `Temperature` or `sht`. ### Define Your Options @@ -40,6 +40,9 @@ If the fan speed is unlocked, it will revert to temperature controlled speed on ## Change Log 2021-10 + * First public release + 2022-05 + * Added JSON API call to allow changing of speed diff --git a/usermods/PWM_fan/setup_deps.py b/usermods/PWM_fan/setup_deps.py new file mode 100644 index 0000000000..11879079a6 --- /dev/null +++ b/usermods/PWM_fan/setup_deps.py @@ -0,0 +1,12 @@ +from platformio.package.meta import PackageSpec +Import('env') + + +libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] +# Check for dependencies +if "Temperature" in libs: + env.Append(CPPDEFINES=[("USERMOD_DALLASTEMPERATURE")]) +elif "sht" in libs: + env.Append(CPPDEFINES=[("USERMOD_SHT")]) +elif "PWM_fan" in libs: # The script can be run if this module was previously selected + raise RuntimeError("PWM_fan usermod requires Temperature or sht to be enabled") diff --git a/usermods/RTC/usermod_rtc.h b/usermods/RTC/RTC.cpp similarity index 96% rename from usermods/RTC/usermod_rtc.h rename to usermods/RTC/RTC.cpp index 42965e3af3..2b9c3b4f71 100644 --- a/usermods/RTC/usermod_rtc.h +++ b/usermods/RTC/RTC.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "src/dependencies/time/DS1307RTC.h" #include "wled.h" @@ -48,4 +46,7 @@ class RTCUsermod : public Usermod { { return USERMOD_ID_RTC; } -}; \ No newline at end of file +}; + +static RTCUsermod rtc; +REGISTER_USERMOD(rtc); \ No newline at end of file diff --git a/usermods/RTC/library.json b/usermods/RTC/library.json new file mode 100644 index 0000000000..688dfc2d0c --- /dev/null +++ b/usermods/RTC/library.json @@ -0,0 +1,4 @@ +{ + "name": "RTC", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/SN_Photoresistor/SN_Photoresistor.cpp b/usermods/SN_Photoresistor/SN_Photoresistor.cpp new file mode 100644 index 0000000000..ffd78c0f6d --- /dev/null +++ b/usermods/SN_Photoresistor/SN_Photoresistor.cpp @@ -0,0 +1,146 @@ +#include "wled.h" +#include "SN_Photoresistor.h" + +//Pin defaults for QuinLed Dig-Uno (A0) +#ifndef PHOTORESISTOR_PIN +#define PHOTORESISTOR_PIN A0 +#endif + +static bool checkBoundSensor(float newValue, float prevValue, float maxDiff) +{ + return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff; +} + +uint16_t Usermod_SN_Photoresistor::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); +} + +void Usermod_SN_Photoresistor::setup() +{ + // set pinmode + pinMode(PHOTORESISTOR_PIN, INPUT); +} + +void Usermod_SN_Photoresistor::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(F("Missing MQTT connection. Not publishing data")); + } + } +#endif +} + + +void Usermod_SN_Photoresistor::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")); +} + + +/** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ +void Usermod_SN_Photoresistor::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 Usermod_SN_Photoresistor::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"; + +static Usermod_SN_Photoresistor sn_photoresistor; +REGISTER_USERMOD(sn_photoresistor); \ No newline at end of file diff --git a/usermods/SN_Photoresistor/SN_Photoresistor.h b/usermods/SN_Photoresistor/SN_Photoresistor.h new file mode 100644 index 0000000000..87836c0e49 --- /dev/null +++ b/usermods/SN_Photoresistor/SN_Photoresistor.h @@ -0,0 +1,90 @@ +#pragma once +#include "wled.h" + +// 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 difference 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 = 65535; + + // 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[]; + + uint16_t getLuminance(); + +public: + void setup(); + void loop(); + + uint16_t getLastLDRValue() + { + return lastLDRValue; + } + + void addToJsonInfo(JsonObject &root); + + uint16_t getId() + { + return USERMOD_ID_SN_PHOTORESISTOR; + } + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void addToConfig(JsonObject &root); + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + */ + bool readFromConfig(JsonObject &root); +}; diff --git a/usermods/SN_Photoresistor/library.json b/usermods/SN_Photoresistor/library.json new file mode 100644 index 0000000000..c896644f71 --- /dev/null +++ b/usermods/SN_Photoresistor/library.json @@ -0,0 +1,4 @@ +{ + "name": "SN_Photoresistor", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h deleted file mode 100644 index 45cdb66ad7..0000000000 --- a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h +++ /dev/null @@ -1,212 +0,0 @@ -#pragma once - -#include "wled.h" - -//Pin defaults for QuinLed Dig-Uno (A0) -#ifndef PHOTORESISTOR_PIN -#define PHOTORESISTOR_PIN A0 -#endif - -// 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 difference 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(F("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"; diff --git a/usermods/SN_Photoresistor/usermods_list.cpp b/usermods/SN_Photoresistor/usermods_list.cpp deleted file mode 100644 index a2c6ca165f..0000000000 --- a/usermods/SN_Photoresistor/usermods_list.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#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 - UsermodManager::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 index ebaae49228..7dd3b599e5 100644 --- a/usermods/ST7789_display/README.md +++ b/usermods/ST7789_display/README.md @@ -27,19 +27,6 @@ This usermod enables display of the following: ### 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: diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.cpp similarity index 99% rename from usermods/ST7789_display/ST7789_display.h rename to usermods/ST7789_display/ST7789_display.cpp index 65f4cae5d3..c596baecce 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.cpp @@ -1,8 +1,6 @@ // 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 @@ -410,4 +408,7 @@ class St7789DisplayUsermod : 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 +}; + +static name. st7789_display; +REGISTER_USERMOD(st7789_display); \ No newline at end of file diff --git a/usermods/ST7789_display/library.json.disabled b/usermods/ST7789_display/library.json.disabled new file mode 100644 index 0000000000..725e20a65a --- /dev/null +++ b/usermods/ST7789_display/library.json.disabled @@ -0,0 +1,4 @@ +{ + "name:": "ST7789_display", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h b/usermods/Si7021_MQTT_HA/Si7021_MQTT_HA.cpp similarity index 98% rename from usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h rename to usermods/Si7021_MQTT_HA/Si7021_MQTT_HA.cpp index 9f027382de..7845658ad1 100644 --- a/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h +++ b/usermods/Si7021_MQTT_HA/Si7021_MQTT_HA.cpp @@ -1,9 +1,3 @@ -#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) @@ -11,7 +5,11 @@ #include #include // EnvironmentCalculations::HeatIndex(), ::DewPoint(), ::AbsoluteHumidity() -Adafruit_Si7021 si7021; +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +static Adafruit_Si7021 si7021; class Si7021_MQTT_HA : public Usermod { @@ -229,3 +227,7 @@ const char Si7021_MQTT_HA::_name[] PROGMEM = "Si7021 MQTT (Hom 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"; + + +static Si7021_MQTT_HA si7021_mqtt_ha; +REGISTER_USERMOD(si7021_mqtt_ha); \ No newline at end of file diff --git a/usermods/Si7021_MQTT_HA/library.json b/usermods/Si7021_MQTT_HA/library.json new file mode 100644 index 0000000000..36a930ca58 --- /dev/null +++ b/usermods/Si7021_MQTT_HA/library.json @@ -0,0 +1,10 @@ +{ + "name": "Si7021_MQTT_HA", + "build": { "libArchive": false }, + "dependencies": { + "finitespace/BME280":"3.0.0", + "adafruit/Adafruit Si7021 Library" : "1.5.3", + "SPI":"*", + "adafruit/Adafruit BusIO": "1.17.1" + } +} \ No newline at end of file diff --git a/usermods/Si7021_MQTT_HA/readme.md b/usermods/Si7021_MQTT_HA/readme.md index 99a240f7dd..b8ee06a73d 100644 --- a/usermods/Si7021_MQTT_HA/readme.md +++ b/usermods/Si7021_MQTT_HA/readme.md @@ -49,18 +49,8 @@ SDA_PIN = 4; ## Software -Add to `build_flags` in platformio.ini: +Add `Si7021_MQTT_HA` to custom_usermods -``` - -D USERMOD_SI7021_MQTT_HA -``` - -Add to `lib_deps` in platformio.ini: - -``` - adafruit/Adafruit Si7021 Library @ 1.4.0 - BME280@~3.0.0 -``` # Credits diff --git a/usermods/TTGO-T-Display/usermod.cpp b/usermods/TTGO-T-Display/usermod.cpp index cbba07771d..d8dcb29996 100644 --- a/usermods/TTGO-T-Display/usermod.cpp +++ b/usermods/TTGO-T-Display/usermod.cpp @@ -1,7 +1,7 @@ /* * This file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/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 unused, but might be used for future wled features */ diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/Temperature.cpp similarity index 75% rename from usermods/Temperature/usermod_temperature.h rename to usermods/Temperature/Temperature.cpp index 178bc05a0d..6dcaa70c6b 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/Temperature.cpp @@ -1,113 +1,6 @@ -#pragma once - -#include "wled.h" -#include "OneWire.h" - -//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 - -// the frequency to check temperature, 1 minute -#ifndef USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL -#define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 -#endif - -static uint16_t mode_temperature(); - -class UsermodTemperature : public Usermod { - - private: - - 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; - int16_t idx = -1; // Domoticz virtual sensor idx - - // 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[]; - static const char _domoticzIDX[]; - static const char _sensor[]; - static const char _temperature[]; - static const char _Temperature[]; - static const char _data_fx[]; - - //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 - - static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE); - - public: - - UsermodTemperature() { _instance = this; } - static UsermodTemperature *getInstance() { return UsermodTemperature::_instance; } +#include "UsermodTemperature.h" - /* - * 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() override { return USERMOD_ID_TEMPERATURE; } - - void setup() override; - void loop() override; - //void connected() override; -#ifndef WLED_DISABLE_MQTT - void onMqttConnect(bool sessionPresent) override; -#endif - //void onUpdateBegin(bool init) override; - - //bool handleButton(uint8_t b) override; - //void handleOverlayDraw() override; - - void addToJsonInfo(JsonObject& root) override; - //void addToJsonState(JsonObject &root) override; - //void readFromJsonState(JsonObject &root) override; - void addToConfig(JsonObject &root) override; - bool readFromConfig(JsonObject &root) override; - - void appendConfigData() override; -}; +static void mode_temperature(); //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 float UsermodTemperature::readDallas() { @@ -127,17 +20,19 @@ float UsermodTemperature::readDallas() { } #endif switch(sensorFound) { - case 0x10: // DS18S20 has 9-bit precision + case 0x10: // DS18S20 has 9-bit precision - 1-bit fraction part result = (data[1] << 8) | data[0]; retVal = float(result) * 0.5f; break; - case 0x22: // DS18B20 - case 0x28: // DS1822 + case 0x22: // DS1822 + case 0x28: // DS18B20 case 0x3B: // DS1825 case 0x42: // DS28EA00 - result = (data[1]<<4) | (data[0]>>4); // we only need whole part, we will add fraction when returning - if (data[1] & 0x80) result |= 0xF000; // fix negative value - retVal = float(result) + ((data[0] & 0x08) ? 0.5f : 0.0f); + // 12-bit precision - 4-bit fraction part + result = (data[1] << 8) | data[0]; + // Clear LSBs to match desired precision (9/10/11-bit) rounding towards negative infinity + result &= 0xFFFF << (3 - (resolution & 3)); + retVal = float(result) * 0.0625f; // 2^(-4) break; } } @@ -176,8 +71,8 @@ bool UsermodTemperature::findSensor() { if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) { switch (deviceAddress[0]) { case 0x10: // DS18S20 - case 0x22: // DS18B20 - case 0x28: // DS1822 + case 0x22: // DS1822 + case 0x28: // DS18B20 case 0x3B: // DS1825 case 0x42: // DS28EA00 DEBUG_PRINTLN(F("Sensor found.")); @@ -384,6 +279,7 @@ void UsermodTemperature::addToConfig(JsonObject &root) { top[FPSTR(_parasite)] = parasite; top[FPSTR(_parasitePin)] = parasitePin; top[FPSTR(_domoticzIDX)] = idx; + top[FPSTR(_resolution)] = resolution; DEBUG_PRINTLN(F("Temperature config saved.")); } @@ -411,6 +307,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { parasite = top[FPSTR(_parasite)] | parasite; parasitePin = top[FPSTR(_parasitePin)] | parasitePin; idx = top[FPSTR(_domoticzIDX)] | idx; + resolution = top[FPSTR(_resolution)] | resolution; if (!initDone) { // first run: reading from cfg.json @@ -431,7 +328,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { } } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_domoticzIDX)].isNull(); + return !top[FPSTR(_resolution)].isNull(); } void UsermodTemperature::appendConfigData() { @@ -439,6 +336,14 @@ void UsermodTemperature::appendConfigData() { oappend(F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); oappend(F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field + oappend(F("dd=addDD('")); oappend(String(FPSTR(_name)).c_str()); + oappend(F("','")); oappend(String(FPSTR(_resolution)).c_str()); oappend(F("');")); + oappend(F("addO(dd,'0.5 °C (9-bit)',0);")); + oappend(F("addO(dd,'0.25°C (10-bit)',1);")); + oappend(F("addO(dd,'0.125°C (11-bit)',2);")); + oappend(F("addO(dd,'0.0625°C (12-bit)',3);")); + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_resolution)).c_str()); + oappend(F("',1,'(ignored on DS18S20)');")); // 0 is field type, 1 is actual field } float UsermodTemperature::getTemperature() { @@ -458,16 +363,20 @@ const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s"; const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr"; const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin"; const char UsermodTemperature::_domoticzIDX[] PROGMEM = "domoticz-idx"; +const char UsermodTemperature::_resolution[] PROGMEM = "resolution"; const char UsermodTemperature::_sensor[] PROGMEM = "sensor"; const char UsermodTemperature::_temperature[] PROGMEM = "temperature"; const char UsermodTemperature::_Temperature[] PROGMEM = "/temperature"; const char UsermodTemperature::_data_fx[] PROGMEM = "Temperature@Min,Max;;!;01;pal=54,sx=255,ix=0"; -static uint16_t mode_temperature() { +static void mode_temperature() { float low = roundf(mapf((float)SEGMENT.speed, 0.f, 255.f, -150.f, 150.f)); // default: 15°C, range: -15°C to 15°C float high = roundf(mapf((float)SEGMENT.intensity, 0.f, 255.f, 300.f, 600.f)); // default: 30°C, range 30°C to 60°C float temp = constrain(UsermodTemperature::getInstance()->getTemperatureC()*10.f, low, high); // get a little better resolution (*10) unsigned i = map(roundf(temp), (unsigned)low, (unsigned)high, 0, 248); SEGMENT.fill(SEGMENT.color_from_palette(i, false, false, 255)); - return FRAMETIME; } + + +static UsermodTemperature temperature; +REGISTER_USERMOD(temperature); \ No newline at end of file diff --git a/usermods/Temperature/UsermodTemperature.h b/usermods/Temperature/UsermodTemperature.h new file mode 100644 index 0000000000..555b57cf7a --- /dev/null +++ b/usermods/Temperature/UsermodTemperature.h @@ -0,0 +1,110 @@ +#pragma once +#include "wled.h" +#include "OneWire.h" + +//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 + +// the frequency to check temperature, 1 minute +#ifndef USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL +#define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 +#endif + +class UsermodTemperature : public Usermod { + + private: + + 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; + int16_t idx = -1; // Domoticz virtual sensor idx + uint8_t resolution = 0; // 9bits=0, 10bits=1, 11bits=2, 12bits=3 + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _readInterval[]; + static const char _parasite[]; + static const char _parasitePin[]; + static const char _domoticzIDX[]; + static const char _resolution[]; + static const char _sensor[]; + static const char _temperature[]; + static const char _Temperature[]; + static const char _data_fx[]; + + //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 + + static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE); + + public: + + UsermodTemperature() { _instance = this; } + static UsermodTemperature *getInstance() { return UsermodTemperature::_instance; } + + /* + * 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() override { return USERMOD_ID_TEMPERATURE; } + + void setup() override; + void loop() override; + //void connected() override; +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) override; +#endif + //void onUpdateBegin(bool init) override; + + //bool handleButton(uint8_t b) override; + //void handleOverlayDraw() override; + + void addToJsonInfo(JsonObject& root) override; + //void addToJsonState(JsonObject &root) override; + //void readFromJsonState(JsonObject &root) override; + void addToConfig(JsonObject &root) override; + bool readFromConfig(JsonObject &root) override; + + void appendConfigData() override; +}; + diff --git a/usermods/Temperature/library.json b/usermods/Temperature/library.json new file mode 100644 index 0000000000..5439bc13e3 --- /dev/null +++ b/usermods/Temperature/library.json @@ -0,0 +1,7 @@ +{ + "name": "Temperature", + "build": { "libArchive": false}, + "dependencies": { + "paulstoffregen/OneWire":"~2.3.8" + } +} diff --git a/usermods/Temperature/platformio_override.ini b/usermods/Temperature/platformio_override.ini index cc86367fde..a53b5974d9 100644 --- a/usermods/Temperature/platformio_override.ini +++ b/usermods/Temperature/platformio_override.ini @@ -1,10 +1,5 @@ ; 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.8 \ No newline at end of file + diff --git a/usermods/Temperature/readme.md b/usermods/Temperature/readme.md index 3d65be7eb2..b09495feaa 100644 --- a/usermods/Temperature/readme.md +++ b/usermods/Temperature/readme.md @@ -11,11 +11,19 @@ Maintained by @blazoncek ## Installation -Copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. +Add `Temperature` to `custom_usermods` in your platformio_override.ini. + +Example **platformio_override.ini**: + +```ini +[env:usermod_temperature_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + Temperature +``` ### 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 Fahrenheit and measurement interval. @@ -25,43 +33,25 @@ All parameters can be configured at runtime via the Usermods settings page, incl * [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 - -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 -... -[platformio] -... -; default_envs = esp07 -default_envs = d1_mini -... -[common] -... -lib_deps = - ... - #For Dallas sensor uncomment following - paulstoffregen/OneWire @ ~2.3.8 -``` - ## Change Log -2020-09-12 +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 2024-09 + * Update OneWire to version 2.3.8, which includes stickbreaker's and garyd9's ESP32 fixes: - blazoncek's fork is no longer needed \ No newline at end of file + blazoncek's fork is no longer needed diff --git a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h b/usermods/TetrisAI_v2/TetrisAI_v2.cpp similarity index 98% rename from usermods/TetrisAI_v2/usermod_v2_tetrisai.h rename to usermods/TetrisAI_v2/TetrisAI_v2.cpp index 0f7039dac9..d4fefe1040 100644 --- a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h +++ b/usermods/TetrisAI_v2/TetrisAI_v2.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include "FX.h" #include "fcn_declare.h" @@ -100,13 +98,13 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) //////////////////////////// // 2D Tetris AI // //////////////////////////// -uint16_t mode_2DTetrisAI() +void mode_2DTetrisAI() { if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data))) { // not a 2D set-up SEGMENT.fill(SEGCOLOR(0)); - return 350; + return; } TetrisAI_data* tetrisai_data = reinterpret_cast(SEGENV.data); @@ -224,8 +222,6 @@ uint16_t mode_2DTetrisAI() { tetrisai_data->tetris.poll(); } - - return FRAMETIME; } // mode_2DTetrisAI() static const char _data_FX_MODE_2DTETRISAI[] PROGMEM = "Tetris AI@!,Look ahead,Intelligence,Rotate color,Mistake free,Show next,Border,Mistakes;Game Over,!,Border;!;2;sx=127,ix=64,c1=255,c2=0,c3=31,o1=1,o2=1,o3=0,pal=11"; @@ -250,3 +246,7 @@ class TetrisAIUsermod : public Usermod return USERMOD_ID_TETRISAI; } }; + + +static TetrisAIUsermod tetrisai_v2; +REGISTER_USERMOD(tetrisai_v2); \ No newline at end of file diff --git a/usermods/TetrisAI_v2/gridbw.h b/usermods/TetrisAI_v2/gridbw.h index deea027d79..a96351749a 100644 --- a/usermods/TetrisAI_v2/gridbw.h +++ b/usermods/TetrisAI_v2/gridbw.h @@ -13,7 +13,6 @@ #ifndef __GRIDBW_H__ #define __GRIDBW_H__ -#include #include #include "pieces.h" diff --git a/usermods/TetrisAI_v2/library.json b/usermods/TetrisAI_v2/library.json new file mode 100644 index 0000000000..54aa22d35b --- /dev/null +++ b/usermods/TetrisAI_v2/library.json @@ -0,0 +1,4 @@ +{ + "name": "TetrisAI_v2", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/TetrisAI_v2/pieces.h b/usermods/TetrisAI_v2/pieces.h index 5d461615ae..0a13704dcf 100644 --- a/usermods/TetrisAI_v2/pieces.h +++ b/usermods/TetrisAI_v2/pieces.h @@ -19,7 +19,6 @@ #include #include #include -#include #define numPieces 7 diff --git a/usermods/TetrisAI_v2/readme.md b/usermods/TetrisAI_v2/readme.md index b56f801a87..5ac8028967 100644 --- a/usermods/TetrisAI_v2/readme.md +++ b/usermods/TetrisAI_v2/readme.md @@ -6,7 +6,7 @@ Version 1.0 ## Installation -Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/Aircoookie/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). +Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/wled-dev/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). If needed simply add to `platformio_override.ini` (or `platformio_override.ini`): diff --git a/usermods/TetrisAI_v2/tetrisbag.h b/usermods/TetrisAI_v2/tetrisbag.h index 592dac6c7f..b1698d8143 100644 --- a/usermods/TetrisAI_v2/tetrisbag.h +++ b/usermods/TetrisAI_v2/tetrisbag.h @@ -15,7 +15,6 @@ #include #include -#include #include "tetrisbag.h" @@ -87,17 +86,12 @@ class TetrisBag void queuePiece() { //move vector to left - std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end()); + for (uint8_t i = 1; i < piecesQueue.size(); i++) { + piecesQueue[i - 1] = piecesQueue[i]; + } piecesQueue[piecesQueue.size() - 1] = getNextPiece(); } - void queuePiece(uint8_t idx) - { - //move vector to left - std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end()); - piecesQueue[piecesQueue.size() - 1] = Piece(idx % nPieces); - } - void reset() { bag.clear(); diff --git a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h b/usermods/VL53L0X_gestures/VL53L0X_gestures.cpp similarity index 98% rename from usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h rename to usermods/VL53L0X_gestures/VL53L0X_gestures.cpp index fe6b958f55..af3220b3c0 100644 --- a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h +++ b/usermods/VL53L0X_gestures/VL53L0X_gestures.cpp @@ -13,8 +13,6 @@ * lib_deps = ${env.lib_deps} * pololu/VL53L0X @ ^1.3.0 */ -#pragma once - #include "wled.h" #include @@ -126,4 +124,7 @@ class UsermodVL53L0XGestures : public Usermod { { return USERMOD_ID_VL53L0X; } -}; \ No newline at end of file +}; + +static UsermodVL53L0XGestures vl53l0x_gestures; +REGISTER_USERMOD(vl53l0x_gestures); \ No newline at end of file diff --git a/usermods/VL53L0X_gestures/library.json b/usermods/VL53L0X_gestures/library.json new file mode 100644 index 0000000000..08f5921c76 --- /dev/null +++ b/usermods/VL53L0X_gestures/library.json @@ -0,0 +1,7 @@ +{ + "name": "VL53L0X_gestures", + "build": { "libArchive": false}, + "dependencies": { + "pololu/VL53L0X" : "^1.3.0" + } +} diff --git a/usermods/VL53L0X_gestures/readme.md b/usermods/VL53L0X_gestures/readme.md index a230b1a655..04c4a4aa58 100644 --- a/usermods/VL53L0X_gestures/readme.md +++ b/usermods/VL53L0X_gestures/readme.md @@ -10,20 +10,4 @@ Useful for controlling strips when you want to avoid touching anything. 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/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.cpp similarity index 97% rename from usermods/audioreactive/audio_reactive.h rename to usermods/audioreactive/audio_reactive.cpp index ad449fc83a..7baa796894 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.cpp @@ -1,4 +1,3 @@ -#pragma once #include "wled.h" @@ -7,10 +6,6 @@ #include #include -#ifdef WLED_ENABLE_DMX - #error This audio reactive usermod is not compatible with DMX Out. -#endif - #endif #if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) @@ -19,7 +14,7 @@ /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is an audioreactive v2 usermod. * .... @@ -66,16 +61,19 @@ static bool udpSyncConnected = false; // UDP connection status -> true i // audioreactive variables #ifdef ARDUINO_ARCH_ESP32 + #ifndef SR_AGC // Automatic gain control mode + #define SR_AGC 0 // default mode = off + #endif 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 -static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +static uint8_t soundAgc = SR_AGC; // Automatic gain control: 0 - off, 1 - normal, 2 - vivid, 3 - lazy (config value) #endif //static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() +static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getFrameTime() static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData static unsigned long timeOfPeak = 0; // time of last sample peak detection. static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects @@ -222,8 +220,8 @@ void FFTcode(void * parameter) DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); // allocate FFT buffers on first call - if (vReal == nullptr) vReal = (float*) calloc(sizeof(float), samplesFFT); - if (vImag == nullptr) vImag = (float*) calloc(sizeof(float), samplesFFT); + if (vReal == nullptr) vReal = (float*) calloc(samplesFFT, sizeof(float)); + if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float)); if ((vReal == nullptr) || (vImag == nullptr)) { // something went wrong if (vReal) free(vReal); vReal = nullptr; @@ -536,8 +534,8 @@ static void detectSamplePeak(void) { #endif 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. + uint16_t peakDelay = max(uint16_t(50), strip.getFrameTime()); + if (millis() - timeOfPeak > peakDelay) { // Auto-reset of samplePeak after at least one complete frame has passed. samplePeak = false; if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData } @@ -870,7 +868,7 @@ class AudioReactive : public Usermod { 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 + micIn = perlin8(millis(), millis()); // Simulated analog read micDataReal = micIn; #else #ifdef ARDUINO_ARCH_ESP32 @@ -1225,7 +1223,6 @@ class AudioReactive : public Usermod { #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // ADC over I2S is only possible on "classic" ESP32 case 0: - default: DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); delay(100); @@ -1233,10 +1230,25 @@ class AudioReactive : public Usermod { if (audioSource) audioSource->initialize(audioPin); break; #endif + + case 254: // dummy "network receive only" mode + if (audioSource) delete audioSource; audioSource = nullptr; + disableSoundProcessing = true; + audioSyncEnabled = 2; // force udp sound receive mode + enabled = true; + break; + + case 255: // 255 = -1 = no audio source + // falls through to default + default: + if (audioSource) delete audioSource; audioSource = nullptr; + disableSoundProcessing = true; + enabled = false; + break; } delay(250); // give microphone enough time to initialise - if (!audioSource) enabled = false; // audio failed to initialise + if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise #endif if (enabled) onUpdateBegin(false); // create FFT task, and initialize network @@ -1319,7 +1331,7 @@ class AudioReactive : public Usermod { disableSoundProcessing = true; } else { #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" + if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && audioSource->isInitialized()) { // we just switched to "enabled" DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); } @@ -1331,7 +1343,7 @@ class AudioReactive : public Usermod { if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode #ifdef ARDUINO_ARCH_ESP32 - if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source + if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true; // no audio source // Only run the sampling code IF we're not in Receive mode or realtime mode @@ -1528,7 +1540,7 @@ class AudioReactive : public Usermod { // better would be for AudioSource to implement getType() if (enabled && dmType == 0 && audioPin>=0 - && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) + && (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) ) { return true; } @@ -1730,14 +1742,14 @@ class AudioReactive : public Usermod { } #endif } - if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { + if (palettes > 0 && root.containsKey(F("rmcpal"))) { // handle removal of custom palettes from JSON call so we don't break things removeAudioPalettes(); } } void onStateChange(uint8_t callMode) override { - if (initDone && enabled && addPalettes && palettes==0 && strip.customPalettes.size()<10) { + if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()0) { - strip.customPalettes.pop_back(); + customPalettes.pop_back(); DEBUG_PRINTLN(palettes); palettes--; } - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(strip.customPalettes.size()); + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); } void AudioReactive::createAudioPalettes(void) { - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(strip.customPalettes.size()); + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); if (palettes) return; DEBUG_PRINTLN(F("Adding audio palettes.")); for (int i=0; i= palettes) lastCustPalette -= palettes; for (int pal=0; pal= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 6)) +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 5, 0)) // espressif bug: only_left has no sound, left and right are swapped // https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138) // https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918) diff --git a/usermods/audioreactive/library.json b/usermods/audioreactive/library.json new file mode 100644 index 0000000000..fb2254a0ed --- /dev/null +++ b/usermods/audioreactive/library.json @@ -0,0 +1,15 @@ +{ + "name": "audioreactive", + "build": { + "libArchive": false, + "extraScript": "override_sqrt.py" + }, + "dependencies": [ + { + "owner": "kosme", + "name": "arduinoFFT", + "version": "2.0.1", + "platforms": "espressif32" + } + ] +} diff --git a/usermods/audioreactive/override_sqrt.py b/usermods/audioreactive/override_sqrt.py new file mode 100644 index 0000000000..36aa79df4b --- /dev/null +++ b/usermods/audioreactive/override_sqrt.py @@ -0,0 +1,5 @@ +Import('env') + +for lb in env.GetLibBuilders(): + if lb.name == "arduinoFFT": + lb.env.Append(CPPDEFINES=[("sqrt_internal", "sqrtf")]) diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index aad269c675..5ee575ffff 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -8,30 +8,29 @@ Does audio processing and provides data structure that specially written effects **does not** provide effects or draw anything to an LED strip/matrix. ## Additional Documentation + This usermod is an evolution of [SR-WLED](https://github.com/atuline/WLED), and a lot of documentation and information can be found in the [SR-WLED wiki](https://github.com/atuline/WLED/wiki): + * [getting started with audio](https://github.com/atuline/WLED/wiki/First-Time-Setup#sound) * [Sound settings](https://github.com/atuline/WLED/wiki/Sound-Settings) - similar to options on the usemod settings page in WLED. * [Digital Audio](https://github.com/atuline/WLED/wiki/Digital-Microphone-Hookup) * [Analog Audio](https://github.com/atuline/WLED/wiki/Analog-Audio-Input-Options) * [UDP Sound sync](https://github.com/atuline/WLED/wiki/UDP-Sound-Sync) - ## Supported MCUs -This audioreactive usermod works best on "classic ESP32" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support. -It will compile successfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. +This audioreactive usermod works best on "classic ESP32" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support. + +It will compile successfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. Analog audio is only possible on "classic" ESP32, but not on other MCUs like ESP32-S3. -Currently ESP8266 is not supported, due to low speed and small RAM of this chip. +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 latest _arduinoFFT_ library version 2.x -The latest arduinoFFT release version should be used for audioreactive. +## Installation -* `build_flags` = `-D USERMOD_AUDIOREACTIVE -D sqrt_internal=sqrtf` -* `lib_deps`= `kosme/arduinoFFT @ 2.0.1` +Add 'ADS1115_v2' to `custom_usermods` in your platformio environment. ## Configuration @@ -39,29 +38,32 @@ All parameters are runtime configurable. Some may require a hard reset after cha 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) +* `-D SR_DMTYPE=x` : defines digital microphone type: 0=analog, 1=generic I2S (default), 2=ES7243 I2S, 3=SPH0645 I2S, 4=generic I2S with master clock, 5=PDM I2S +* `-D AUDIOPIN=x` : GPIO for analog microphone/AUX-in (36) +* `-D I2S_SDPIN=x` : GPIO for SD pin on digital microphone (32) +* `-D I2S_WSPIN=x` : GPIO for WS pin on digital microphone (15) +* `-D I2S_CKPIN=x` : GPIO for SCK pin on digital microphone (14) +* `-D MCLK_PIN=x` : GPIO for master clock pin on digital Line-In boards (-1) +* `-D ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1) +* `-D ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1) Other options: -- `-D UM_AUDIOREACTIVE_ENABLE` : makes usermod default enabled (not the same as include into build option!) -- `-D UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF` : disables rise/fall limiter default +* `-D UM_AUDIOREACTIVE_ENABLE` : makes usermod default enabled (not the same as include into build option!) +* `-D UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF` : disables rise/fall limiter default **NOTE** I2S is used for analog audio sampling. Hence, the analog *buttons* (i.e. potentiometers) are disabled when running this usermod with an analog microphone. ### Advanced Compile-Time Options + You can use the following additional flags in your `build_flags` + * `-D SR_SQUELCH=x` : Default "squelch" setting (10) * `-D SR_GAIN=x` : Default "gain" setting (60) +* `-D SR_AGC=x` : (Only ESP32) Default "AGC (Automatic Gain Control)" setting (0): 0=off, 1=normal, 2=vivid, 3=lazy * `-D I2S_USE_RIGHT_CHANNEL`: Use RIGHT instead of LEFT channel (not recommended unless you strictly need this). -* `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM ressources (not recommended unless you absolutely need this). -* `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continuously sample analog ADC microphone. Only effective on ESP32. WARNING this _will_ cause conflicts(lock-up) with any analogRead() call. +* `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM resources (not recommended unless you absolutely need this). +* `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continuously sample analog ADC microphone. Only effective on ESP32. WARNING this *will* cause conflicts(lock-up) with any analogRead() call. * `-D MIC_LOGGER` : (debugging) Logs samples from the microphone to serial USB. Use with serial plotter (Arduino IDE) * `-D SR_DEBUG` : (debugging) Additional error diagnostics and debug info on serial USB. diff --git a/usermods/boblight/boblight.h b/usermods/boblight/boblight.cpp similarity index 99% rename from usermods/boblight/boblight.h rename to usermods/boblight/boblight.cpp index b04b78fac7..5980443d37 100644 --- a/usermods/boblight/boblight.h +++ b/usermods/boblight/boblight.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" /* @@ -457,3 +455,7 @@ void BobLightUsermod::pollBob() { } } } + + +static BobLightUsermod boblight; +REGISTER_USERMOD(boblight); \ No newline at end of file diff --git a/usermods/boblight/library.json b/usermods/boblight/library.json new file mode 100644 index 0000000000..b54fb35058 --- /dev/null +++ b/usermods/boblight/library.json @@ -0,0 +1,4 @@ +{ + "name": "boblight", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/boblight/readme.md b/usermods/boblight/readme.md index 3458300933..c476345743 100644 --- a/usermods/boblight/readme.md +++ b/usermods/boblight/readme.md @@ -20,8 +20,7 @@ The LEDs should be wired in a clockwise orientation starting in the middle of bo ## 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*. +Add `boblight` to `custom_usermods` in your PlatformIO environment. ## Configuration diff --git a/usermods/buzzer/usermod_v2_buzzer.h b/usermods/buzzer/buzzer.cpp similarity index 85% rename from usermods/buzzer/usermod_v2_buzzer.h rename to usermods/buzzer/buzzer.cpp index ebd8dcb15e..9d62fc20c8 100644 --- a/usermods/buzzer/usermod_v2_buzzer.h +++ b/usermods/buzzer/buzzer.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include "Arduino.h" @@ -7,16 +5,17 @@ #define USERMOD_ID_BUZZER 900 #ifndef USERMOD_BUZZER_PIN +#ifdef GPIO_NUM_32 #define USERMOD_BUZZER_PIN GPIO_NUM_32 +#else +#define USERMOD_BUZZER_PIN 21 +#endif #endif /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/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 { @@ -78,4 +77,7 @@ class BuzzerUsermod : public Usermod { { return USERMOD_ID_BUZZER; } -}; \ No newline at end of file +}; + +static BuzzerUsermod buzzer; +REGISTER_USERMOD(buzzer); \ No newline at end of file diff --git a/usermods/buzzer/library.json b/usermods/buzzer/library.json new file mode 100644 index 0000000000..0dbb547e34 --- /dev/null +++ b/usermods/buzzer/library.json @@ -0,0 +1,4 @@ +{ + "name": "buzzer", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/deep_sleep/deep_sleep.cpp b/usermods/deep_sleep/deep_sleep.cpp new file mode 100644 index 0000000000..cff40f86de --- /dev/null +++ b/usermods/deep_sleep/deep_sleep.cpp @@ -0,0 +1,346 @@ +#include "wled.h" +#include "driver/rtc_io.h" +#ifndef CONFIG_IDF_TARGET_ESP32C3 + #include "soc/touch_sensor_periph.h" +#endif +#ifdef ESP8266 +#error The "Deep Sleep" usermod does not support ESP8266 +#endif + +#ifndef DEEPSLEEP_WAKEUPPIN +#define DEEPSLEEP_WAKEUPPIN 0 +#endif +#ifndef DEEPSLEEP_WAKEWHENHIGH +#define DEEPSLEEP_WAKEWHENHIGH 0 +#endif +#ifndef DEEPSLEEP_DISABLEPULL +#define DEEPSLEEP_DISABLEPULL 1 +#endif +#ifndef DEEPSLEEP_WAKEUPINTERVAL +#define DEEPSLEEP_WAKEUPINTERVAL 0 +#endif +#ifndef DEEPSLEEP_DELAY +#define DEEPSLEEP_DELAY 1 +#endif + +#ifndef DEEPSLEEP_WAKEUP_TOUCH_PIN +#define DEEPSLEEP_WAKEUP_TOUCH_PIN 1 +#endif +RTC_DATA_ATTR bool powerup = true; // this is first boot after power cycle. note: variable in RTC data persists on a reboot +RTC_DATA_ATTR uint8_t wakeupPreset = 0; // preset to apply after deep sleep wakeup (0 = none), set to timer macro preset + +class DeepSleepUsermod : public Usermod { + + private: + bool enabled = false; // do not enable by default + bool initDone = false; + uint8_t wakeupPin = DEEPSLEEP_WAKEUPPIN; + uint8_t wakeWhenHigh = DEEPSLEEP_WAKEWHENHIGH; // wake up when pin goes high if 1, triggers on low if 0 + bool noPull = true; // use pullup/pulldown resistor + bool enableTouchWakeup = false; + uint8_t touchPin = DEEPSLEEP_WAKEUP_TOUCH_PIN; + int wakeupAfter = DEEPSLEEP_WAKEUPINTERVAL; // in seconds, <=0: button only + bool presetWake = true; // wakeup timer for preset + int sleepDelay = DEEPSLEEP_DELAY; // in seconds, 0 = immediate + int delaycounter = 10; // delay deep sleep at bootup until preset settings are applied, force wake up if offmode persists after bootup + uint32_t lastLoopTime = 0; + + // string that are used multiple time (this will save some flash memory) + static const char _name[]; + static const char _enabled[]; + + bool pin_is_valid(uint8_t wakePin) { + #ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up. note: input-only GPIOs 34-39 do not have internal pull resistors + if (wakePin == 0 || wakePin == 2 || wakePin == 4 || (wakePin >= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) { + return true; + } + #endif + #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2) //ESP32 S3 & S3: GPIOs 0-21 can be used for wake-up + if (wakePin <= 21) { + return true; + } + #endif + #ifdef CONFIG_IDF_TARGET_ESP32C3 // ESP32 C3: GPIOs 0-5 can be used for wake-up + if (wakePin <= 5) { + return true; + } + #endif + DEBUG_PRINTLN(F("Error: unsupported deep sleep wake-up pin")); + return false; + } + + // functions to calculate time difference between now and next scheduled timer event + int calculateTimeDifference(int hour1, int minute1, int hour2, int minute2) { + int totalMinutes1 = hour1 * 60 + minute1; + int totalMinutes2 = hour2 * 60 + minute2; + if (totalMinutes2 < totalMinutes1) { + totalMinutes2 += 24 * 60; + } + return totalMinutes2 - totalMinutes1; + } + + int findNextTimerInterval() { + if (toki.getTimeSource() == TOKI_TS_NONE) { + DEBUG_PRINTLN("DeepSleep: local time not yet synchronized, skipping timer check."); + return -1; + } + int currentHour = hour(localTime); + int currentMinute = minute(localTime); + int currentWeekday = weekdayMondayFirst(); // 1=Monday ... 7=Sunday + int minDifference = INT_MAX; + + for (uint8_t i = 0; i < 8; i++) { + // check if timer is enabled and date is in range, also wakes up if no macro is used + if ((timerWeekday[i] & 0x01) && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i])) { + + // if timer is enabled (bit0 of timerWeekday) and date is in range, check all weekdays it is set for + for (int dayOffset = 0; dayOffset < 7; dayOffset++) { + int checkWeekday = ((currentWeekday + dayOffset) % 7); // 1-7, check all weekdays starting from today + if (checkWeekday == 0) { + checkWeekday = 7; // sunday is 7 not 0 + } + + int targetHour = timerHours[i]; + int targetMinute = timerMinutes[i]; + if ((timerWeekday[i] >> (checkWeekday)) & 0x01) { + if (dayOffset == 0 && (targetHour < currentHour || (targetHour == currentHour && targetMinute <= currentMinute))) + continue; // skip if time has already passed today + + int timeDifference = calculateTimeDifference(currentHour, currentMinute, targetHour + (dayOffset * 24), targetMinute); + if (timeDifference < minDifference) { + minDifference = timeDifference; + wakeupPreset = timerMacro[i]; + } + } + } + } + } + return minDifference; + } + + public: + inline void enable(bool enable) { enabled = enable; } // Enable/Disable the usermod + inline bool isEnabled() { return enabled; } //Get usermod enabled/disabled state + + // setup is called at boot (or in this case after every exit of sleep mode) + void setup() { + //TODO: if the de-init of RTC pins is required to do it could be done here + //rtc_gpio_deinit(wakeupPin); + #ifdef WLED_DEBUG + DEBUG_PRINTF("sleep wakeup cause: %d\n", esp_sleep_get_wakeup_cause()); + #endif + if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_TIMER) + wakeupPreset = 0; // not a timed wakeup, don't apply preset + initDone = true; + } + + void loop() { + if (!enabled) return; + if (!offMode) { // LEDs are on + lastLoopTime = 0; // reset timer + if (delaycounter) + delaycounter--; // decrease delay counter if LEDs are on (they are always turned on after a wake-up, see below) + else if (wakeupPreset) + applyPreset(wakeupPreset); // apply preset if set, this ensures macro is applied even if we missed the wake-up time + return; + } + + if (sleepDelay > 0) { + powerup = false; // disable "safety" powerup sleep if delay is set + if (lastLoopTime == 0) + lastLoopTime = millis(); // initialize + if (millis() - lastLoopTime < sleepDelay * 1000) + return; // wait until delay is over + } else if (powerup && delaycounter) { + delaycounter--; // on first boot without sleepDelay set, do not force-turn on + delay(1000); // just in case: give user a short ~10s window to turn LEDs on in UI (delaycounter is 10 by default) + return; + } + if (powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (beginStrip() / handleIO() does enable offMode temporarily in this case) + delaycounter--; + if (delaycounter == 1 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false) + if (briS == 0) bri = 10; // turn on and set low brightness to avoid automatic turn off + else bri = briS; + strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset) + offMode = false; + applyPresetWithFallback(0, CALL_MODE_INIT, FX_MODE_STATIC, 0); // try to apply preset 0, fallback to static + if (rlyPin >= 0) { + digitalWrite(rlyPin, (rlyMde ? HIGH : LOW)); // turn relay on TODO: this should be done by wled, what function to call? + } + } + return; + } + DEBUG_PRINTLN(F("DeepSleep UM: entering deep sleep...")); + powerup = false; // turn leds on in all subsequent bootups (overrides Turn LEDs on after power up/reset' at reboot) + if (!pin_is_valid(wakeupPin)) return; + esp_err_t halerror = ESP_OK; + pinMode(wakeupPin, INPUT); // make sure GPIO is input with pullup/pulldown disabled + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); //disable all wake-up sources (just in case) + + uint32_t wakeupAfterSec = 0; + if (presetWake) { + int nextInterval = findNextTimerInterval(); + if (nextInterval > 1 && nextInterval < INT_MAX) + wakeupAfterSec = (nextInterval - 1) * 60; // wakeup before next preset + } + if (wakeupAfter > 0) { // user-defined interval + if (wakeupAfterSec == 0 || (uint32_t)wakeupAfter < wakeupAfterSec) { + wakeupAfterSec = wakeupAfter; + } + } + if (wakeupAfterSec > 0) { + esp_sleep_enable_timer_wakeup(wakeupAfterSec * (uint64_t)1e6); + DEBUG_PRINTF("wakeup after %d seconds\n", wakeupAfterSec); + } + + #if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3 + gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold and configure pin + if (wakeWhenHigh) + halerror = esp_deep_sleep_enable_gpio_wakeup(1<= 0) { + oappend(SET_F("addOption(dd,'")); + oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str()); + oappend(SET_F("',")); + oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str()); + oappend(SET_F(");")); + } + } + #endif + + oappend(SET_F("dd=addDropdown('DeepSleep','wakeWhen');")); + oappend(SET_F("addOption(dd,'Low',0);")); + oappend(SET_F("addOption(dd,'High',1);")); + + oappend(SET_F("addInfo('DeepSleep:pull',1,'','-up/down disable: ');")); // first string is suffix, second string is prefix + oappend(SET_F("addInfo('DeepSleep:wakeAfter',1,'seconds (0 = never)');")); + oappend(SET_F("addInfo('DeepSleep:presetWake',1,'(wake up before next preset timer)');")); + oappend(SET_F("addInfo('DeepSleep:delaySleep',1,'seconds (0 = sleep at powerup)');")); // first string is suffix, second string is prefix + } + + /* + * 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_DEEP_SLEEP; + } +}; + +// add more strings here to reduce flash memory usage +const char DeepSleepUsermod::_name[] PROGMEM = "DeepSleep"; +const char DeepSleepUsermod::_enabled[] PROGMEM = "enabled"; + +static DeepSleepUsermod deep_sleep; +REGISTER_USERMOD(deep_sleep); \ No newline at end of file diff --git a/usermods/deep_sleep/library.json b/usermods/deep_sleep/library.json new file mode 100644 index 0000000000..82e32c9947 --- /dev/null +++ b/usermods/deep_sleep/library.json @@ -0,0 +1,4 @@ +{ + "name": "deep_sleep", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/deep_sleep/readme.md b/usermods/deep_sleep/readme.md new file mode 100644 index 0000000000..b88e3514a1 --- /dev/null +++ b/usermods/deep_sleep/readme.md @@ -0,0 +1,85 @@ +# Deep Sleep usermod + +This usermod unleashes the low power capabilities of th ESP: when you power off your LEDs (using the UI power button or a macro) the ESP will be put into deep sleep mode, reducing power consumption to a minimum. +During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is by using an external signal, a button, or an automation timer set to the nearest preset time. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.*** + +# A word of warning + +When you disable the WLED option 'Turn LEDs on after power up/reset' and 'DelaySleep' is set to zero the ESP will go into deep sleep directly after power-up and only start WLED after it has been woken up. +If the ESP can not be awoken from deep sleep due to a wrong configuration it has to be factory reset, disabling sleep at power-up. There is no other way to wake it up. + +# Power Consumption in deep sleep + +The current drawn by the ESP in deep sleep mode depends on the type and is in the range of 5uA-20uA (as in micro Amperes): +- ESP32: 10uA +- ESP32 S3: 8uA +- ESP32 S2: 20uA +- ESP32 C3: 5uA +- ESP8266: 20uA (not supported in this usermod) + +However, there is usually additional components on a controller that increase the value: +- Power LED: the power LED on a ESP board draws 500uA - 1mA +- LDO: the voltage regulator also draws idle current. Depending on the type used this can be around 50uA up to 10mA (LM1117). Special low power LDOs with very low idle currents do exist +- Digital LEDs: WS2812 for example draw a current of about 1mA per LED. To make good use of this usermod it is required to power them off using MOSFETs or a Relay + +For lowest power consumption, remove the Power LED and make sure your board does not use an LM1117. On a ESP32 C3 Supermini with the power LED removed (no other modifications) powered through the 5V pin I measured a current draw of 50uA in deep sleep. + +# Useable GPIOs + +The GPIOs that can be used to wake the ESP from deep sleep are limited. Only pins connected to the internal RTC unit can be used: + +- ESP32: GPIO 0, 2, 4, 12-15, 25-39 note: input-only GPIOs 34-39 do not have internal pull up/down resistors, external resistors are required +- ESP32 S3: GPIO 0-21 +- ESP32 S2: GPIO 0-21 +- ESP32 C3: GPIO 0-5 +- ESP8266 is not supported in this usermod + +You can however use the selected wake-up pin normally in WLED, it only gets activated as a wake-up pin when your LEDs are powered down. + +# Limitations + +To keep this usermod simple and easy to use, it is a very basic implementation of the low-power capabilities provided by the ESP. If you need more advanced control you are welcome to implement your own version based on this usermod. + +## Usermod installation + +Add `deep_sleep` to `custom_usermods` in your platformio.ini. Settings can be changed in the usermod config UI. + +### Define Settings + +There are five parameters you can set: + +- GPIO: the pin to use for wake-up +- WakeWhen High/Low: the pin state that triggers the wake-up +- Pull-up/down disable: enable or disable the internal pullup resistors during sleep (does not affect normal use while running) +- Wake after: if set larger than 0, ESP will automatically wake-up after this many seconds (Turn LEDs on after power up/reset is overriden, it will always turn on) +- Delay sleep: if set larger than 0, ESP will not go to sleep for this many seconds after you power it off. Timer is reset when switched back on during this time. + +To override the default settings, place the `#define` in wled.h or add `-D DEEPSLEEP_xxx` to your platformio_override.ini build flags + +* `DEEPSLEEP_WAKEUPPIN x` - define the pin to be used for wake-up, see list of useable pins above. The pin can be used normally as a button pin in WLED. +* `DEEPSLEEP_WAKEWHENHIGH` - if defined, wakes up when pin goes high (default is low) +* `DEEPSLEEP_DISABLEPULL` - if defined, internal pullup/pulldown is disabled in deep sleep (default is ebnabled) +* `DEEPSLEEP_WAKEUPINTERVAL` - number of seconds after which a wake-up happens automatically, sooner if button is pressed. 0 = never. accuracy is about 2% +* `DEEPSLEEP_DELAY` - delay between power-off and sleep +* `DEEPSLEEP_WAKEUP_TOUCH_PIN` - specify GPIO pin used for touch-based wakeup + +example for env build flags: + `-D USERMOD_DEEP_SLEEP` + `-D DEEPSLEEP_WAKEUPPIN=4` + `-D DEEPSLEEP_DISABLEPULL=0` ;enable pull-up/down resistors by default + `-D DEEPSLEEP_WAKEUPINTERVAL=43200` ;wake up after 12 hours (or when button is pressed) + +### Hardware Setup + +To wake from deep-sleep an external trigger signal on the configured GPIO is required. When using timed-only wake-up, use a GPIO that has an on-board pull-up resistor (GPIO0 on most boards). When using push-buttons it is highly recommended to use an external pull-up resistor: not all IO's on all devices have properly working internal resistors. + +Using sensors like PIR, IR, touch sensors or any other sensor with a digital output can be used instead of a button. + +now go on and save some power +@dedehai + +## Change log +2024-09 +* Initial version +2024-10 +* Changed from #define configuration to UI configuration \ No newline at end of file diff --git a/usermods/mpu6050_imu/library.json b/usermods/mpu6050_imu/library.json new file mode 100644 index 0000000000..d2a0f70af3 --- /dev/null +++ b/usermods/mpu6050_imu/library.json @@ -0,0 +1,7 @@ +{ + "name": "mpu6050_imu", + "build": { "libArchive": false}, + "dependencies": { + "electroniccats/MPU6050":"1.0.1" + } +} diff --git a/usermods/mpu6050_imu/usermod_mpu6050_imu.h b/usermods/mpu6050_imu/mpu6050_imu.cpp similarity index 99% rename from usermods/mpu6050_imu/usermod_mpu6050_imu.h rename to usermods/mpu6050_imu/mpu6050_imu.cpp index f04578fe3a..6df5d64e12 100644 --- a/usermods/mpu6050_imu/usermod_mpu6050_imu.h +++ b/usermods/mpu6050_imu/mpu6050_imu.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" /* This driver reads quaternion data from the MPU6060 and adds it to the JSON @@ -105,7 +103,7 @@ class MPU6050Driver : public Usermod { VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements VectorInt16 aaWorld; // [x, y, z] world-frame accel sensor measurements VectorFloat gravity; // [x, y, z] gravity vector - uint32 sample_count; + uint32_t sample_count; // Usermod output um_data_t um_data; @@ -446,3 +444,7 @@ const char MPU6050Driver::_z_acc_bias[] PROGMEM = "z_acc_bias"; const char MPU6050Driver::_x_gyro_bias[] PROGMEM = "x_gyro_bias"; const char MPU6050Driver::_y_gyro_bias[] PROGMEM = "y_gyro_bias"; const char MPU6050Driver::_z_gyro_bias[] PROGMEM = "z_gyro_bias"; + + +static MPU6050Driver mpu6050_imu; +REGISTER_USERMOD(mpu6050_imu); \ No newline at end of file diff --git a/usermods/mpu6050_imu/readme.md b/usermods/mpu6050_imu/readme.md index c324738d6b..87c4391313 100644 --- a/usermods/mpu6050_imu/readme.md +++ b/usermods/mpu6050_imu/readme.md @@ -16,26 +16,6 @@ As a memento to a long trip I was on, I built an icosahedron globe. I put lights I wanted to integrate an IMU to allow either on-board, or off-board effects that would react to the globes orientation. See the blog post on building it or a video demo . -## Adding Dependencies - -I2Cdev and MPU6050 must be installed. - -To install them, add electroniccats/MPU6050@1.0.1 to lib_deps in the platformio.ini file. - -For example: - -``` -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 - electroniccats/MPU6050@1.0.1 -``` - ## Wiring The connections needed to the MPU6050 are as follows: @@ -74,18 +54,13 @@ 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"` in the top and `registerUsermod(new MPU6050Driver());` in the bottom of `usermods_list.cpp`. - -Example **usermods_list.cpp**: +Add `mpu6050_imu` to `custom_usermods` in your platformio_override.ini. -```cpp -#include "wled.h" +Example **platformio_override.ini**: -#include "usermod_mpu6050_imu.h" - -void registerUsermods() -{ - UsermodManager::add(new MPU6050Driver()); -} +```ini +[env:usermod_mpu6050_imu_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + mpu6050_imu ``` diff --git a/usermods/mqtt_switch_v2/README.md b/usermods/mqtt_switch_v2/README.md deleted file mode 100644 index 382f72d0e8..0000000000 --- a/usermods/mqtt_switch_v2/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# 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() -{ - UsermodManager::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 DATA_PINS=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 shouldn'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 deleted file mode 100644 index 67dfc9cc08..0000000000 --- a/usermods/mqtt_switch_v2/usermod_mqtt_switch.h +++ /dev/null @@ -1,159 +0,0 @@ -#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/library.json b/usermods/multi_relay/library.json new file mode 100644 index 0000000000..a5e5c6934b --- /dev/null +++ b/usermods/multi_relay/library.json @@ -0,0 +1,4 @@ +{ + "name": "multi_relay", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/multi_relay.cpp similarity index 94% rename from usermods/multi_relay/usermod_multi_relay.h rename to usermods/multi_relay/multi_relay.cpp index c4446c7a20..4cbdb2fe39 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/multi_relay.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x]))))) @@ -564,11 +562,11 @@ void MultiRelay::loop() { bool MultiRelay::handleButton(uint8_t b) { yield(); if (!enabled - || buttonType[b] == BTN_TYPE_NONE - || buttonType[b] == BTN_TYPE_RESERVED - || buttonType[b] == BTN_TYPE_PIR_SENSOR - || buttonType[b] == BTN_TYPE_ANALOG - || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { + || buttons[b].type == BTN_TYPE_NONE + || buttons[b].type == BTN_TYPE_RESERVED + || buttons[b].type == BTN_TYPE_PIR_SENSOR + || buttons[b].type == BTN_TYPE_ANALOG + || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { return false; } @@ -583,20 +581,20 @@ bool MultiRelay::handleButton(uint8_t b) { unsigned long now = millis(); //button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0) - if (buttonType[b] == BTN_TYPE_SWITCH) { + if (buttons[b].type == BTN_TYPE_SWITCH) { //handleSwitch(b); - if (buttonPressedBefore[b] != isButtonPressed(b)) { - buttonPressedTime[b] = now; - buttonPressedBefore[b] = !buttonPressedBefore[b]; + if (buttons[b].pressedBefore != isButtonPressed(b)) { + buttons[b].pressedTime = now; + buttons[b].pressedBefore = !buttons[b].pressedBefore; } - if (buttonLongPressed[b] == buttonPressedBefore[b]) return handled; + if (buttons[b].longPressed == buttons[b].pressedBefore) return handled; - if (now - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) + if (now - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) for (int i=0; i 600) { //long press + if (now - buttons[b].pressedTime > 600) { //long press //longPressAction(b); //not exposed //handled = false; //use if you want to pass to default behaviour - buttonLongPressed[b] = true; + buttons[b].longPressed = true; } - } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + } else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released - long dur = now - buttonPressedTime[b]; + long dur = now - buttons[b].pressedTime; if (dur < WLED_DEBOUNCE_THRESHOLD) { - buttonPressedBefore[b] = false; + buttons[b].pressedBefore = false; return handled; } //too short "press", debounce - bool doublePress = buttonWaitTime[b]; //did we have short press before? - buttonWaitTime[b] = 0; + bool doublePress = buttons[b].waitTime; //did we have short press before? + buttons[b].waitTime = 0; - if (!buttonLongPressed[b]) { //short press + if (!buttons[b].longPressed) { //short press // if this is second release within 350ms it is a double press (buttonWaitTime!=0) if (doublePress) { //doublePressAction(b); //not exposed //handled = false; //use if you want to pass to default behaviour } else { - buttonWaitTime[b] = now; + buttons[b].waitTime = now; } } - buttonPressedBefore[b] = false; - buttonLongPressed[b] = false; + buttons[b].pressedBefore = false; + buttons[b].longPressed = false; } // if 350ms elapsed since last press/release it is a short press - if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) { - buttonWaitTime[b] = 0; + if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) { + buttons[b].waitTime = 0; //shortPressAction(b); //not exposed for (int i=0; i> 4); // window uint32_t cycleTime = 50 + (255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; @@ -63,10 +63,9 @@ static uint16_t running_copy(uint32_t color1, uint32_t color2, bool theatre = fa SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1)); SEGENV.step = it; } - return FRAMETIME; } -static uint16_t simple_roll() { +static void simple_roll() { auto roll = GetLastRollForSegment(); if (roll.state != pixels::RollState::ON_FACE) { SEGMENT.fill(0); @@ -79,7 +78,6 @@ static uint16_t simple_roll() { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } } - return FRAMETIME; } // See https://kno.wled.ge/interfaces/json-api/#effect-metadata // Name - DieSimple @@ -92,31 +90,34 @@ static uint16_t simple_roll() { static const char _data_FX_MODE_SIMPLE_DIE[] PROGMEM = "DieSimple@,,Selected Die;!,!;;1;c1=255"; -static uint16_t pulse_roll() { +static void pulse_roll() { auto roll = GetLastRollForSegment(); if (roll.state != pixels::RollState::ON_FACE) { - return mode_breath(); + mode_breath(); + return; } else { - uint16_t ret = mode_blends(); + mode_blends(); uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN; for (int i = num_segments; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } - return ret; } } static const char _data_FX_MODE_PULSE_DIE[] PROGMEM = "DiePulse@!,!,Selected Die;!,!;!;1;sx=24,pal=50,c1=255"; -static uint16_t check_roll() { +static void check_roll() { auto roll = GetLastRollForSegment(); if (roll.state != pixels::RollState::ON_FACE) { - return running_copy(SEGCOLOR(0), SEGCOLOR(2)); + running_copy(SEGCOLOR(0), SEGCOLOR(2)); + return; } else { if (roll.current_face + 1 >= SEGMENT.custom2) { - return mode_glitter(); + mode_glitter(); + return; } else { - return mode_gravcenter(); + mode_gravcenter(); + return; } } } diff --git a/usermods/pixels_dice_tray/pixels_dice_tray.h b/usermods/pixels_dice_tray/pixels_dice_tray.cpp similarity index 93% rename from usermods/pixels_dice_tray/pixels_dice_tray.h rename to usermods/pixels_dice_tray/pixels_dice_tray.cpp index 61348ebb8e..2e97aff650 100644 --- a/usermods/pixels_dice_tray/pixels_dice_tray.h +++ b/usermods/pixels_dice_tray/pixels_dice_tray.cpp @@ -1,5 +1,3 @@ -#pragma once - #include // https://github.com/axlan/arduino-pixels-dice #include "wled.h" @@ -463,11 +461,11 @@ class PixelsDiceTrayUsermod : public Usermod { #if USING_TFT_DISPLAY bool handleButton(uint8_t b) override { if (!enabled || b > 1 // buttons 0,1 only - || buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_NONE || - buttonType[b] == BTN_TYPE_RESERVED || - buttonType[b] == BTN_TYPE_PIR_SENSOR || - buttonType[b] == BTN_TYPE_ANALOG || - buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { + || buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE || + buttons[b].type == BTN_TYPE_RESERVED || + buttons[b].type == BTN_TYPE_PIR_SENSOR || + buttons[b].type == BTN_TYPE_ANALOG || + buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { return false; } @@ -478,43 +476,43 @@ class PixelsDiceTrayUsermod : public Usermod { static unsigned long buttonWaitTime[2] = {0}; //momentary button logic - if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed - if (!buttonPressedBefore[b]) { - buttonPressedTime[b] = now; + if (!buttons[b].longPressed && isButtonPressed(b)) { //pressed + if (!buttons[b].pressedBefore) { + buttons[b].pressedTime = now; } - buttonPressedBefore[b] = true; + buttons[b].pressedBefore = true; - if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press + if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press menu_ctrl.HandleButton(ButtonType::LONG, b); - buttonLongPressed[b] = true; + buttons[b].longPressed = true; return true; } - } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + } else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released - long dur = now - buttonPressedTime[b]; + long dur = now - buttons[b].pressedTime; if (dur < WLED_DEBOUNCE_THRESHOLD) { - buttonPressedBefore[b] = false; + buttons[b].pressedBefore = false; return true; } //too short "press", debounce - bool doublePress = buttonWaitTime[b]; //did we have short press before? - buttonWaitTime[b] = 0; + bool doublePress = buttons[b].waitTime; //did we have short press before? + buttons[b].waitTime = 0; - if (!buttonLongPressed[b]) { //short press + if (!buttons[b].longPressed) { //short press // if this is second release within 350ms it is a double press (buttonWaitTime!=0) if (doublePress) { menu_ctrl.HandleButton(ButtonType::DOUBLE, b); } else { - buttonWaitTime[b] = now; + buttons[b].waitTime = now; } } - buttonPressedBefore[b] = false; - buttonLongPressed[b] = false; + buttons[b].pressedBefore = false; + buttons[b].longPressed = false; } // if 350ms elapsed since last press/release it is a short press - if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && - !buttonPressedBefore[b]) { - buttonWaitTime[b] = 0; + if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && + !buttons[b].pressedBefore) { + buttons[b].waitTime = 0; menu_ctrl.HandleButton(ButtonType::SINGLE, b); } @@ -533,3 +531,7 @@ class PixelsDiceTrayUsermod : public Usermod { // extended. Your usermod will remain compatible as it does not need to // implement all methods from the Usermod base class! }; + + +static PixelsDiceTrayUsermod pixels_dice_tray; +REGISTER_USERMOD(pixels_dice_tray); \ No newline at end of file diff --git a/usermods/pixels_dice_tray/platformio_override.ini.sample b/usermods/pixels_dice_tray/platformio_override.ini.sample index b712f8b2e7..6b4fa7768e 100644 --- a/usermods/pixels_dice_tray/platformio_override.ini.sample +++ b/usermods/pixels_dice_tray/platformio_override.ini.sample @@ -102,7 +102,7 @@ lib_deps = ${esp32s3.lib_deps} # parallel. Also not clear exactly what difference between the ESP32 and the # ESP32S3 would be causing this, though they do run different BLE versions. # May be related to some of the issues discussed in: -# https://github.com/Aircoookie/WLED/issues/1382 +# https://github.com/wled-dev/WLED/issues/1382 ; [env:esp32dev_dice] ; extends = env:esp32dev ; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 diff --git a/usermods/platformio_override.usermods.ini b/usermods/platformio_override.usermods.ini new file mode 100644 index 0000000000..6a402c2f73 --- /dev/null +++ b/usermods/platformio_override.usermods.ini @@ -0,0 +1,31 @@ +[platformio] +default_envs = usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermods_esp32s3 + +[env:usermods_esp32] +extends = env:esp32dev +custom_usermods = ${usermods.custom_usermods} +board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat + + +[env:usermods_esp32c3] +extends = env:esp32c3dev +board = esp32-c3-devkitm-1 +custom_usermods = ${usermods.custom_usermods} +board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat + + +[env:usermods_esp32s2] +extends = env:lolin_s2_mini +custom_usermods = ${usermods.custom_usermods} +board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat + + +[env:usermods_esp32s3] +extends = env:esp32s3dev_16MB_opi +custom_usermods = ${usermods.custom_usermods} +board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat + + + +[usermods] +# Added in CI diff --git a/usermods/pov_display/README.md b/usermods/pov_display/README.md new file mode 100644 index 0000000000..2b1659085f --- /dev/null +++ b/usermods/pov_display/README.md @@ -0,0 +1,48 @@ +## POV Display usermod + +This usermod adds a new effect called “POV Image”. + +![the usermod at work](pov_display.gif?raw=true) + +###How does it work? +With proper configuration (see below) the main segment will display a single row of pixels from an image stored on the ESP. +It displays the image row by row at a high refresh rate. +If you move the pixel segment at the right speed, you will see the full image floating in the air thanks to the persistence of vision. +RGB LEDs only (no RGBW), with grouping set to 1 and spacing set to 0. +Best results with high-density strips (e.g., 144 LEDs/m). + +To get it working: +- Resize your image. The height must match the number of LEDs in your strip/segment. +- Rotate your image 90° clockwise (height becomes width). +- Upload a BMP image (24-bit, uncompressed) to the ESP filesystem using the “/edit” URL. +- Select the “POV Image” effect. +- Set the segment name to the absolute filesystem path of the image (e.g., “/myimage.bmp”). +- The path is case-sensitive and must start with “/”. +- Rotate the pixel strip at approximately 20 RPM. +- Tune as needed so that one full revolution maps to the image width (if the image appears stretched or compressed, adjust RPM slightly). +- Enjoy the show! + +Notes: +- Only 24-bit uncompressed BMP files are supported. +- The image must fit into ~64 KB of RAM (width × height × 3 bytes, plus row padding to a 4-byte boundary). +- Examples (approximate, excluding row padding): + - 128×128 (49,152 bytes) fits. + - 160×160 (76,800 bytes) does NOT fit. + - 96×192 (55,296 bytes) fits; padding may add a small overhead. +- If the rendered image appears mirrored or upside‑down, rotate 90° the other way or flip horizontally in your editor and try again. +- The path must be absolute. + +### Requirements +- 1D rotating LED strip/segment (POV setup). Ensure the segment length equals the number of physical LEDs. +- BMP image saved as 24‑bit, uncompressed (no alpha, no palette). +- Sufficient free RAM (~64 KB) for the image buffer. + +### Troubleshooting +- Nothing displays: verify the file exists at the exact absolute path (case‑sensitive) and is a 24‑bit uncompressed BMP. +- Garbled colors or wrong orientation: re‑export as 24‑bit BMP and retry the rotation/flip guidance above. +- Image too large: reduce width and/or height until it fits within ~64 KB (see examples). +- Path issues: confirm you uploaded the file via the “/edit” URL and can see it in the filesystem browser. + +### Safety +- Secure the rotating assembly and keep clear of moving parts. +- Balance the strip/hub to minimize vibration before running at speed. \ No newline at end of file diff --git a/usermods/pov_display/bmpimage.cpp b/usermods/pov_display/bmpimage.cpp new file mode 100644 index 0000000000..2aea5c8d6e --- /dev/null +++ b/usermods/pov_display/bmpimage.cpp @@ -0,0 +1,146 @@ +#include "bmpimage.h" +#define BUF_SIZE 64000 + +byte * _buffer = nullptr; + +uint16_t read16(File &f) { + uint16_t result; + f.read((uint8_t *)&result,2); + return result; +} + +uint32_t read32(File &f) { + uint32_t result; + f.read((uint8_t *)&result,4); + return result; +} + +bool BMPimage::init(const char * fn) { + File bmpFile; + int bmpDepth; + //first, check if filename exists + if (!WLED_FS.exists(fn)) { + return false; + } + + bmpFile = WLED_FS.open(fn); + if (!bmpFile) { + _valid=false; + return false; + } + + //so, the file exists and is opened + // Parse BMP header + uint16_t header = read16(bmpFile); + if(header != 0x4D42) { // BMP signature + _valid=false; + bmpFile.close(); + return false; + } + + //read and ingnore file size + read32(bmpFile); + (void)read32(bmpFile); // Read & ignore creator bytes + _imageOffset = read32(bmpFile); // Start of image data + // Read DIB header + read32(bmpFile); + _width = read32(bmpFile); + _height = read32(bmpFile); + if(read16(bmpFile) != 1) { // # planes -- must be '1' + _valid=false; + bmpFile.close(); + return false; + } + bmpDepth = read16(bmpFile); // bits per pixel + if((bmpDepth != 24) || (read32(bmpFile) != 0)) { // 0 = uncompressed { + _width=0; + _valid=false; + bmpFile.close(); + return false; + } + // If _height is negative, image is in top-down order. + // This is not canon but has been observed in the wild. + if(_height < 0) { + _height = -_height; + } + //now, we have successfully got all the basics + // BMP rows are padded (if needed) to 4-byte boundary + _rowSize = (_width * 3 + 3) & ~3; + //check image size - if it is too large, it will be unusable + if (_rowSize*_height>BUF_SIZE) { + _valid=false; + bmpFile.close(); + return false; + } + + bmpFile.close(); + // Ensure filename fits our buffer (segment name length constraint). + size_t len = strlen(fn); + if (len > WLED_MAX_SEGNAME_LEN) { + return false; + } + strncpy(filename, fn, sizeof(filename)); + filename[sizeof(filename) - 1] = '\0'; + _valid = true; + return true; +} + +void BMPimage::clear(){ + strcpy(filename, ""); + _width=0; + _height=0; + _rowSize=0; + _imageOffset=0; + _loaded=false; + _valid=false; +} + +bool BMPimage::load(){ + const size_t size = (size_t)_rowSize * (size_t)_height; + if (size > BUF_SIZE) { + return false; + } + File bmpFile = WLED_FS.open(filename); + if (!bmpFile) { + return false; + } + + if (_buffer != nullptr) free(_buffer); + _buffer = (byte*)malloc(size); + if (_buffer == nullptr) return false; + + bmpFile.seek(_imageOffset); + const size_t readBytes = bmpFile.read(_buffer, size); + bmpFile.close(); + if (readBytes != size) { + _loaded = false; + return false; + } + _loaded = true; + return true; +} + +byte* BMPimage::line(uint16_t n){ + if (_loaded) { + return (_buffer+n*_rowSize); + } else { + return NULL; + } +} + +uint32_t BMPimage::pixelColor(uint16_t x, uint16_t y){ + uint32_t pos; + byte b,g,r; //colors + if (! _loaded) { + return 0; + } + if ( (x>=_width) || (y>=_height) ) { + return 0; + } + pos=y*_rowSize + 3*x; + //get colors. Note that in BMP files, they go in BGR order + b= _buffer[pos++]; + g= _buffer[pos++]; + r= _buffer[pos]; + return (r<<16|g<<8|b); +} diff --git a/usermods/pov_display/bmpimage.h b/usermods/pov_display/bmpimage.h new file mode 100644 index 0000000000..a83d1fa904 --- /dev/null +++ b/usermods/pov_display/bmpimage.h @@ -0,0 +1,50 @@ +#ifndef _BMPIMAGE_H +#define _BMPIMAGE_H +#include "Arduino.h" +#include "wled.h" + +/* + * This class describes a bitmap image. Each object refers to a bmp file on + * filesystem fatfs. + * To initialize, call init(), passign to it name of a bitmap file + * at the root of fatfs filesystem: + * + * BMPimage myImage; + * myImage.init("logo.bmp"); + * + * For performance reasons, before actually usign the image, you need to load + * it from filesystem to RAM: + * myImage.load(); + * All load() operations use the same reserved buffer in RAM, so you can only + * have one file loaded at a time. Before loading a new file, always unload the + * previous one: + * myImage.unload(); + */ + +class BMPimage { + public: + int height() {return _height; } + int width() {return _width; } + int rowSize() {return _rowSize;} + bool isLoaded() {return _loaded; } + bool load(); + void unload() {_loaded=false; } + byte * line(uint16_t n); + uint32_t pixelColor(uint16_t x,uint16_t y); + bool init(const char* fn); + void clear(); + char * getFilename() {return filename;}; + + private: + char filename[WLED_MAX_SEGNAME_LEN+1]=""; + int _width=0; + int _height=0; + int _rowSize=0; + int _imageOffset=0; + bool _loaded=false; + bool _valid=false; +}; + +extern byte * _buffer; + +#endif diff --git a/usermods/pov_display/library.json b/usermods/pov_display/library.json new file mode 100644 index 0000000000..461b1e2d48 --- /dev/null +++ b/usermods/pov_display/library.json @@ -0,0 +1,5 @@ +{ + "name:": "pov_display", + "build": { "libArchive": false}, + "platforms": ["espressif32"] +} diff --git a/usermods/pov_display/pov.cpp b/usermods/pov_display/pov.cpp new file mode 100644 index 0000000000..ea5a43ed68 --- /dev/null +++ b/usermods/pov_display/pov.cpp @@ -0,0 +1,47 @@ +#include "pov.h" + +POV::POV() {} + +void POV::showLine(const byte * line, uint16_t size){ + uint16_t i, pos; + uint8_t r, g, b; + if (!line) { + // All-black frame on null input + for (i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, CRGB::Black); + } + strip.show(); + lastLineUpdate = micros(); + return; + } + for (i = 0; i < SEGLEN; i++) { + if (i < size) { + pos = 3 * i; + // using bgr order + b = line[pos++]; + g = line[pos++]; + r = line[pos]; + SEGMENT.setPixelColor(i, CRGB(r, g, b)); + } else { + SEGMENT.setPixelColor(i, CRGB::Black); + } + } + strip.show(); + lastLineUpdate = micros(); +} + +bool POV::loadImage(const char * filename){ + if(!image.init(filename)) return false; + if(!image.load()) return false; + currentLine=0; + return true; +} + +int16_t POV::showNextLine(){ + if (!image.isLoaded()) return 0; + //move to next line + showLine(image.line(currentLine), image.width()); + currentLine++; + if (currentLine == image.height()) {currentLine=0;} + return currentLine; +} diff --git a/usermods/pov_display/pov.h b/usermods/pov_display/pov.h new file mode 100644 index 0000000000..cb543d2ea7 --- /dev/null +++ b/usermods/pov_display/pov.h @@ -0,0 +1,42 @@ +#ifndef _POV_H +#define _POV_H +#include "bmpimage.h" + + +class POV { + public: + POV(); + + /* Shows one line. line should be pointer to array which holds pixel colors + * (3 bytes per pixel, in BGR order). Note: 3, not 4!!! + * size should be size of array (number of pixels, not number of bytes) + */ + void showLine(const byte * line, uint16_t size); + + /* Reads from file an image and making it current image */ + bool loadImage(const char * filename); + + /* Show next line of active image + Retunrs the index of next line to be shown (not yet shown!) + If it retunrs 0, it means we have completed showing the image and + next call will start again + */ + int16_t showNextLine(); + + //time since strip was last updated, in micro sec + uint32_t timeSinceUpdate() {return (micros()-lastLineUpdate);} + + + BMPimage * currentImage() {return ℑ} + + char * getFilename() {return image.getFilename();} + + private: + BMPimage image; + int16_t currentLine=0; //next line to be shown + uint32_t lastLineUpdate=0; //time in microseconds +}; + + + +#endif diff --git a/usermods/pov_display/pov_display.cpp b/usermods/pov_display/pov_display.cpp new file mode 100644 index 0000000000..c57f3d8d59 --- /dev/null +++ b/usermods/pov_display/pov_display.cpp @@ -0,0 +1,75 @@ +#include "wled.h" +#include "pov.h" + +static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;;"; + +static POV s_pov; + +void mode_pov_image(void) { + Segment& mainseg = strip.getMainSegment(); + const char* segName = mainseg.name; + if (!segName) { + return; + } + // Only proceed for files ending with .bmp (case-insensitive) + size_t segLen = strlen(segName); + if (segLen < 4) return; + const char* ext = segName + (segLen - 4); + // compare case-insensitive to ".bmp" + if (!((ext[0]=='.') && + (ext[1]=='b' || ext[1]=='B') && + (ext[2]=='m' || ext[2]=='M') && + (ext[3]=='p' || ext[3]=='P'))) { + return; + } + + const char* current = s_pov.getFilename(); + if (current && strcmp(segName, current) == 0) { + s_pov.showNextLine(); + return; + } + + static unsigned long s_lastLoadAttemptMs = 0; + unsigned long nowMs = millis(); + // Retry at most twice per second if the image is not yet loaded. + if (nowMs - s_lastLoadAttemptMs < 500) return; + s_lastLoadAttemptMs = nowMs; + s_pov.loadImage(segName); + return; +} + +class PovDisplayUsermod : public Usermod { +protected: + bool enabled = false; //WLEDMM + const char *_name; //WLEDMM + bool initDone = false; //WLEDMM + unsigned long lastTime = 0; //WLEDMM +public: + + PovDisplayUsermod(const char *name, bool enabled) + : enabled(enabled) , _name(name) {} + + void setup() override { + strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE); + //initDone removed (unused) + } + + + void loop() override { + // if usermod is disabled or called during strip updating just exit + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!enabled || strip.isUpdating()) return; + + // do your magic here + if (millis() - lastTime > 1000) { + lastTime = millis(); + } + } + + uint16_t getId() override { + return USERMOD_ID_POV_DISPLAY; + } +}; + +static PovDisplayUsermod pov_display("POV Display", false); +REGISTER_USERMOD(pov_display); diff --git a/usermods/pov_display/pov_display.gif b/usermods/pov_display/pov_display.gif new file mode 100644 index 0000000000..58f8ee0c1f Binary files /dev/null and b/usermods/pov_display/pov_display.gif differ diff --git a/usermods/pov_display/usermod_pov_display.h b/usermods/pov_display/usermod_pov_display.h deleted file mode 100644 index b1fc0dba60..0000000000 --- a/usermods/pov_display/usermod_pov_display.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once -#include "wled.h" -#include - -void * openFile(const char *filename, int32_t *size) { - f = WLED_FS.open(filename); - *size = f.size(); - return &f; -} - -void closeFile(void *handle) { - if (f) f.close(); -} - -int32_t readFile(PNGFILE *pFile, uint8_t *pBuf, int32_t iLen) -{ - int32_t iBytesRead; - iBytesRead = iLen; - File *f = static_cast(pFile->fHandle); - // Note: If you read a file all the way to the last byte, seek() stops working - if ((pFile->iSize - pFile->iPos) < iLen) - iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around - if (iBytesRead <= 0) - return 0; - iBytesRead = (int32_t)f->read(pBuf, iBytesRead); - pFile->iPos = f->position(); - return iBytesRead; -} - -int32_t seekFile(PNGFILE *pFile, int32_t iPosition) -{ - int i = micros(); - File *f = static_cast(pFile->fHandle); - f->seek(iPosition); - pFile->iPos = (int32_t)f->position(); - i = micros() - i; - return pFile->iPos; -} - -void draw(PNGDRAW *pDraw) { - uint16_t usPixels[SEGLEN]; - png.getLineAsRGB565(pDraw, usPixels, PNG_RGB565_LITTLE_ENDIAN, 0xffffffff); - for(int x=0; x < SEGLEN; x++) { - uint16_t color = usPixels[x]; - byte r = ((color >> 11) & 0x1F); - byte g = ((color >> 5) & 0x3F); - byte b = (color & 0x1F); - SEGMENT.setPixelColor(x, RGBW32(r,g,b,0)); - } - strip.show(); -} - -uint16_t mode_pov_image(void) { - const char * filepath = SEGMENT.name; - int rc = png.open(filepath, openFile, closeFile, readFile, seekFile, draw); - if (rc == PNG_SUCCESS) { - rc = png.decode(NULL, 0); - png.close(); - return FRAMETIME; - } - return FRAMETIME; -} - -class PovDisplayUsermod : public Usermod -{ - public: - static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;1"; - - PNG png; - File f; - - void setup() { - strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE); - } - - void loop() { - } - - uint16_t getId() - { - return USERMOD_ID_POV_DISPLAY; - } - - void connected() {} -}; diff --git a/usermods/project_cars_shiftlight/readme.md b/usermods/project_cars_shiftlight/readme.md index 433da43006..338936a805 100644 --- a/usermods/project_cars_shiftlight/readme.md +++ b/usermods/project_cars_shiftlight/readme.md @@ -1,11 +1,11 @@ -### Shift Light for Project Cars +# Shift Light for Project Cars Turn your WLED lights into a rev light and shift indicator for Project Cars. It's easy 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. However, 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 | | ------ | -------------- | @@ -19,5 +19,5 @@ It's easy to use. | 8 | 05 | | 9 | 1 | -3. Once you enter a race, WLED should automatically shift to PCARS mode. -4. Done. +_3._ Once you enter a race, WLED should automatically shift to PCARS mode. +_4._ Done. diff --git a/usermods/pwm_outputs/library.json b/usermods/pwm_outputs/library.json new file mode 100644 index 0000000000..a01068bd43 --- /dev/null +++ b/usermods/pwm_outputs/library.json @@ -0,0 +1,4 @@ +{ + "name": "pwm_outputs", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/pwm_outputs/usermod_pwm_outputs.h b/usermods/pwm_outputs/pwm_outputs.cpp similarity index 98% rename from usermods/pwm_outputs/usermod_pwm_outputs.h rename to usermods/pwm_outputs/pwm_outputs.cpp index 09232f043a..d94f1d848f 100644 --- a/usermods/pwm_outputs/usermod_pwm_outputs.h +++ b/usermods/pwm_outputs/pwm_outputs.cpp @@ -1,4 +1,3 @@ -#pragma once #include "wled.h" #ifndef ESP32 @@ -219,3 +218,7 @@ class PwmOutputsUsermod : public Usermod { const char PwmOutputsUsermod::USERMOD_NAME[] PROGMEM = "PwmOutputs"; const char PwmOutputsUsermod::PWM_STATE_NAME[] PROGMEM = "pwm"; + + +static PwmOutputsUsermod pwm_outputs; +REGISTER_USERMOD(pwm_outputs); \ No newline at end of file diff --git a/usermods/quinled-an-penta/library.json b/usermods/quinled-an-penta/library.json new file mode 100644 index 0000000000..fca9b0e3a4 --- /dev/null +++ b/usermods/quinled-an-penta/library.json @@ -0,0 +1,8 @@ +{ + "name": "quinled-an-penta", + "build": { "libArchive": false}, + "dependencies": { + "olikraus/U8g2":"~2.28.8", + "robtillaart/SHT85":"~0.3.3" + } +} diff --git a/usermods/quinled-an-penta/quinled-an-penta.h b/usermods/quinled-an-penta/quinled-an-penta.cpp similarity index 99% rename from usermods/quinled-an-penta/quinled-an-penta.h rename to usermods/quinled-an-penta/quinled-an-penta.cpp index e446720398..a3b452bf18 100644 --- a/usermods/quinled-an-penta/quinled-an-penta.h +++ b/usermods/quinled-an-penta/quinled-an-penta.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "U8g2lib.h" #include "SHT85.h" #include "Wire.h" @@ -752,4 +750,7 @@ 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, 0xFF, 0xFF, 0xFF, -}; \ No newline at end of file +}; + +static QuinLEDAnPentaUsermod quinled_an_penta; +REGISTER_USERMOD(quinled_an_penta); \ No newline at end of file diff --git a/usermods/quinled-an-penta/readme.md b/usermods/quinled-an-penta/readme.md index c1260d9134..db1f72c4a7 100644 --- a/usermods/quinled-an-penta/readme.md +++ b/usermods/quinled-an-penta/readme.md @@ -5,29 +5,6 @@ The (un)official usermod to get the best out of the QuinLED-An-Penta (https://qu * "u8g2" by olikraus, v2.28 or higher: https://github.com/olikraus/u8g2 * "SHT85" by Rob Tillaart, v0.2 or higher: https://github.com/RobTillaart/SHT85 -## Usermod installation -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. diff --git a/usermods/readme.md b/usermods/readme.md index 8aa8d6abcf..eefb64dbd0 100644 --- a/usermods/readme.md +++ b/usermods/readme.md @@ -1,4 +1,4 @@ -### Usermods +# Usermods This folder serves as a repository for usermods (custom `usermod.cpp` files)! @@ -6,11 +6,11 @@ If you have created a usermod you believe is useful (for example to support a pa 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 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! +* 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 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 updated, usermods might break. I am not actively maintaining any usermod in this directory, that is your responsibility as the creator of the usermod. diff --git a/usermods/rgb-rotary-encoder/library.json b/usermods/rgb-rotary-encoder/library.json new file mode 100644 index 0000000000..fa8a65d184 --- /dev/null +++ b/usermods/rgb-rotary-encoder/library.json @@ -0,0 +1,7 @@ +{ + "name": "rgb-rotary-encoder", + "build": { "libArchive": false}, + "dependencies": { + "lennarthennigs/ESP Rotary":"^2.1.1" + } +} diff --git a/usermods/rgb-rotary-encoder/readme.md b/usermods/rgb-rotary-encoder/readme.md index ba5aad4df7..abd8a812c4 100644 --- a/usermods/rgb-rotary-encoder/readme.md +++ b/usermods/rgb-rotary-encoder/readme.md @@ -8,30 +8,6 @@ https://user-images.githubusercontent.com/3090131/124680599-0180ab80-dec7-11eb-9 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 diff --git a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp similarity index 95% rename from usermods/rgb-rotary-encoder/rgb-rotary-encoder.h rename to usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp index 00fc227252..3ec46f6f77 100644 --- a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h +++ b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "ESPRotary.h" #include #include "wled.h" @@ -56,10 +54,14 @@ class RgbRotaryEncoderUsermod : public Usermod void initLedBus() { - byte _pins[5] = {(byte)ledIo, 255, 255, 255, 255}; + // Initialize all pins to the sentinel value first… + byte _pins[OUTPUT_MAX_PINS]; + std::fill(std::begin(_pins), std::end(_pins), 255); + // …then set only the LED pin + _pins[0] = static_cast(ledIo); BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0); - - ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1); + busCfg.iType = BusManager::getI(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver + ledBus = new BusDigital(busCfg); if (!ledBus->isOk()) { cleanup(); return; @@ -340,4 +342,7 @@ 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 +const char RgbRotaryEncoderUsermod::_incrementPerClick[] PROGMEM = "Increment-per-Click"; + +static RgbRotaryEncoderUsermod rgb_rotary_encoder; +REGISTER_USERMOD(rgb_rotary_encoder); \ No newline at end of file diff --git a/usermods/sd_card/library.json b/usermods/sd_card/library.json new file mode 100644 index 0000000000..33e8f98f27 --- /dev/null +++ b/usermods/sd_card/library.json @@ -0,0 +1,4 @@ +{ + "name": "sd_card", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/sd_card/usermod_sd_card.h b/usermods/sd_card/sd_card.cpp similarity index 99% rename from usermods/sd_card/usermod_sd_card.h rename to usermods/sd_card/sd_card.cpp index da1999d9b5..4e68b97a34 100644 --- a/usermods/sd_card/usermod_sd_card.h +++ b/usermods/sd_card/sd_card.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" // SD connected via MMC / SPI @@ -240,4 +238,7 @@ void listDir( const char * dirname, uint8_t levels){ } } -#endif \ No newline at end of file +#endif + +static UsermodSdCard sd_card; +REGISTER_USERMOD(sd_card); \ No newline at end of file diff --git a/usermods/sensors_to_mqtt/library.json b/usermods/sensors_to_mqtt/library.json new file mode 100644 index 0000000000..977053da78 --- /dev/null +++ b/usermods/sensors_to_mqtt/library.json @@ -0,0 +1,10 @@ +{ + "name": "sensors_to_mqtt", + "build": { "libArchive": false}, + "dependencies": { + "adafruit/Adafruit BMP280 Library":"2.6.8", + "adafruit/Adafruit CCS811 Library":"1.1.3", + "adafruit/Adafruit Si7021 Library":"1.5.3", + "adafruit/Adafruit Unified Sensor":"^1.1.15" + } +} diff --git a/usermods/sensors_to_mqtt/readme.md b/usermods/sensors_to_mqtt/readme.md index d427d3e144..0616279370 100644 --- a/usermods/sensors_to_mqtt/readme.md +++ b/usermods/sensors_to_mqtt/readme.md @@ -60,25 +60,6 @@ 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 diff --git a/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h b/usermods/sensors_to_mqtt/sensors_to_mqtt.cpp similarity index 95% rename from usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h rename to usermods/sensors_to_mqtt/sensors_to_mqtt.cpp index 9b5bd8c882..5f7da97a98 100644 --- a/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h +++ b/usermods/sensors_to_mqtt/sensors_to_mqtt.cpp @@ -1,9 +1,3 @@ -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - -#pragma once - #include "wled.h" #include #include @@ -11,9 +5,13 @@ #include #include -Adafruit_BMP280 bmp; -Adafruit_Si7021 si7021; -Adafruit_CCS811 ccs811; +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +static Adafruit_BMP280 bmp; +static Adafruit_Si7021 si7021; +static Adafruit_CCS811 ccs811; class UserMod_SensorsToMQTT : public Usermod { @@ -23,7 +21,7 @@ class UserMod_SensorsToMQTT : public Usermod float SensorPressure = 0; float SensorTemperature = 0; float SensorHumidity = 0; - char *SensorIaq = "Unknown"; + const char *SensorIaq = "Unknown"; String mqttTemperatureTopic = ""; String mqttHumidityTopic = ""; String mqttPressureTopic = ""; @@ -117,7 +115,7 @@ class UserMod_SensorsToMQTT : public Usermod /** * 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) + const char *_getIaqIndex(float humidity, int tvoc, int eco2) { int iaq_index = 0; @@ -216,6 +214,7 @@ class UserMod_SensorsToMQTT : public Usermod { return "Excellent"; } + return "Unknown"; } public: @@ -276,3 +275,7 @@ class UserMod_SensorsToMQTT : public Usermod } } }; + + +static UserMod_SensorsToMQTT sensors_to_mqtt; +REGISTER_USERMOD(sensors_to_mqtt); \ No newline at end of file diff --git a/usermods/seven_segment_display/library.json b/usermods/seven_segment_display/library.json new file mode 100644 index 0000000000..f78aad87b9 --- /dev/null +++ b/usermods/seven_segment_display/library.json @@ -0,0 +1,4 @@ +{ + "name": "seven_segment_display", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h b/usermods/seven_segment_display/seven_segment_display.cpp similarity index 99% rename from usermods/seven_segment_display/usermod_v2_seven_segment_display.h rename to usermods/seven_segment_display/seven_segment_display.cpp index 20fef15df5..13a6306be5 100644 --- a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h +++ b/usermods/seven_segment_display/seven_segment_display.cpp @@ -1,11 +1,9 @@ -#ifndef WLED_ENABLE_MQTT +#include "wled.h" + +#ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif -#pragma once - -#include "wled.h" - class SevenSegmentDisplay : public Usermod { @@ -498,4 +496,7 @@ 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 +const char SevenSegmentDisplay::_str_sevenSeg[] PROGMEM = "sevenSeg"; + +static SevenSegmentDisplay seven_segment_display; +REGISTER_USERMOD(seven_segment_display); \ No newline at end of file diff --git a/usermods/seven_segment_display_reloaded/library.json b/usermods/seven_segment_display_reloaded/library.json new file mode 100644 index 0000000000..1b7d0687f2 --- /dev/null +++ b/usermods/seven_segment_display_reloaded/library.json @@ -0,0 +1,7 @@ +{ + "name": "seven_segment_display_reloaded", + "build": { + "libArchive": false, + "extraScript": "setup_deps.py" + } +} \ No newline at end of file diff --git a/usermods/seven_segment_display_reloaded/readme.md b/usermods/seven_segment_display_reloaded/readme.md index a3398c3e59..94788df7e4 100644 --- a/usermods/seven_segment_display_reloaded/readme.md +++ b/usermods/seven_segment_display_reloaded/readme.md @@ -9,7 +9,7 @@ Very loosely based on the existing usermod "seven segment display". 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. +For the auto brightness option, the usermod SN_Photoresistor or BH1750_V2 has to be installed as well. See SN_Photoresistor/readme.md or BH1750_V2/readme.md for instructions. ## Settings All settings can be controlled via the usermod settings page. @@ -28,10 +28,10 @@ Enables the blinking colon(s) if they are defined Shows the leading zero of the hour if it exists (i.e. shows `07` instead of `7`) ### enable-auto-brightness -Enables the auto brightness feature. Can be used only when the usermod SN_Photoresistor is installed. +Enables the auto brightness feature. Can be used only when the usermods SN_Photoresistor or BH1750_V2 are installed. ### auto-brightness-min / auto-brightness-max -The lux value calculated from usermod SN_Photoresistor will be mapped to the values defined here. +The lux value calculated from usermod SN_Photoresistor or BH1750_V2 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. diff --git a/usermods/seven_segment_display_reloaded/setup_deps.py b/usermods/seven_segment_display_reloaded/setup_deps.py new file mode 100644 index 0000000000..1c51acccec --- /dev/null +++ b/usermods/seven_segment_display_reloaded/setup_deps.py @@ -0,0 +1,10 @@ +from platformio.package.meta import PackageSpec +Import('env') + + +libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] +# Check for partner usermods +if "SN_Photoresistor" in libs: + env.Append(CPPDEFINES=[("USERMOD_SN_PHOTORESISTOR")]) +if any(mod in ("BH1750_v2", "BH1750") for mod in libs): + env.Append(CPPDEFINES=[("USERMOD_BH1750")]) diff --git a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h b/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp similarity index 94% rename from usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h rename to usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp index 1436f8fc4c..893e061bc8 100644 --- a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h +++ b/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp @@ -1,10 +1,14 @@ -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." +#include "wled.h" +#ifdef USERMOD_SN_PHOTORESISTOR + #include "SN_Photoresistor.h" +#endif +#ifdef USERMOD_BH1750 + #include "BH1750_v2.h" #endif -#pragma once - -#include "wled.h" +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif class UsermodSSDR : public Usermod { @@ -97,6 +101,11 @@ class UsermodSSDR : public Usermod { #else void* ptr = nullptr; #endif +#ifdef USERMOD_BH1750 + Usermod_BH1750* bh1750 = nullptr; +#else + void* bh1750 = nullptr; +#endif void _overlaySevenSegmentDraw() { int displayMaskLen = static_cast(umSSDRDisplayMask.length()); @@ -387,6 +396,9 @@ class UsermodSSDR : public Usermod { #ifdef USERMOD_SN_PHOTORESISTOR ptr = (Usermod_SN_Photoresistor*) UsermodManager::lookup(USERMOD_ID_SN_PHOTORESISTOR); #endif + #ifdef USERMOD_BH1750 + bh1750 = (Usermod_BH1750*) UsermodManager::lookup(USERMOD_ID_BH1750); + #endif DEBUG_PRINTLN(F("Setup done")); } @@ -410,6 +422,20 @@ class UsermodSSDR : public Usermod { umSSDRLastRefresh = millis(); } #endif + #ifdef USERMOD_BH1750 + if(bri != 0 && umSSDREnableLDR && (millis() - umSSDRLastRefresh > umSSDRResfreshTime)) { + if (bh1750 != nullptr) { + float lux = bh1750->getIlluminance(); + uint16_t brightness = map(lux, 0, 1000, umSSDRBrightnessMin, umSSDRBrightnessMax); + if (bri != brightness) { + DEBUG_PRINTF("Adjusting brightness based on lux value: %.2f lx, new brightness: %d\n", lux, brightness); + bri = brightness; + stateUpdated(1); + } + } + umSSDRLastRefresh = millis(); + } + #endif } void handleOverlayDraw() { @@ -571,3 +597,7 @@ 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"; + + +static UsermodSSDR seven_segment_display_reloaded; +REGISTER_USERMOD(seven_segment_display_reloaded); \ No newline at end of file diff --git a/usermods/sht/ShtUsermod.h b/usermods/sht/ShtUsermod.h new file mode 100644 index 0000000000..5dd83f46d6 --- /dev/null +++ b/usermods/sht/ShtUsermod.h @@ -0,0 +1,71 @@ +#pragma once +#include "wled.h" + +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +#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 SHT; + +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; } +}; diff --git a/usermods/sht/library.json b/usermods/sht/library.json new file mode 100644 index 0000000000..0916e9a378 --- /dev/null +++ b/usermods/sht/library.json @@ -0,0 +1,7 @@ +{ + "name": "sht", + "build": { "libArchive": false }, + "dependencies": { + "robtillaart/SHT85": "~0.3.3" + } +} \ No newline at end of file diff --git a/usermods/sht/readme.md b/usermods/sht/readme.md index 0337805b3a..4470c8836f 100644 --- a/usermods/sht/readme.md +++ b/usermods/sht/readme.md @@ -1,40 +1,43 @@ # 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 + +* "SHT85" by Rob Tillaart, v0.2 or higher: ## 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. + +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 custom_usermod `sht`. ESP32: -``` + +```ini [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 +custom_usermods = ${env:esp32dev.custom_usermods} sht ``` ESP8266: -``` + +```ini [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 +custom_usermods = ${env:d1_mini.custom_usermods} sht ``` ## 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 @@ -49,8 +52,11 @@ Navigate to the "Config" and then to the "Usermods" section. If you compiled WLE * Default: Disabled ## Change log + 2022-12 + * First implementation. ## Credits -ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG + +ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: diff --git a/usermods/sht/usermod_sht.h b/usermods/sht/sht.cpp similarity index 79% rename from usermods/sht/usermod_sht.h rename to usermods/sht/sht.cpp index f10c78a251..e1eb9e885f 100644 --- a/usermods/sht/usermod_sht.h +++ b/usermods/sht/sht.cpp @@ -1,74 +1,6 @@ -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - -#pragma once - +#include "ShtUsermod.h" #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"; @@ -477,4 +409,7 @@ float ShtUsermod::getTemperature() { */ const char* ShtUsermod::getUnitString() { return unitOfTemp ? "°F" : "°C"; -} \ No newline at end of file +} + +static ShtUsermod sht; +REGISTER_USERMOD(sht); diff --git a/usermods/smartnest/library.json b/usermods/smartnest/library.json new file mode 100644 index 0000000000..3e9ea63a9b --- /dev/null +++ b/usermods/smartnest/library.json @@ -0,0 +1,4 @@ +{ + "name": "smartnest", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/smartnest/usermod_smartnest.h b/usermods/smartnest/smartnest.cpp similarity index 97% rename from usermods/smartnest/usermod_smartnest.h rename to usermods/smartnest/smartnest.cpp index 92d524c88a..d0cb92dcfd 100644 --- a/usermods/smartnest/usermod_smartnest.h +++ b/usermods/smartnest/smartnest.cpp @@ -1,11 +1,9 @@ -#ifndef WLED_ENABLE_MQTT +#include "wled.h" + +#ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif -#pragma once - -#include "wled.h" - class Smartnest : public Usermod { private: @@ -49,7 +47,7 @@ class Smartnest : public Usermod void setColor(int r, int g, int b) { - strip.setColor(0, r, g, b); + strip.getMainSegment().setColor(0, RGBW32(r, g, b, 0)); stateUpdated(CALL_MODE_DIRECT_CHANGE); char msg[18] {}; sprintf(msg, "rgb(%d,%d,%d)", r, g, b); @@ -203,3 +201,7 @@ class Smartnest : public Usermod } } }; + + +static Smartnest smartnest; +REGISTER_USERMOD(smartnest); \ No newline at end of file diff --git a/usermods/stairway_wipe_basic/library.json b/usermods/stairway_wipe_basic/library.json new file mode 100644 index 0000000000..f7d353b596 --- /dev/null +++ b/usermods/stairway_wipe_basic/library.json @@ -0,0 +1,4 @@ +{ + "name": "stairway_wipe_basic", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h b/usermods/stairway_wipe_basic/stairway_wipe_basic.cpp similarity index 96% rename from usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h rename to usermods/stairway_wipe_basic/stairway_wipe_basic.cpp index 707479df17..cddd655d6d 100644 --- a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h +++ b/usermods/stairway_wipe_basic/stairway_wipe_basic.cpp @@ -2,7 +2,7 @@ /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is Stairway-Wipe as a v2 usermod. * @@ -126,3 +126,7 @@ void setup() { //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! }; + + +static StairwayWipeUsermod stairway_wipe_basic; +REGISTER_USERMOD(stairway_wipe_basic); \ No newline at end of file diff --git a/usermods/stairway_wipe_basic/wled06_usermod.ino b/usermods/stairway_wipe_basic/wled06_usermod.ino deleted file mode 100644 index dc2159ee9d..0000000000 --- a/usermods/stairway_wipe_basic/wled06_usermod.ino +++ /dev/null @@ -1,111 +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 and userVar1 (API calls &U0=,&U1=, uint16_t) - -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 - -//gets called once at boot. Do all initialization that doesn't depend on network here -void userSetup() -{ - //setup PIR sensor here, if needed -} - -//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() -{ - //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 startWipe() -{ - bri = briLast; //turn on - transitionDelayTemp = 0; //no transition - effectCurrent = FX_MODE_COLOR_WIPE; - strip.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; -} diff --git a/usermods/udp_name_sync/library.json b/usermods/udp_name_sync/library.json new file mode 100644 index 0000000000..4c5bb44814 --- /dev/null +++ b/usermods/udp_name_sync/library.json @@ -0,0 +1,5 @@ +{ + "name": "udp_name_sync", + "build": { "libArchive": false }, + "dependencies": {} +} diff --git a/usermods/udp_name_sync/udp_name_sync.cpp b/usermods/udp_name_sync/udp_name_sync.cpp new file mode 100644 index 0000000000..b31b856983 --- /dev/null +++ b/usermods/udp_name_sync/udp_name_sync.cpp @@ -0,0 +1,85 @@ +#include "wled.h" + +class UdpNameSync : public Usermod { + + private: + + bool enabled = false; + char segmentName[WLED_MAX_SEGNAME_LEN] = {0}; + static constexpr uint8_t kPacketType = 200; // custom usermod packet type + static const char _name[]; + static const char _enabled[]; + + public: + /** + * Enable/Disable the usermod + */ + inline void enable(bool value) { enabled = value; } + + /** + * Get usermod enabled/disabled state + */ + inline bool isEnabled() const { return enabled; } + + void setup() override { + // Enabled when this usermod is compiled, set to false if you prefer runtime opt-in + enable(true); + } + + void loop() override { + if (!enabled) return; + if (!WLED_CONNECTED) return; + if (!udpConnected) return; + Segment& mainseg = strip.getMainSegment(); + if (segmentName[0] == '\0' && !mainseg.name) return; //name was never set, do nothing + + const char* curName = mainseg.name ? mainseg.name : ""; + if (strncmp(curName, segmentName, sizeof(segmentName)) == 0) return; // same name, do nothing + + IPAddress broadcastIp = uint32_t(Network.localIP()) | ~uint32_t(Network.subnetMask()); + byte udpOut[WLED_MAX_SEGNAME_LEN + 2]; + udpOut[0] = kPacketType; // custom usermod packet type (avoid 0..5 used by core protocols) + + if (segmentName[0] != '\0' && !mainseg.name) { // name cleared + notifierUdp.beginPacket(broadcastIp, udpPort); + segmentName[0] = '\0'; + DEBUG_PRINTLN(F("UdpNameSync: sending empty name")); + udpOut[1] = 0; // explicit empty string + notifierUdp.write(udpOut, 2); + notifierUdp.endPacket(); + return; + } + + notifierUdp.beginPacket(broadcastIp, udpPort); + DEBUG_PRINT(F("UdpNameSync: saving segment name ")); + DEBUG_PRINTLN(curName); + strlcpy(segmentName, curName, sizeof(segmentName)); + strlcpy((char *)&udpOut[1], segmentName, sizeof(udpOut) - 1); // leave room for header byte + size_t nameLen = strnlen((char *)&udpOut[1], sizeof(udpOut) - 1); + notifierUdp.write(udpOut, 2 + nameLen); + notifierUdp.endPacket(); + DEBUG_PRINT(F("UdpNameSync: Sent segment name : ")); + DEBUG_PRINTLN(segmentName); + return; + } + + bool onUdpPacket(uint8_t * payload, size_t len) override { + DEBUG_PRINT(F("UdpNameSync: Received packet")); + if (!enabled) return false; + if (receiveDirect) return false; + if (len < 2) return false; // need type + at least 1 byte for name (can be 0) + if (payload[0] != kPacketType) return false; + Segment& mainseg = strip.getMainSegment(); + char tmp[WLED_MAX_SEGNAME_LEN] = {0}; + size_t copyLen = len - 1; + if (copyLen > sizeof(tmp) - 1) copyLen = sizeof(tmp) - 1; + memcpy(tmp, &payload[1], copyLen); + tmp[copyLen] = '\0'; + mainseg.setName(tmp); + DEBUG_PRINT(F("UdpNameSync: set segment name")); + return true; + } +}; + +static UdpNameSync udp_name_sync; +REGISTER_USERMOD(udp_name_sync); diff --git a/usermods/user_fx/README.md b/usermods/user_fx/README.md new file mode 100644 index 0000000000..2d6e800e46 --- /dev/null +++ b/usermods/user_fx/README.md @@ -0,0 +1,505 @@ +# Usermod user FX + +This usermod is a common place to put various users’ WLED effects. It lets you load your own custom effects or bring back deprecated ones—without touching core WLED source code. + +Multiple Effects can be specified inside this single usermod, as we will illustrate below. You will be able to define them with custom names, sliders, etc. as with any other Effect. + +* [Installation](./README.md#installation) +* [How The Usermod Works](./README.md#how-the-usermod-works) +* [Basic Syntax for WLED Effect Creation](./README.md#basic-syntax-for-wled-effect-creation) +* [Understanding 2D WLED Effects](./README.md#understanding-2d-wled-effects) +* [The Metadata String](./README.md#the-metadata-string) +* [Understanding 1D WLED Effects](./README.md#understanding-1d-wled-effects) +* [Combining Multiple Effects in this Usermod](./README.md#combining-multiple-effects-in-this-usermod) +* [Compiling](./README.md#compiling) +* [Change Log](./README.md#change-log) +* [Contact Us](./README.md#contact-us) + +## Installation + +To activate the usermod, add the following line to your platformio_override.ini +```ini +custom_usermods = user_fx +``` +Or if you are already using a usermod, append user_fx to the list +```ini +custom_usermods = audioreactive user_fx +``` + +## How The Usermod Works + +The `user_fx.cpp` file can be broken down into four main parts: +* **static effect definition** - This is a static LED setting that is displayed if an effect fails to initialize. +* **User FX function definition(s)** - This area is where you place the FX code for all of the custom effects you want to use. This mainly includes the FX code and the static variable containing the [metadata string](https://kno.wled.ge/interfaces/json-api/#effect-metadata). +* **Usermod Class definition(s)** - The class definition defines the blueprint from which all your custom Effects (or any usermod, for that matter) are created. +* **Usermod registration** - All usermods have to be registered so that they are able to be compiled into your binary. + +We will go into greater detail on how custom effects work in the usermod and how to go about creating your own in the section below. + + +## Basic Syntax for WLED Effect Creation + +WLED effects generally follow a certain procedure for their operation: +1. Determine dimension of segment +2. Calculate new state if needed +3. Implement a loop that calculates color for each pixel and sets it using `SEGMENT.setPixelColor()` +4. The function is called at current frame rate. + +Below are some helpful variables and functions to know as you start your journey towards WLED effect creation: + +| Syntax Element | Size | Description | +| :---------------------------------------------- | :----- | :---------- | +| [`SEGMENT.speed / intensity / custom1 / custom2`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L450) | 8-bit | These read-only variables help you control aspects of your custom effect using the UI sliders. You can edit these variables through the UI sliders when WLED is running your effect. (These variables can be controlled by the API as well.) Note that while `SEGMENT.intensity` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. The other three bits are used by the boolean parameters `SEGMENT.check1` through `SEGMENT.check3` and are bit-packed to conserve data size and memory. | +| [`SEGMENT.custom3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L454) | 5-bit | Another optional UI slider for custom effect control. While `SEGMENT.speed` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. | +| [`SEGMENT.check1 / check2 / check3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L455) | 1-bit | These variables are boolean parameters which show up as checkbox options in the User Interface. They are bit-packed along with `SEGMENT.custom3` to conserve data size and memory. | +| [`SEGENV.aux0 / aux1`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L467) | 16-bit | These are state variables that persists between function calls, and they are free to be overwritten by the user for any use case. | +| [`SEGENV.step`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L465) | 32-bit | This is a timestamp variable that contains the last update time. It is initially set during effect initialization to 0, and then it updates with the elapsed time after each frame runs. | +| [`SEGENV.call`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L466) | 32-bit | A counter for how many times this effect function has been invoked since it started. | +| [`strip.now`](https://github.com/wled/WLED/blob/main/wled00/FX.h) | 32-bit | Current timestamp in milliseconds. (Equivalent to `millis()`, but use `strip.now()` instead.) `strip.now` respects the timebase, which can be used to advance or reset effects in a preset. This can be useful to sync multiple segments. | +| [`SEGLEN / SEG_W / SEG_H`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L116) | 16-bit | These variables are macros that help define the length and width of your LED strip/matrix segment. | +| [`SEGPALETTE`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L115) | --- | Macro that gets the currently selected palette for the currently processing segment. | +| [`hw_random8()`](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/fcn_declare.h#L548) | 8-bit | One of several functions that generates a random integer. (All of the "hw_" functions are similar to the FastLED library's random functions, but in WLED they use true hardware-based randomness instead of a pseudo random number. In short, they are better and faster.) | +| [`SEGCOLOR(x)`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L114) | 32-bit | Macro that gets user-selected colors from UI, where x is an integer 1, 2, or 3 for primary, secondary, and tertiary colors, respectively. | +| [`SEGMENT.setPixelColor`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) / [`setPixelColorXY`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_2Dfcn.cpp) | 32-bit | Function that paints one pixel. `setPixelColor` is 1‑D; `setPixelColorXY` expects `(x, y)` and an RGBW color value. | +| [`SEGMENT.color_wheel()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1092) | 32-bit | Input 0–255 to get a color. Transitions r→g→b→r. In HSV terms, `pos` is H. Note: only returns palette color unless the Default palette is selected. | +| [`SEGMENT.color_from_palette()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1093) | 32-bit | Gets a single color from the currently selected palette for a segment. (This function which should be favoured over `ColorFromPalette()` because this function returns an RGBW color with white from the `SEGCOLOR` passed, while also respecting the setting for palette wrapping. On the other hand, `ColorFromPalette()` simply gets the RGB palette color.) | +| [`fade_out()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1012) | --- | fade out function, higher rate = quicker fade. fading is highly dependent on frame rate (higher frame rates, faster fading). each frame will fade at max 9% or as little as 0.8%. | +| [`fadeToBlackBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | can be used to fade all pixels to black. | +| [`fadeToSecondaryBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | fades all pixels to secondary color. | +| [`move()`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) | --- | Moves/shifts pixels in the desired direction. | +| [`blur / blur2d`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1053) | --- | Blurs all pixels for the desired segment. Blur also has the boolean option `smear`, which, when activated, does not fade the blurred pixel(s). | + + You will see how these syntax elements work in the examples below. + + + +## Understanding 2D WLED Effects + +In this section we give some advice to those who are new to WLED Effect creation. We will illustrate how to load in multiple Effects using this single usermod, and we will do a deep dive into the anatomy of a 1D Effect as well as a 2D Effect. +(Special thanks to @mryndzionek for offering this "Diffusion Fire" 2D Effect for this tutorial.) + +### Imports +The first line of the code imports the [wled.h](https://github.com/wled/WLED/blob/main/wled00/wled.h) file into this module. Importing `wled.h` brings all of the variables, files, and functions listed in the table above (and more) into your custom effect for you to use. + +```cpp +#include "wled.h" +``` + +### Static Effect Definition +The next code block is the `mode_static` definition. This is usually left as `SEGMENT.fill(SEGCOLOR(0));` to leave all pixels off if the effect fails to load, but in theory one could use this as a 'fallback effect' to take on a different behavior, such as displaying some other color instead of leaving the pixels off. + +`FX_FALLBACK_STATIC` is a macro that calls `mode_static()` and then returns. + +### User Effect Definitions +Pre-loaded in this template is an example 2D Effect called "Diffusion Fire". (This is the name that would be shown in the UI once the binary is compiled and run on your device, as defined in the metadata string.) +The effect starts off by checking to see if the segment that the effect is being applied to is a 2D Matrix, and if it is not, then it runs the static effect which displays no pattern: +```cpp +if (!strip.isMatrix || !SEGMENT.is2D()) + FX_FALLBACK_STATIC; // not a 2D set-up +``` +The next code block contains several constant variable definitions which essentially serve to extract the dimensions of the user's 2D matrix and allow WLED to interpret the matrix as a 1D coordinate system (WLED must do this for all 2D animations): +```cpp +const int cols = SEG_W; +const int rows = SEG_H; +const auto XY = [&](int x, int y) { return x + y * cols; }; +``` +* The first line assigns the number of columns (width) in the active segment to cols. + * SEG_W is a macro defined in WLED that expands to SEGMENT.width(). This value is the width of your 2D matrix segment, used to traverse the matrix correctly. +* Next, we assign the number of rows (height) in the segment to rows. + * SEG_H is a macro for SEGMENT.height(). Combined with cols, this allows pixel addressing in 2D (x, y) space. +* The third line declares a lambda function named `XY` to map (x, y) matrix coordinates into a 1D index in the LED array. This assumes row-major order (left to right, top to bottom). + * This lambda helps with mapping a local 1D array to a 2D one. + +The next lines of code further the setup process by defining variables that allow the effect's settings to be configurable using the UI sliders (or alternatively, through API calls): +```cpp +const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80); +const unsigned refresh_ms = 1000 / refresh_hz; +const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100); +const uint8_t spark_rate = SEGMENT.intensity; +const uint8_t turbulence = SEGMENT.custom2; +``` +* The first line maps the SEGMENT.speed (user-controllable parameter from 0–255) to a value between 20 and 80 Hz. + * This determines how often the effect should refresh per second (Higher speed = more frames per second). +* Next we convert refresh rate from Hz to milliseconds. (It’s easier to schedule animation updates in WLED using elapsed time in milliseconds.) + * This value is used to time when to update the effect. +* The third line utilizes the `custom1` control (0–255 range, usually exposed via sliders) to define the diffusion rate, mapped to 0–100. + * This controls how much "heat" spreads to neighboring pixels — more diffusion = smoother flame spread. +* Next we assign `SEGMENT.intensity` (user input 0–255) to a variable named `spark_rate`. + * This controls how frequently new "spark" pixels appear at the bottom of the matrix. + * A higher value means more frequent ignition of flame points. +* The final line stores the user-defined `custom2` value to a variable called `turbulence`. + * This is used to introduce randomness in spark generation or flow — more turbulence means more chaotic behavior. + +Next we will look at some lines of code that handle memory allocation and effect initialization: + +```cpp +unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D +``` +* This part calculates how much memory we need to represent per-pixel state. + * `cols * rows` or `(or SEGLEN)` returns the total number of pixels in the current segment. + * This fire effect models heat values per pixel (not just colors), so we need persistent storage — one uint8_t per pixel — for the entire effect. + > **_NOTE:_** Virtual lengths `vWidth()` and `vHeight()` will be evaluated differently based on your own custom effect, and based on what other settings are active. For example: If you have an LED strip of length = 60 and you enable grouping = 2, then the virtual length will be 30, so the FX will render 30 pixels instead of 60. This is also true for mirroring or adding gaps--it halves the size. For a 1D strip mapped to 2D, the virtual length depends on selected mode. Keep these things in mind during your custom effect's creation. + +```cpp +if (!SEGENV.allocateData(dataSize)) + FX_FALLBACK_STATIC; // allocation failed +``` +* Upon the first call, this section allocates a persistent data buffer tied to the segment environment (`SEGENV.data`). All subsequent calls simply ensure that the data is still valid. +* The syntax `SEGENV.allocateData(n)` requests a buffer of size n bytes (1 byte per pixel here). +* If allocation fails (e.g., out of memory), it returns false, and the effect can’t proceed. +* It calls previously defined `mode_static()` fallback effect, which just fills the segment with a static color. We need to do this because WLED needs a fail-safe behavior if a custom effect can't run properly due to memory constraints. + + +The next lines of code clear the LEDs and initialize timing: +```cpp +if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + SEGENV.step = 0; +} +``` +* The first line checks whether this is the first time the effect is being run; `SEGENV.call` is a counter for how many times this effect function has been invoked since it started. +* If `SEGENV.call` equals 0 (which it does on the very first call, making it useful for initialization), then it clears the LED segment by filling it with black (turns off all LEDs). +* This gives a clean starting point for the fire animation. +* It also initializes `SEGENV.step`, a timing marker, to 0. This value is later used as a timestamp to control when the next animation frame should occur (based on elapsed time). + + +The next block of code is where the animation update logic starts to kick in: +```cpp +if ((strip.now - SEGENV.step) >= refresh_ms) { + uint8_t tmp_row[cols]; // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch. + SEGENV.step = strip.now; + // scroll up + for (unsigned y = 1; y < rows; y++) + for (unsigned x = 0; x < cols; x++) { + unsigned src = XY(x, y); + unsigned dst = XY(x, y - 1); + SEGENV.data[dst] = SEGENV.data[src]; + } +``` +* The first line checks if it's time to update the effect frame. `strip.now` is the current timestamp in milliseconds; `SEGENV.step` is the last update time (set during initialization or previous frame). `refresh_ms` is how long to wait between frames, computed earlier based on SEGMENT.speed. +* The conditional statement in the first line of code ensures the effect updates on a fixed interval — e.g., every 20 ms for 50 Hz. +* The second line of code declares a temporary row buffer for intermediate diffusion results that is one byte per column (horizontal position), so this buffer holds one row's worth of heat values. +* You'll see later that it writes results here before updating `SEGENV.data`. + * Note: this is allocated on the stack each frame. Keep such VLAs ≤ ~1 KiB; for larger sizes, prefer a buffer in `SEGENV.data`. + +> **_IMPORTANT NOTE:_** Creating variable‑length arrays (VLAs) is non‑standard C++, but this practice is used throughout WLED and works in practice. But be aware that VLAs live on the stack, which is limited. If the array scales with segment length (1D), it can overflow the stack and crash. Keep VLAs ≲ ~1 KiB; an array with 4000 LEDs is ~4 KiB and will likely crash. It’s worse with `uint16_t`. Anything larger than ~1 KiB should go into `SEGENV.data`, which has a higher limit. + + +Now we get to the spark generation portion, where new bursts of heat appear at the bottom of the matrix: +```cpp +if (hw_random8() > turbulence) { + // create new sparks at bottom row + for (unsigned x = 0; x < cols; x++) { + uint8_t p = hw_random8(); + if (p < spark_rate) { + unsigned dst = XY(x, rows - 1); + SEGENV.data[dst] = 255; + } + } +} +``` +* The first line randomizes whether we even attempt to spawn sparks this frame. + * `hw_random8()` gives a random number between 0–255 using a fast hardware RNG. + * `turbulence` is a user-controlled parameter (SEGMENT.custom2, set earlier). + * Higher turbulence means this block is less likely to run (because `hw_random8()` is less likely to exceed a high threshold). + * This adds randomness to when sparks appear — simulating natural flicker and chaotic fire. +* The next line loops over all columns in the bottom row (row `rows - 1`). +* Another random number, `p`, is used to probabilistically decide whether a spark appears at this (x, `rows-1`) position. +* Next is a conditional statement. The lower spark_rate is, the fewer sparks will appear. + * `spark_rate` comes from `SEGMENT.intensity` (0–255). + * High intensity means more frequent ignition. +* `dst` calculates the destination index in the bottom row at column x. +* The final line here sets the heat at this pixel to maximum (255). + * This simulates a fresh burst of flame, which will diffuse and move upward over time in subsequent frames. + +Next we reach the first part of the core of the fire simulation, which is diffusion (how heat spreads to neighboring pixels): +```cpp +// diffuse +for (unsigned y = 0; y < rows; y++) { + for (unsigned x = 0; x < cols; x++) { + unsigned v = SEGENV.data[XY(x, y)]; + if (x > 0) { + v += SEGENV.data[XY(x - 1, y)]; + } + if (x < (cols - 1)) { + v += SEGENV.data[XY(x + 1, y)]; + } + tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion))); + } +``` +* This block of code starts by looping over each row from top to bottom. (We will do diffusion for each pixel row.) +* Next we start an inner loop which iterates across each column in the current row. +* Starting with the current heat value of pixel (x, y) assigned `v`: + * if there’s a pixel to the left, add its heat to the total. + * If there’s a pixel to the right, add its heat as well. + * So essentially, what the two `if` statements accomplish is: `v = center + left + right`. +* The final line of code applies diffusion smoothing: + * The denominator controls how much the neighboring heat contributes. `300 + diffusion` means that with higher diffusion, you get more smoothing (since the sum is divided more). + * The `v * 100` scales things before dividing (preserving some dynamic range). + * `min(255, ...)` clamps the result to 8-bit range. + * This entire line of code stores the smoothed heat into the temporary row buffer. + +After calculating tmp_row, we now handle rendering the pixels by updating the actual segment data and turning 'heat' into visible colors: +```cpp + for (unsigned x = 0; x < cols; x++) { + SEGENV.data[XY(x, y)] = tmp_row[x]; + if (SEGMENT.check1) { + uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0); + SEGMENT.setPixelColorXY(x, y, color); + } else { + uint32_t base = SEGCOLOR(0); + SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x])); + } + } +} +``` +* This next loop starts iterating over each row from top to bottom. (We're now doing this for color-rendering for each pixel row.) +* Next we update the main segment data with the smoothed value for this pixel. +* The if statement creates a conditional rendering path — the user can toggle this. If `check1` is enabled in the effect metadata, we use a color palette to display the flame. +* The next line converts the heat value (`tmp_row[x]`) into a `color` from the current palette with 255 brightness, and no wrapping in palette lookup. + * This creates rich gradient flames (e.g., yellow → red → black). +* Finally we set the rendered color for the pixel (x, y). + * This repeats for each pixel in each row. +* If palette use is disabled, we fallback to fading a base color. +* `SEGCOLOR(0)` gets the first user-selected color for the segment. +* The final line of code fades that base color according to the heat value (acts as brightness multiplier). + +* Even though the effect logic itself controls when to update based on refresh_ms, WLED will still call this function at roughly FRAMETIME intervals (the FPS limit set in config) to check whether an update is needed. If nothing needs to change, the frame still needs to be re-rendered so color or brightness transitions will be smooth. + +If you want to run your effect at a fixed frame rate you can use the following code to not update your effect state, be aware however that transitions for your effect will also run at this frame rate - for example if you limit your effect to say 5 FPS, brightness changes and color changes may not look smooth. Also `SEGMENT.call` is still incremented on each function call. +```cpp +//limit update rate +if (strip.now - SEGENV.step < FRAMETIME_FIXED) return; +SEGENV.step = strip.now; +``` + +### The Metadata String +At the end of every effect is an important line of code called the **metadata string**. +It defines how the effect is to be interacted with in the UI: +```cpp +static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35"; +``` +This metadata string is passed into `strip.addEffect()` and parsed by WLED to determine how your effect appears and behaves in the UI. +The string follows the syntax of `;;;;`, where Effect Parameters are specified by a comma-separated list. +The values for Effect Parameters will always follow the convention in the table below: + +| Parameter | Default tooltip label | +| :-------- | :-------------------- | +| sx | Effect Speed | +| ix | Effect Intensity | +| c1 | Custom 1 | +| c2 | Custom 2 | +| c3 | Custom 3 | +| o1 | Checkbox 1 | +| o2 | Checkbox 2 | +| o3 | Checkbox 3 | + +Using this info, let’s split the Metadata string above into logical sections: + +| Syntax Element | Description | +| :---------------------------------------------- | :---------- | +| "Diffusion Fire@! | Name. (The @ symbol marks the end of the Effect Name, and the beginning of the Parameter String elements.) | +| !, | Use default UI entry; for the first space, this will automatically create a slider for Speed | +| Spark rate, Diffusion Speed, Turbulence, | UI sliders for Spark Rate, Diffusion Speed, and Turbulence. Defining slider 2 as "Spark Rate" overwrites the default value of Intensity. | +| (blank), | unused (empty field with not even a space) | +| Use palette; | This occupies the spot for the 6th effect parameter, which automatically makes this a checkbox argument `o1` called Use palette in the UI. When this is enabled, the effect uses `SEGMENT.color_from_palette(...)` (RGBW-aware, respects wrap), otherwise it fades from `SEGCOLOR(0)`. The first semicolon marks the end of the Effect Parameters and the beginning of the `Colors` parameter. | +| Color; | Custom color field `(SEGCOLOR(0))` | +| (blank); | Empty means the effect does not allow Palettes to be selected by the user. But used in conjunction with the checkbox argument, palette use can be turned on/off by the user. | +| 2; | Flag specifying that the effect requires a 2D matrix setup | +| pal=35" | Default Palette ID. this is the setting that the effect starts up with. | + +More information on metadata strings can be found [here](https://kno.wled.ge/interfaces/json-api/#effect-metadata). + + +## Understanding 1D WLED Effects + +Next, we will look at a 1D WLED effect called `Sinelon`. This one is an especially interesting example because it shows how a single effect function can be used to create several different selectable effects in the UI. +We will break this effect down step by step. +(This effect was originally one of the FastLED example effects; more information on FastLED can be found [here](https://fastled.io/).) + +```cpp +static void sinelon_base(bool dual, bool rainbow=false) { +``` +* The first line of code defines `sinelon base` as static helper function. This is how all effects are initially defined. +* Notice that it has some optional flags; these parameters will allow us to easily define the effect in different ways in the UI. + +```cpp + if (SEGLEN <= 1) FX_FALLBACK_STATIC; +``` +* If segment length ≤ 1, there’s nothing to animate. Just show static mode. + +The line of code helps create the "Fade Out" Trail: +```cpp + SEGMENT.fade_out(SEGMENT.intensity); +``` +* Gradually dims all LEDs each frame using SEGMENT.intensity as fade amount. +* Creates the trailing "comet" effect by leaving a fading path behind the moving dot. + +Next, the effect computes some position information for the actively changing pixel, and the rest of the pixels as well: +```cpp + unsigned pos = beatsin16_t(SEGMENT.speed/10, 0, SEGLEN-1); + if (SEGENV.call == 0) SEGENV.aux0 = pos; +``` +* Calculates a sine-based oscillation to move the dot smoothly back and forth. + * `beatsin16_t` is an improved version of FastLED’s beatsin16 function, generating smooth oscillations + * SEGMENT.speed / 10: affects oscillation speed. Higher = faster. + * 0: minimum position. + * SEGLEN-1: maximum position. +* On first call `(SEGENV.call == 0)`, stores initial position in `SEGENV.aux0`. (`SEGENV.aux0` is a temporary state variable to keep track of last position.) + +The next lines of code help determine the colors to be used: +```cpp + uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0); + uint32_t color2 = SEGCOLOR(2); +``` +* `color1`: main moving dot color, chosen from palette using the current position as index. +* `color2`: secondary color from user-configured color slot 2. + +The next part takes into account the optional argument for if a Rainbow colored palette is in use: +```cpp + if (rainbow) { + color1 = SEGMENT.color_wheel((pos & 0x07) * 32); + } +``` +* If `rainbow` is true, override color1 using a rainbow wheel, producing rainbow cycling colors. +* `(pos & 0x07) * 32` ensures the color changes gradually with position. + +```cpp + SEGMENT.setPixelColor(pos, color1); +``` +* Lights up the computed position with the selected color. + +The next line takes into account another one of the optional arguments for the effect to potentially handle dual mirrored dots which create the animation: +```cpp + if (dual) { + if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0); + if (rainbow) color2 = color1; // share rainbow color + SEGMENT.setPixelColor(SEGLEN-1-pos, color2); + } +``` +* If dual is true: + * Uses `color2` for mirrored dot on opposite side. + * If `color2` is not set (0), fallback to same palette color as `color1`. + * In `rainbow` mode, force both dots to share the rainbow color. + * Sets pixel at `SEGLEN-1-pos` to `color2`. + +This final part of the effect function will fill in the 'trailing' pixels to complete the animation: +```cpp + if (SEGENV.aux0 < pos) { + for (unsigned i = SEGENV.aux0; i < pos ; i++) { + SEGMENT.setPixelColor(i, color1); + if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); + } + } else { + for (unsigned i = SEGENV.aux0; i > pos ; i--) { + SEGMENT.setPixelColor(i, color1); + if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); + } + } + SEGENV.aux0 = pos; + } +``` +* The first line checks if current position has changed since last frame. (Prevents holes if the dot moves quickly and "skips" pixels.) If the position has changed, then it will implement the logic to update the rest of the pixels. +* Fills in all pixels between previous position (SEGENV.aux0) and new position (pos) to ensure smooth continuous trail. + * Works in both directions: Forward (if new pos > old pos), and Backward (if new pos < old pos). +* Updates `SEGENV.aux0` to current position at the end. + +The last part of this effect has the Wrapper functions for different Sinelon modes. +Notice that there are three different modes that we can define from the single effect definition by leveraging the arguments in the function: +```cpp +void mode_sinelon(void) { + sinelon_base(false); +} +// Calls sinelon_base with dual = false and rainbow = false + +void mode_sinelon_dual(void) { + sinelon_base(true); +} +// Calls sinelon_base with dual = true and rainbow = false + +void mode_sinelon_rainbow(void) { + sinelon_base(false, true); +} +// Calls sinelon_base with dual = false and rainbow = true +``` + +And then the last part defines the metadata strings for each effect to specify how it will be portrayed in the UI: +```cpp +static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!"; +static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!"; +static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!"; +``` +Refer to the section above for guidance on understanding metadata strings. + + +### The UserFxUsermod Class + +The `UserFxUsermod` class registers the `mode_diffusionfire` effect with WLED. This section starts right after the effect function and metadata string, and is responsible for making the effect usable in the WLED interface: +```cpp +class UserFxUsermod : public Usermod { + private: + public: + void setup() override { + strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); + + //////////////////////////////////////// + // add your effect function(s) here // + //////////////////////////////////////// + + // use id=255 for all custom user FX (the final id is assigned when adding the effect) + + // strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT); + // strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2); + // strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3); + } + void loop() override {} // nothing to do in the loop + uint16_t getId() override { return USERMOD_ID_USER_FX; } +}; +``` +* The first line declares a new class called UserFxUsermod. It inherits from `Usermod`, which is the base class WLED uses for any pluggable user-defined modules. + * This makes UserFxUsermod a valid WLED extension that can hook into `setup()`, `loop()`, and other lifecycle events. +* The `void setup()` function runs once when WLED initializes the usermod. + * It's where you should register your effects, initialize hardware, or do any other setup logic. + * `override` ensures that this matches the Usermod base class definition. +* The `strip.addEffect` line is an important one that registers the custom effect so WLED knows about it. + * 255: Temporary ID — WLED will assign a unique ID automatically. (**Create all custom effects with the 255 ID.**) + * `&mode_diffusionfire`: Pointer to the effect function. + * `_data_FX_MODE_DIFFUSIONFIRE`: Metadata string stored in PROGMEM, describing the effect name and UI fields (like sliders). + * After this, your custom effect shows up in the WLED effects list. +* The `loop()` function remains empty because this usermod doesn’t need to do anything continuously. WLED still calls this every main loop, but nothing is done here. + * If your usermod had to respond to input or update state, you'd do it here. +* The last part returns a unique ID constant used to identify this usermod. + * USERMOD_ID_USER_FX is defined in [const.h](https://github.com/wled/WLED/blob/main/wled00/const.h). WLED uses this for tracking, debugging, or referencing usermods internally. + +The final part of this file handles instantiation and initialization: +```cpp +static UserFxUsermod user_fx; +REGISTER_USERMOD(user_fx); +``` +* The first line creates a single, global instance of your usermod class. +* The last line is a macro that tells WLED: “This is a valid usermod — load it during startup.” + * WLED adds it to the list of active usermods, calls `setup()` and `loop()`, and lets it interact with the system. + + + +## Combining Multiple Effects in this Usermod + +So now let's say that you wanted add the effects "Diffusion Fire" and "Sinelon" through this same Usermod file: +* Navigate to [the code for Sinelon](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/FX.cpp#L3110). +* Copy this code, and place it below the metadata string for Diffusion Fire. Be sure to get the metadata string as well--and to name it something different than what's already inside the core WLED code. (Refer to the metadata String section above for more information.) +* Register the effect using the `addEffect` function in the Usermod class. +* Compile the code! + +## Compiling +Compiling WLED yourself is beyond the scope of this tutorial, but [the complete guide to compiling WLED can be found here](https://kno.wled.ge/advanced/compiling-wled/), on the official WLED documentation website. + +## Change Log + +### Version 1.0.0 + +* First version of the custom effect creation guide + +## Contact Us + +This custom effect tutorial guide is still in development. +If you have suggestions on what should be added, or if you've found any parts of this guide which seem incorrect, feel free to reach out [here](mailto:aregis1992@gmail.com) and help us improve this guide for future creators. diff --git a/usermods/user_fx/library.json b/usermods/user_fx/library.json new file mode 100644 index 0000000000..83f6358bf7 --- /dev/null +++ b/usermods/user_fx/library.json @@ -0,0 +1,4 @@ +{ + "name": "user_fx", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp new file mode 100644 index 0000000000..bcff5a0190 --- /dev/null +++ b/usermods/user_fx/user_fx.cpp @@ -0,0 +1,140 @@ +#include "wled.h" + +// for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata + +// static effect, used if an effect fails to initialize +static void mode_static(void) { + SEGMENT.fill(SEGCOLOR(0)); +} + +#define FX_FALLBACK_STATIC { mode_static(); return; } + +// If you define configuration options in your class and need to reference them in your effect function, add them here. +// If you only need to use them in your class you can define them as class members instead. +// bool myConfigValue = false; + +///////////////////////// +// User FX functions // +///////////////////////// + +// Diffusion Fire: fire effect intended for 2D setups smaller than 16x16 +static void mode_diffusionfire(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) + FX_FALLBACK_STATIC; // not a 2D set-up + + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return x + y * cols; }; + + const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80); + const unsigned refresh_ms = 1000 / refresh_hz; + const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100); + const uint8_t spark_rate = SEGMENT.intensity; + const uint8_t turbulence = SEGMENT.custom2; + +unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D + if (!SEGENV.allocateData(dataSize)) + FX_FALLBACK_STATIC; // allocation failed + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + SEGENV.step = 0; + } + + if ((strip.now - SEGENV.step) >= refresh_ms) { + // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch. + uint8_t tmp_row[cols]; + SEGENV.step = strip.now; + // scroll up + for (unsigned y = 1; y < rows; y++) + for (unsigned x = 0; x < cols; x++) { + unsigned src = XY(x, y); + unsigned dst = XY(x, y - 1); + SEGENV.data[dst] = SEGENV.data[src]; + } + + if (hw_random8() > turbulence) { + // create new sparks at bottom row + for (unsigned x = 0; x < cols; x++) { + uint8_t p = hw_random8(); + if (p < spark_rate) { + unsigned dst = XY(x, rows - 1); + SEGENV.data[dst] = 255; + } + } + } + + // diffuse + for (unsigned y = 0; y < rows; y++) { + for (unsigned x = 0; x < cols; x++) { + unsigned v = SEGENV.data[XY(x, y)]; + if (x > 0) { + v += SEGENV.data[XY(x - 1, y)]; + } + if (x < (cols - 1)) { + v += SEGENV.data[XY(x + 1, y)]; + } + tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion))); + } + + for (unsigned x = 0; x < cols; x++) { + SEGENV.data[XY(x, y)] = tmp_row[x]; + if (SEGMENT.check1) { + uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0); + SEGMENT.setPixelColorXY(x, y, color); + } else { + uint32_t base = SEGCOLOR(0); + SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x])); + } + } + } + } +} +static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35"; + + +///////////////////// +// UserMod Class // +///////////////////// + +class UserFxUsermod : public Usermod { + private: + public: + void setup() override { + strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); + + //////////////////////////////////////// + // add your effect function(s) here // + //////////////////////////////////////// + + // use id=255 for all custom user FX (the final id is assigned when adding the effect) + + // strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT); + // strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2); + // strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3); + } + + + /////////////////////////////////////////////////////////////////////////////////////////////// + // If you want configuration options in the usermod settings page, implement these methods // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // void addToConfig(JsonObject& root) override + // { + // JsonObject top = root.createNestedObject(FPSTR("User FX")); + // top["myConfigValue"] = myConfigValue; + // } + // bool readFromConfig(JsonObject& root) override + // { + // JsonObject top = root[FPSTR("User FX")]; + // bool configComplete = !top.isNull(); + // configComplete &= getJsonValue(top["myConfigValue"], myConfigValue); + // return configComplete; + // } + + void loop() override {} // nothing to do in the loop + uint16_t getId() override { return USERMOD_ID_USER_FX; } +}; + +static UserFxUsermod user_fx; +REGISTER_USERMOD(user_fx); diff --git a/usermods/usermod_rotary_brightness_color/README.md b/usermods/usermod_rotary_brightness_color/README.md index 06a3a0f82a..feb778dd66 100644 --- a/usermods/usermod_rotary_brightness_color/README.md +++ b/usermods/usermod_rotary_brightness_color/README.md @@ -37,6 +37,7 @@ Open Usermod Settings in WLED to change settings: No special requirements. ## Change Log - -2021-07 -* Upgraded to work with the latest WLED code, and make settings configurable in Usermod Settings +- 2021-07
+Upgraded to work with the latest WLED code, and make settings configurable in Usermod Settings +- 2025-03
+Upgraded to work with the latest WLED code diff --git a/usermods/usermod_rotary_brightness_color/library.json b/usermods/usermod_rotary_brightness_color/library.json new file mode 100644 index 0000000000..4f7a146a0b --- /dev/null +++ b/usermods/usermod_rotary_brightness_color/library.json @@ -0,0 +1,4 @@ +{ + "name": "usermod_rotary_brightness_color", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp similarity index 91% rename from usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h rename to usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp index 85a9a16054..0a485152f1 100644 --- a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h +++ b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" //v2 usermod that allows to change brightness and color using a rotary encoder, @@ -95,9 +93,9 @@ class RotaryEncoderBrightnessColor : public Usermod } else { - fastled_col.red = col[0]; - fastled_col.green = col[1]; - fastled_col.blue = col[2]; + fastled_col.red = colPri[0]; + fastled_col.green = colPri[1]; + fastled_col.blue = colPri[2]; prim_hsv = rgb2hsv_approximate(fastled_col); new_val = (int16_t)prim_hsv.h + fadeAmount; if (new_val > 255) @@ -106,9 +104,9 @@ class RotaryEncoderBrightnessColor : public Usermod 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; + colPri[0] = fastled_col.red; + colPri[1] = fastled_col.green; + colPri[2] = fastled_col.blue; } } else if (Enc_B == LOW) @@ -120,9 +118,9 @@ class RotaryEncoderBrightnessColor : public Usermod } else { - fastled_col.red = col[0]; - fastled_col.green = col[1]; - fastled_col.blue = col[2]; + fastled_col.red = colPri[0]; + fastled_col.green = colPri[1]; + fastled_col.blue = colPri[2]; prim_hsv = rgb2hsv_approximate(fastled_col); new_val = (int16_t)prim_hsv.h - fadeAmount; if (new_val > 255) @@ -131,9 +129,9 @@ class RotaryEncoderBrightnessColor : public Usermod 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; + colPri[0] = fastled_col.red; + colPri[1] = fastled_col.green; + colPri[2] = fastled_col.blue; } } //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) @@ -187,3 +185,7 @@ class RotaryEncoderBrightnessColor : public Usermod return configComplete; } }; + + +static RotaryEncoderBrightnessColor usermod_rotary_brightness_color; +REGISTER_USERMOD(usermod_rotary_brightness_color); \ No newline at end of file diff --git a/usermods/usermod_v2_HttpPullLightControl/library.json b/usermods/usermod_v2_HttpPullLightControl/library.json new file mode 100644 index 0000000000..870753b994 --- /dev/null +++ b/usermods/usermod_v2_HttpPullLightControl/library.json @@ -0,0 +1,4 @@ +{ + "name": "usermod_v2_HttpPullLightControl", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_v2_HttpPullLightControl/readme.md b/usermods/usermod_v2_HttpPullLightControl/readme.md index eb56d505d2..d86ece4d91 100644 --- a/usermods/usermod_v2_HttpPullLightControl/readme.md +++ b/usermods/usermod_v2_HttpPullLightControl/readme.md @@ -5,7 +5,7 @@ The `usermod_v2_HttpPullLightControl` is a custom user module for WLED that enab ## Features * Configure the URL endpoint (only support HTTP for now, no HTTPS) and polling interval via the WLED user interface. -* All options from the JSON API are supported (since v0.0.3). See: https://kno.wled.ge/interfaces/json-api/ +* All options from the JSON API are supported (since v0.0.3). See: [https://kno.wled.ge/interfaces/json-api/](https://kno.wled.ge/interfaces/json-api/) * The ability to control the brightness of all lights and the state (on/off) and color of individual lights remotely. * Start or stop an effect and when you run the same effect when its's already running, it won't restart. * The ability to control all these settings per segment. @@ -13,13 +13,15 @@ The `usermod_v2_HttpPullLightControl` is a custom user module for WLED that enab * Unique ID generation based on the device's MAC address and a configurable salt value, appended to the request URL for identification. ## Configuration + * Enable the `usermod_v2_HttpPullLightControl` via the WLED user interface. * Specify the URL endpoint and polling interval. ## JSON Format and examples + * The module sends a GET request to the configured URL, appending a unique identifier as a query parameter: `https://www.example.com/mycustompage.php?id=xxxxxxxx` where xxxxxxx is a 40 character long SHA1 hash of the MAC address combined with a given salt. -* Response Format (since v0.0.3) it is eactly the same as the WLED JSON API, see: https://kno.wled.ge/interfaces/json-api/ +* Response Format (since v0.0.3) it is eactly the same as the WLED JSON API, see: [https://kno.wled.ge/interfaces/json-api/](https://kno.wled.ge/interfaces/json-api/) After getting the URL (it can be a static file like static.json or a mylogic.php which gives a dynamic response), the response is read and parsed to WLED. * An example of a response to set the individual lights: 0 to RED, 12 to Green and 14 to BLUE. Remember that is will SET lights, you might want to set all the others to black. @@ -58,48 +60,51 @@ After getting the URL (it can be a static file like static.json or a mylogic.php }` * Or use the following example to start an effect, but first we UNFREEZE (frz=false) the segment because it was frozen by individual light control in the previous examples (28=Chase effect, Speed=180m Intensity=128). The three color slots are the slots you see under the color wheel and used by the effect. RED, Black, White in this case. + +```json `{ "seg": { - "frz": false, - "fx": 28, - "sx": 200, - "ix": 128, - "col": [ - "FF0000", - "000000", - "FFFFFF" - ] - } + "frz": false, + "fx": 28, + "sx": 200, + "ix": 128, + "col": [ + "FF0000", + "000000", + "FFFFFF" + ] + } }` - +``` ## Installation 1. Add `usermod_v2_HttpPullLightControl` to your WLED project following the instructions provided in the WLED documentation. 2. Compile by setting the build_flag: -D USERMOD_HTTP_PULL_LIGHT_CONTROL and upload to your ESP32/ESP8266! 3. There are several compile options which you can put in your platformio.ini or platformio_override.ini: -- -DUSERMOD_HTTP_PULL_LIGHT_CONTROL ;To Enable the usermod -- -DHTTP_PULL_LIGHT_CONTROL_URL="\"http://mydomain.com/json-response.php\"" ; The URL which will be requested all the time to set the lights/effects -- -DHTTP_PULL_LIGHT_CONTROL_SALT="\"my_very-S3cret_C0de\"" ; A secret SALT which will help by making the ID more safe -- -DHTTP_PULL_LIGHT_CONTROL_INTERVAL=30 ; The interval at which the URL is requested in seconds -- -DHTTP_PULL_LIGHT_CONTROL_HIDE_SALT ; Do you want to Hide the SALT in the User Interface? If yes, Set this flag. Note that the salt can now only be set via the above -DHTTP_PULL_LIGHT_CONTROL_SALT= setting - -- -DWLED_AP_SSID="\"Christmas Card\"" ; These flags are not just for my Usermod but you probably want to set them -- -DWLED_AP_PASS="\"christmas\"" -- -DWLED_OTA_PASS="\"otapw-secret\"" -- -DMDNS_NAME="\"christmascard\"" -- -DSERVERNAME="\"CHRISTMASCARD\"" -- -D ABL_MILLIAMPS_DEFAULT=450 -- -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs -- -D BTNPIN=41 ; The M5Stack Atom S3 Lite has a button on GPIO41 -- -D DATA_PINS=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 -- -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it -- -D IRPIN=4 ; The M5Stack Atom S3 Lite has a IR LED on GPIO4 - -- -D DEBUG=1 ; Set these DEBUG flags ONLY if you want to debug and read out Serial (using Visual Studio Code - Serial Monitor) -- -DDEBUG_LEVEL=5 -- -DWLED_DEBUG + +* -DUSERMOD_HTTP_PULL_LIGHT_CONTROL ;To Enable the usermod +* -DHTTP_PULL_LIGHT_CONTROL_URL="\"`http://mydomain.com/json-response.php`\"" ; The URL which will be requested all the time to set the lights/effects +* -DHTTP_PULL_LIGHT_CONTROL_SALT="\"my_very-S3cret_C0de\"" ; A secret SALT which will help by making the ID more safe +* -DHTTP_PULL_LIGHT_CONTROL_INTERVAL=30 ; The interval at which the URL is requested in seconds +* -DHTTP_PULL_LIGHT_CONTROL_HIDE_SALT ; Do you want to Hide the SALT in the User Interface? If yes, Set this flag. Note that the salt can now only be set via the above -DHTTP_PULL_LIGHT_CONTROL_SALT= setting + +* -DWLED_AP_SSID="\"Christmas Card\"" ; These flags are not just for my Usermod but you probably want to set them +* -DWLED_AP_PASS="\"christmas\"" +* -DWLED_OTA_PASS="\"otapw-secret\"" +* -DMDNS_NAME="\"christmascard\"" +* -DSERVERNAME="\"CHRISTMASCARD\"" +* -D ABL_MILLIAMPS_DEFAULT=450 +* -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs +* -D BTNPIN=41 ; The M5Stack Atom S3 Lite has a button on GPIO41 +* -D DATA_PINS=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 +* -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it +* -D IRPIN=4 ; The M5Stack Atom S3 Lite has a IR LED on GPIO4 + +* -D DEBUG=1 ; Set these DEBUG flags ONLY if you want to debug and read out Serial (using Visual Studio Code - Serial Monitor) +* -DDEBUG_LEVEL=5 +* -DWLED_DEBUG ## Use Case: Interactive Christmas Cards @@ -107,4 +112,4 @@ Imagine distributing interactive Christmas cards embedded with a tiny ESP32 and Your server keeps track of how many cards are active at any given time. If all 20 cards are active, your server instructs each card to light up all of its LEDs. However, if only 4 cards are active, your server instructs each card to light up only 4 LEDs. This creates a real-time interactive experience, symbolizing the collective spirit of the holiday season. Each lit LED represents a friend who's thinking about the others, and the visual feedback creates a sense of connection among the group, despite the physical distance. -This setup demonstrates a unique way to blend traditional holiday sentiments with modern technology, offering an engaging and memorable experience. \ No newline at end of file +This setup demonstrates a unique way to blend traditional holiday sentiments with modern technology, offering an engaging and memorable experience. diff --git a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp index 854cc2067f..c2254a69fa 100644 --- a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp +++ b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp @@ -4,6 +4,9 @@ const char HttpPullLightControl::_name[] PROGMEM = "HttpPullLightControl"; const char HttpPullLightControl::_enabled[] PROGMEM = "Enable"; +static HttpPullLightControl http_pull_usermod; +REGISTER_USERMOD(http_pull_usermod); + void HttpPullLightControl::setup() { //Serial.begin(115200); @@ -281,7 +284,6 @@ void HttpPullLightControl::handleResponse(String& responseStr) { if (!requestJSONBufferLock(myLockId)) { DEBUG_PRINT(F("ERROR: Can not request JSON Buffer Lock, number: ")); DEBUG_PRINTLN(myLockId); - releaseJSONBufferLock(); // Just release in any case, maybe there was already a buffer lock return; } @@ -297,10 +299,10 @@ void HttpPullLightControl::handleResponse(String& responseStr) { // Check for valid JSON, otherwise we brick the program runtime if (jsonStr[0] == '{' || jsonStr[0] == '[') { // Attempt to deserialize the JSON response - DeserializationError error = deserializeJson(doc, jsonStr); + DeserializationError error = deserializeJson(*pDoc, jsonStr); if (error == DeserializationError::Ok) { // Get JSON object from th doc - JsonObject obj = doc.as(); + JsonObject obj = pDoc->as(); // Parse the object throuhg deserializeState (use CALL_MODE_NO_NOTIFY or OR CALL_MODE_DIRECT_CHANGE) deserializeState(obj, CALL_MODE_NO_NOTIFY); } else { diff --git a/usermods/usermod_v2_RF433/library.json b/usermods/usermod_v2_RF433/library.json new file mode 100644 index 0000000000..d8de29b8a5 --- /dev/null +++ b/usermods/usermod_v2_RF433/library.json @@ -0,0 +1,7 @@ +{ + "name": "usermod_v2_RF433", + "build": { "libArchive": false }, + "dependencies": { + "sui77/rc-switch":"2.6.4" + } +} \ No newline at end of file diff --git a/usermods/usermod_v2_RF433/readme.md b/usermods/usermod_v2_RF433/readme.md new file mode 100644 index 0000000000..43919f11b5 --- /dev/null +++ b/usermods/usermod_v2_RF433/readme.md @@ -0,0 +1,18 @@ +# RF433 remote usermod + +Usermod for controlling WLED using a generic 433 / 315MHz remote and simple 3-pin receiver +See for compatibility details + +## Build + +- Create a `platformio_override.ini` file at the root of the wled source directory if not already present +- Copy the `433MHz RF remote example for esp32dev` section from `platformio_override.sample.ini` into it +- Duplicate/adjust for other boards + +## Usage + +- Connect receiver to a free pin +- Set pin in Config->Usermods +- Info pane will show the last received button code +- Upload the remote433.json sample file in this folder to the ESP with the file editor at [http://\[wled-ip\]/edit](http://ip/edit) +- Edit as necessary, the key is the button number retrieved from the info pane, and the "cmd" can be either an [HTTP API](https://kno.wled.ge/interfaces/http-api/) or a [JSON API](https://kno.wled.ge/interfaces/json-api/) command. \ No newline at end of file diff --git a/usermods/usermod_v2_RF433/remote433.json b/usermods/usermod_v2_RF433/remote433.json new file mode 100644 index 0000000000..d5d930a819 --- /dev/null +++ b/usermods/usermod_v2_RF433/remote433.json @@ -0,0 +1,34 @@ +{ + "13985576": { + "cmnt": "Toggle Power using HTTP API", + "cmd": "T=2" + }, + "3670817": { + "cmnt": "Force Power ON using HTTP API", + "cmd": "T=1" + }, + "13985572": { + "cmnt": "Set brightness to 200 using JSON API", + "cmd": {"bri":200} + }, + "3670818": { + "cmnt": "Run Preset 1 using JSON API", + "cmd": {"ps":1} + }, + "13985570": { + "cmnt": "Increase brightness by 40 using HTTP API", + "cmd": "A=~40" + }, + "13985569": { + "cmnt": "Decrease brightness by 40 using HTTP API", + "cmd": "A=~-40" + }, + "7608836": { + "cmnt": "Start 1min timer using JSON API", + "cmd": {"nl":{"on":true,"dur":1,"mode":0}} + }, + "7608840": { + "cmnt": "Select random effect on all segments using JSON API", + "cmd": {"seg":{"fx":"r"}} + } +} \ No newline at end of file diff --git a/usermods/usermod_v2_RF433/usermod_v2_RF433.cpp b/usermods/usermod_v2_RF433/usermod_v2_RF433.cpp new file mode 100644 index 0000000000..ed3d5eb26f --- /dev/null +++ b/usermods/usermod_v2_RF433/usermod_v2_RF433.cpp @@ -0,0 +1,183 @@ +#include "wled.h" +#include "Arduino.h" +#include + +#define RF433_BUSWAIT_TIMEOUT 24 + +class RF433Usermod : public Usermod +{ +private: + RCSwitch mySwitch = RCSwitch(); + unsigned long lastCommand = 0; + unsigned long lastTime = 0; + + bool modEnabled = true; + int8_t receivePin = -1; + + static const char _modName[]; + static const char _modEnabled[]; + static const char _receivePin[]; + + bool initDone = false; + +public: + + void setup() + { + mySwitch.disableReceive(); + if (modEnabled) + { + mySwitch.enableReceive(receivePin); + } + initDone = true; + } + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + } + + void loop() + { + if (!modEnabled || strip.isUpdating()) + return; + + if (mySwitch.available()) + { + unsigned long receivedCommand = mySwitch.getReceivedValue(); + mySwitch.resetAvailable(); + + // Discard duplicates, limit long press repeat + if (lastCommand == receivedCommand && millis() - lastTime < 800) + return; + + lastCommand = receivedCommand; + lastTime = millis(); + + DEBUG_PRINT(F("RF433 Receive: ")); + DEBUG_PRINTLN(receivedCommand); + + if(!remoteJson433(receivedCommand)) + DEBUG_PRINTLN(F("RF433: unknown button")); + } + } + + // Add last received button to info pane + void addToJsonInfo(JsonObject &root) + { + if (!initDone) + return; // prevent crash on boot applyPreset() + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + + JsonArray switchArr = user.createNestedArray("RF433 Last Received"); // name + switchArr.add(lastCommand); + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject(FPSTR(_modName)); // usermodname + top[FPSTR(_modEnabled)] = modEnabled; + JsonArray pinArray = top.createNestedArray("pin"); + pinArray.add(receivePin); + + DEBUG_PRINTLN(F(" config saved.")); + } + + bool readFromConfig(JsonObject &root) + { + JsonObject top = root[FPSTR(_modName)]; + if (top.isNull()) + { + DEBUG_PRINT(FPSTR(_modName)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + getJsonValue(top[FPSTR(_modEnabled)], modEnabled); + getJsonValue(top["pin"][0], receivePin); + + DEBUG_PRINTLN(F("config (re)loaded.")); + + // Redo init on update + if(initDone) + setup(); + + 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_RF433; + } + + // this function follows the same principle as decodeIRJson() / remoteJson() + bool remoteJson433(int button) + { + char objKey[14]; + bool parsed = false; + + if (!requestJSONBufferLock(JSON_LOCK_REMOTE)) return false; + + sprintf_P(objKey, PSTR("\"%d\":"), button); + + unsigned long start = millis(); + while (strip.isUpdating() && millis()-start < RF433_BUSWAIT_TIMEOUT) yield(); // wait for strip to finish updating, accessing FS during sendout causes glitches + + // attempt to read command from remote.json + readObjectFromFile(PSTR("/remote433.json"), objKey, pDoc); + JsonObject fdo = pDoc->as(); + if (fdo.isNull()) { + // the received button does not exist + releaseJSONBufferLock(); + return parsed; + } + + String cmdStr = fdo["cmd"].as(); + JsonObject jsonCmdObj = fdo["cmd"]; //object + + if (jsonCmdObj.isNull()) // we could also use: fdo["cmd"].is() + { + // HTTP API command + String apireq = "win"; apireq += '&'; // reduce flash string usage + if (!cmdStr.startsWith(apireq)) cmdStr = apireq + cmdStr; // if no "win&" prefix + if (!irApplyToAllSelected && cmdStr.indexOf(F("SS="))<0) { + char tmp[10]; + sprintf_P(tmp, PSTR("&SS=%d"), strip.getMainSegmentId()); + cmdStr += tmp; + } + fdo.clear(); // clear JSON buffer (it is no longer needed) + handleSet(nullptr, cmdStr, false); // no stateUpdated() call here + stateUpdated(CALL_MODE_BUTTON); + parsed = true; + } else { + // command is JSON object + if (jsonCmdObj[F("psave")].isNull()) + deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET); + else { + uint8_t psave = jsonCmdObj[F("psave")].as(); + char pname[33]; + sprintf_P(pname, PSTR("IR Preset %d"), psave); + fdo.clear(); + if (psave > 0 && psave < 251) savePreset(psave, pname, fdo); + } + parsed = true; + } + releaseJSONBufferLock(); + return parsed; + } +}; + +const char RF433Usermod::_modName[] PROGMEM = "RF433 Remote"; +const char RF433Usermod::_modEnabled[] PROGMEM = "Enabled"; +const char RF433Usermod::_receivePin[] PROGMEM = "RX Pin"; + +static RF433Usermod usermod_v2_RF433; +REGISTER_USERMOD(usermod_v2_RF433); diff --git a/usermods/usermod_v2_animartrix/library.json b/usermods/usermod_v2_animartrix/library.json new file mode 100644 index 0000000000..667572bad9 --- /dev/null +++ b/usermods/usermod_v2_animartrix/library.json @@ -0,0 +1,7 @@ +{ + "name": "animartrix", + "build": { "libArchive": false }, + "dependencies": { + "Animartrix": "https://github.com/netmindz/animartrix.git#b172586" + } +} diff --git a/usermods/usermod_v2_animartrix/readme.md b/usermods/usermod_v2_animartrix/readme.md index 42d463c50f..f0ff60a782 100644 --- a/usermods/usermod_v2_animartrix/readme.md +++ b/usermods/usermod_v2_animartrix/readme.md @@ -6,9 +6,5 @@ CC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if yo ## Installation -Please uncomment the two references to ANIMartRIX in your platform.ini - -lib_dep to a version of https://github.com/netmindz/animartrix.git -and the build_flags -D USERMOD_ANIMARTRIX - +Add 'animartrix' to 'custom_usermods' in your platformio_override.ini. diff --git a/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h b/usermods/usermod_v2_animartrix/usermod_v2_animartrix.cpp similarity index 83% rename from usermods/usermod_v2_animartrix/usermod_v2_animartrix.h rename to usermods/usermod_v2_animartrix/usermod_v2_animartrix.cpp index d91cf6c96e..a90828d233 100644 --- a/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h +++ b/usermods/usermod_v2_animartrix/usermod_v2_animartrix.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include @@ -87,266 +85,214 @@ class ANIMartRIXMod:public ANIMartRIX { }; ANIMartRIXMod anim; -uint16_t mode_Module_Experiment10() { +void mode_Module_Experiment10() { anim.initEffect(); anim.Module_Experiment10(); - return FRAMETIME; } -uint16_t mode_Module_Experiment9() { +void mode_Module_Experiment9() { anim.initEffect(); anim.Module_Experiment9(); - return FRAMETIME; } -uint16_t mode_Module_Experiment8() { +void mode_Module_Experiment8() { anim.initEffect(); anim.Module_Experiment8(); - return FRAMETIME; } -uint16_t mode_Module_Experiment7() { +void mode_Module_Experiment7() { anim.initEffect(); anim.Module_Experiment7(); - return FRAMETIME; } -uint16_t mode_Module_Experiment6() { +void mode_Module_Experiment6() { anim.initEffect(); anim.Module_Experiment6(); - return FRAMETIME; } -uint16_t mode_Module_Experiment5() { +void mode_Module_Experiment5() { anim.initEffect(); anim.Module_Experiment5(); - return FRAMETIME; } -uint16_t mode_Module_Experiment4() { +void mode_Module_Experiment4() { anim.initEffect(); anim.Module_Experiment4(); - return FRAMETIME; } -uint16_t mode_Zoom2() { +void mode_Zoom2() { anim.initEffect(); anim.Zoom2(); - return FRAMETIME; } -uint16_t mode_Module_Experiment3() { +void mode_Module_Experiment3() { anim.initEffect(); anim.Module_Experiment3(); - return FRAMETIME; } -uint16_t mode_Module_Experiment2() { +void mode_Module_Experiment2() { anim.initEffect(); anim.Module_Experiment2(); - return FRAMETIME; } -uint16_t mode_Module_Experiment1() { +void mode_Module_Experiment1() { anim.initEffect(); anim.Module_Experiment1(); - return FRAMETIME; } -uint16_t mode_Parametric_Water() { +void mode_Parametric_Water() { anim.initEffect(); anim.Parametric_Water(); - return FRAMETIME; } -uint16_t mode_Water() { +void mode_Water() { anim.initEffect(); anim.Water(); - return FRAMETIME; } -uint16_t mode_Complex_Kaleido_6() { +void mode_Complex_Kaleido_6() { anim.initEffect(); anim.Complex_Kaleido_6(); - return FRAMETIME; } -uint16_t mode_Complex_Kaleido_5() { +void mode_Complex_Kaleido_5() { anim.initEffect(); anim.Complex_Kaleido_5(); - return FRAMETIME; } -uint16_t mode_Complex_Kaleido_4() { +void mode_Complex_Kaleido_4() { anim.initEffect(); anim.Complex_Kaleido_4(); - return FRAMETIME; } -uint16_t mode_Complex_Kaleido_3() { +void mode_Complex_Kaleido_3() { anim.initEffect(); anim.Complex_Kaleido_3(); - return FRAMETIME; } -uint16_t mode_Complex_Kaleido_2() { +void mode_Complex_Kaleido_2() { anim.initEffect(); anim.Complex_Kaleido_2(); - return FRAMETIME; } -uint16_t mode_Complex_Kaleido() { +void mode_Complex_Kaleido() { anim.initEffect(); anim.Complex_Kaleido(); - return FRAMETIME; } -uint16_t mode_SM10() { +void mode_SM10() { anim.initEffect(); anim.SM10(); - return FRAMETIME; } -uint16_t mode_SM9() { +void mode_SM9() { anim.initEffect(); anim.SM9(); - return FRAMETIME; } -uint16_t mode_SM8() { +void mode_SM8() { anim.initEffect(); anim.SM8(); - return FRAMETIME; } -// uint16_t mode_SM7() { +// void mode_SM7() { // anim.initEffect(); // anim.SM7(); // -// return FRAMETIME; // } -uint16_t mode_SM6() { +void mode_SM6() { anim.initEffect(); anim.SM6(); - return FRAMETIME; } -uint16_t mode_SM5() { +void mode_SM5() { anim.initEffect(); anim.SM5(); - return FRAMETIME; } -uint16_t mode_SM4() { +void mode_SM4() { anim.initEffect(); anim.SM4(); - return FRAMETIME; } -uint16_t mode_SM3() { +void mode_SM3() { anim.initEffect(); anim.SM3(); - return FRAMETIME; } -uint16_t mode_SM2() { +void mode_SM2() { anim.initEffect(); anim.SM2(); - return FRAMETIME; } -uint16_t mode_SM1() { +void mode_SM1() { anim.initEffect(); anim.SM1(); - return FRAMETIME; } -uint16_t mode_Big_Caleido() { +void mode_Big_Caleido() { anim.initEffect(); anim.Big_Caleido(); - return FRAMETIME; } -uint16_t mode_RGB_Blobs5() { +void mode_RGB_Blobs5() { anim.initEffect(); anim.RGB_Blobs5(); - return FRAMETIME; } -uint16_t mode_RGB_Blobs4() { +void mode_RGB_Blobs4() { anim.initEffect(); anim.RGB_Blobs4(); - return FRAMETIME; } -uint16_t mode_RGB_Blobs3() { +void mode_RGB_Blobs3() { anim.initEffect(); anim.RGB_Blobs3(); - return FRAMETIME; } -uint16_t mode_RGB_Blobs2() { +void mode_RGB_Blobs2() { anim.initEffect(); anim.RGB_Blobs2(); - return FRAMETIME; } -uint16_t mode_RGB_Blobs() { +void mode_RGB_Blobs() { anim.initEffect(); anim.RGB_Blobs(); - return FRAMETIME; } -uint16_t mode_Polar_Waves() { +void mode_Polar_Waves() { anim.initEffect(); anim.Polar_Waves(); - return FRAMETIME; } -uint16_t mode_Slow_Fade() { +void mode_Slow_Fade() { anim.initEffect(); anim.Slow_Fade(); - return FRAMETIME; } -uint16_t mode_Zoom() { +void mode_Zoom() { anim.initEffect(); anim.Zoom(); - return FRAMETIME; } -uint16_t mode_Hot_Blob() { +void mode_Hot_Blob() { anim.initEffect(); anim.Hot_Blob(); - return FRAMETIME; } -uint16_t mode_Spiralus2() { +void mode_Spiralus2() { anim.initEffect(); anim.Spiralus2(); - return FRAMETIME; } -uint16_t mode_Spiralus() { +void mode_Spiralus() { anim.initEffect(); anim.Spiralus(); - return FRAMETIME; } -uint16_t mode_Yves() { +void mode_Yves() { anim.initEffect(); anim.Yves(); - return FRAMETIME; } -uint16_t mode_Scaledemo1() { +void mode_Scaledemo1() { anim.initEffect(); anim.Scaledemo1(); - return FRAMETIME; } -uint16_t mode_Lava1() { +void mode_Lava1() { anim.initEffect(); anim.Lava1(); - return FRAMETIME; } -uint16_t mode_Caleido3() { +void mode_Caleido3() { anim.initEffect(); anim.Caleido3(); - return FRAMETIME; } -uint16_t mode_Caleido2() { +void mode_Caleido2() { anim.initEffect(); anim.Caleido2(); - return FRAMETIME; } -uint16_t mode_Caleido1() { +void mode_Caleido1() { anim.initEffect(); anim.Caleido1(); - return FRAMETIME; } -uint16_t mode_Distance_Experiment() { +void mode_Distance_Experiment() { anim.initEffect(); anim.Distance_Experiment(); - return FRAMETIME; } -uint16_t mode_Center_Field() { +void mode_Center_Field() { anim.initEffect(); anim.Center_Field(); - return FRAMETIME; } -uint16_t mode_Waves() { +void mode_Waves() { anim.initEffect(); anim.Waves(); - return FRAMETIME; } -uint16_t mode_Chasing_Spirals() { +void mode_Chasing_Spirals() { anim.initEffect(); anim.Chasing_Spirals(); - return FRAMETIME; } -uint16_t mode_Rotating_Blob() { +void mode_Rotating_Blob() { anim.initEffect(); anim.Rotating_Blob(); - return FRAMETIME; } @@ -452,5 +398,6 @@ class AnimartrixUsermod : public Usermod { }; - +static AnimartrixUsermod animartrix_module("Animartrix", false); +REGISTER_USERMOD(animartrix_module); diff --git a/usermods/usermod_v2_auto_save/library.json b/usermods/usermod_v2_auto_save/library.json new file mode 100644 index 0000000000..127767eb07 --- /dev/null +++ b/usermods/usermod_v2_auto_save/library.json @@ -0,0 +1,4 @@ +{ + "name": "auto_save", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_v2_auto_save/readme.md b/usermods/usermod_v2_auto_save/readme.md index f54d87a769..ce15d8c277 100644 --- a/usermods/usermod_v2_auto_save/readme.md +++ b/usermods/usermod_v2_auto_save/readme.md @@ -2,6 +2,7 @@ v2 Usermod to automatically save settings to preset number AUTOSAVE_PRESET_NUM after a change to any of: + * brightness * effect speed * effect intensity @@ -19,7 +20,7 @@ Note: WLED doesn't respect the brightness of the preset being auto loaded, so th ## Installation -Copy and update the example `platformio_override.ini.sample` +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`. @@ -50,6 +51,9 @@ 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.cpp similarity index 82% rename from usermods/usermod_v2_auto_save/usermod_v2_auto_save.h rename to usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp index a257413b42..fe508b1f32 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" // v2 Usermod to automatically save settings @@ -18,6 +16,10 @@ // It can be configured to load auto saved preset at startup, // during the first `loop()`. // +// By default it will not save the state if an unmodified preset +// is selected (to not duplicate it). You can change this behaviour +// by setting autoSaveIgnorePresets=false +// // AutoSaveUsermod is standalone, but if FourLineDisplayUsermod // is installed, it will notify the user of the saved changes. @@ -51,6 +53,8 @@ class AutoSaveUsermod : public Usermod { bool applyAutoSaveOnBoot = false; // do we load auto-saved preset on boot? #endif + bool autoSaveIgnorePresets = true; // ignore by default to not duplicate presets + // If we've detected the need to auto save, this will be non zero. unsigned long autoSaveAfter = 0; @@ -70,6 +74,7 @@ class AutoSaveUsermod : public Usermod { static const char _autoSaveAfterSec[]; static const char _autoSavePreset[]; static const char _autoSaveApplyOnBoot[]; + static const char _autoSaveIgnorePresets[]; void inline saveSettings() { char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; @@ -124,7 +129,8 @@ class AutoSaveUsermod : public Usermod { void loop() { static unsigned long lastRun = 0; unsigned long now = millis(); - if (!autoSaveAfterSec || !enabled || currentPreset>0 || (strip.isUpdating() && now - lastRun < 240)) return; // setting 0 as autosave seconds disables autosave + if (!autoSaveAfterSec || !enabled || (autoSaveIgnorePresets && currentPreset>0) || (strip.isUpdating() && now - lastRun < 240)) return; // setting 0 as autosave seconds disables autosave + lastRun = now; uint8_t currentMode = strip.getMainSegment().mode; uint8_t currentPalette = strip.getMainSegment().palette; @@ -221,10 +227,11 @@ class AutoSaveUsermod : public Usermod { void addToConfig(JsonObject& root) { // we add JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}} JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_autoSaveEnabled)] = enabled; - top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam - top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam - top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot; + top[FPSTR(_autoSaveEnabled)] = enabled; + top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam + top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam + top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot; + top[FPSTR(_autoSaveIgnorePresets)] = autoSaveIgnorePresets; DEBUG_PRINTLN(F("Autosave config saved.")); } @@ -247,12 +254,13 @@ class AutoSaveUsermod : public Usermod { return false; } - enabled = top[FPSTR(_autoSaveEnabled)] | enabled; - autoSaveAfterSec = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec; - autoSaveAfterSec = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking - autoSavePreset = top[FPSTR(_autoSavePreset)] | autoSavePreset; - autoSavePreset = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking - applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot; + enabled = top[FPSTR(_autoSaveEnabled)] | enabled; + autoSaveAfterSec = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec; + autoSaveAfterSec = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking + autoSavePreset = top[FPSTR(_autoSavePreset)] | autoSavePreset; + autoSavePreset = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking + applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot; + autoSaveIgnorePresets = top[FPSTR(_autoSaveIgnorePresets)] | autoSaveIgnorePresets; DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(" config (re)loaded.")); @@ -270,8 +278,12 @@ class AutoSaveUsermod : public Usermod { }; // strings to reduce flash memory usage (used more than twice) -const char AutoSaveUsermod::_name[] PROGMEM = "Autosave"; -const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled"; -const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec"; -const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset"; -const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot"; +const char AutoSaveUsermod::_name[] PROGMEM = "Autosave"; +const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled"; +const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec"; +const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset"; +const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot"; +const char AutoSaveUsermod::_autoSaveIgnorePresets[] PROGMEM = "autoSaveIgnorePresets"; + +static AutoSaveUsermod autosave; +REGISTER_USERMOD(autosave); diff --git a/usermods/usermod_v2_brightness_follow_sun/README.md b/usermods/usermod_v2_brightness_follow_sun/README.md new file mode 100644 index 0000000000..cbd87a55a5 --- /dev/null +++ b/usermods/usermod_v2_brightness_follow_sun/README.md @@ -0,0 +1,35 @@ +# Update Brightness Follow Sun + +This UserMod can set brightness by mapping [minimum-maximum-minimum] from [sunrise-suntop-sunset], I use this UserMod to adjust the brightness of my plant growth light (pwm led), and I think it will make my plants happy. + +This UserMod will adjust brightness from sunrise to sunset, reaching maximum brightness at the zenith of the sun. It can also maintain the lowest brightness within 0-6 hours before sunrise and after sunset according to the settings. + +## Installation + +define `USERMOD_BRIGHTNESS_FOLLOW_SUN` e.g. `#define USERMOD_BRIGHTNESS_FOLLOW_SUN` in my_config.h + +or add `-D USERMOD_BRIGHTNESS_FOLLOW_SUN` to `build_flags` in platformio_override.ini + +### Options + +Open Usermod Settings in WLED to change settings: + +`Enable` - When checked `Enable`, turn on the `Brightness Follow Sun` Usermod, which will automatically turn on the lights, adjust the brightness, and turn off the lights. If you need to completely turn off the lights, please unchecked `Enable`. + +`Update Interval Sec` - The unit is seconds, and the brightness will be automatically refreshed according to the set parameters. + +`Min Brightness` - set brightness by map of min-max-min : sunrise-suntop-sunset + +`Max Brightness` - It needs to be set to a value greater than `Min Brightness`, otherwise it will always remain at `Min Brightness`. + +`Relax Hour` - The unit is in hours, with an effective range of 0-6. According to the settings, maintain the lowest brightness for 0-6 hours before sunrise and after sunset. + +### PlatformIO requirements + +No special requirements. + +### Change Log + +2025-01-02 + +* init diff --git a/usermods/usermod_v2_brightness_follow_sun/library.json b/usermods/usermod_v2_brightness_follow_sun/library.json new file mode 100644 index 0000000000..dec00e55b6 --- /dev/null +++ b/usermods/usermod_v2_brightness_follow_sun/library.json @@ -0,0 +1,4 @@ +{ + "name": "brightness_follow_sun", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp b/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp new file mode 100644 index 0000000000..ff97cba468 --- /dev/null +++ b/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp @@ -0,0 +1,131 @@ +#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 UsermodBrightnessFollowSun : public Usermod +{ +private: + static const char _name[]; + static const char _enabled[]; + static const char _update_interval[]; + static const char _min_bri[]; + static const char _max_bri[]; + static const char _relax_hour[]; + +private: + bool enabled = false; //WLEDMM + unsigned long update_interval = 60; + unsigned long update_interval_ms = 60000; + int min_bri = 1; + int max_bri = 255; + float relax_hour = 0; + int relaxSec = 0; + unsigned long lastUMRun = 0; +public: + + void setup() {}; + + float mapFloat(float inputValue, float inMin, float inMax, float outMin, float outMax) { + if (inMax == inMin) + return outMin; + + inputValue = constrain(inputValue, inMin, inMax); + + return ((inputValue - inMin) * (outMax - outMin) / (inMax - inMin)) + outMin; + } + + uint16_t getId() override + { + return USERMOD_ID_BRIGHTNESS_FOLLOW_SUN; + } + + void update() + { + if (sunrise == 0 || sunset == 0 || localTime == 0) + return; + + int curSec = elapsedSecsToday(localTime); + int sunriseSec = elapsedSecsToday(sunrise); + int sunsetSec = elapsedSecsToday(sunset); + int sunMiddleSec = sunriseSec + (sunsetSec-sunriseSec)/2; + + int relaxSecH = sunriseSec-relaxSec; + int relaxSecE = sunsetSec+relaxSec; + + int briSet = 0; + if (curSec >= relaxSecH && curSec <= relaxSecE) { + float timeMapToAngle = curSec < sunMiddleSec ? + mapFloat(curSec, sunriseSec, sunMiddleSec, 0, M_PI/2.0) : + mapFloat(curSec, sunMiddleSec, sunsetSec, M_PI/2.0, M_PI); + float sinValue = sin_t(timeMapToAngle); + briSet = min_bri + (max_bri-min_bri)*sinValue; + } + + bri = briSet; + stateUpdated(CALL_MODE_DIRECT_CHANGE); +} + + void loop() override + { + if (!enabled || strip.isUpdating()) + return; + + if (millis() - lastUMRun < update_interval_ms) + return; + lastUMRun = millis(); + + update(); + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_update_interval)] = update_interval; + top[FPSTR(_min_bri)] = min_bri; + top[FPSTR(_max_bri)] = max_bri; + top[FPSTR(_relax_hour)] = relax_hour; + } + + 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 configComplete = true; + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); + configComplete &= getJsonValue(top[FPSTR(_update_interval)], update_interval, 60); + configComplete &= getJsonValue(top[FPSTR(_min_bri)], min_bri, 1); + configComplete &= getJsonValue(top[FPSTR(_max_bri)], max_bri, 255); + configComplete &= getJsonValue(top[FPSTR(_relax_hour)], relax_hour, 0); + + update_interval = constrain(update_interval, 1, SECS_PER_HOUR); + min_bri = constrain(min_bri, 1, 255); + max_bri = constrain(max_bri, 1, 255); + relax_hour = constrain(relax_hour, 0, 6); + + update_interval_ms = update_interval*1000; + relaxSec = SECS_PER_HOUR*relax_hour; + + lastUMRun = 0; + update(); + + return configComplete; + } +}; + + +const char UsermodBrightnessFollowSun::_name[] PROGMEM = "Brightness Follow Sun"; +const char UsermodBrightnessFollowSun::_enabled[] PROGMEM = "Enabled"; +const char UsermodBrightnessFollowSun::_update_interval[] PROGMEM = "Update Interval Sec"; +const char UsermodBrightnessFollowSun::_min_bri[] PROGMEM = "Min Brightness"; +const char UsermodBrightnessFollowSun::_max_bri[] PROGMEM = "Max Brightness"; +const char UsermodBrightnessFollowSun::_relax_hour[] PROGMEM = "Relax Hour"; + +static UsermodBrightnessFollowSun usermod_brightness_follow_sun; +REGISTER_USERMOD(usermod_brightness_follow_sun); 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.h similarity index 99% rename from usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c rename to usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h index 5495f91947..0fb5f3bbf8 100644 --- a/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c +++ b/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h @@ -1,7 +1,5 @@ -#pragma once - //WLED custom fonts, curtesy of @Benji (https://github.com/Proto-molecule) - +#pragma once /* Fontname: wled_logo_akemi_4x4 diff --git a/usermods/usermod_v2_four_line_display_ALT/library.json b/usermods/usermod_v2_four_line_display_ALT/library.json new file mode 100644 index 0000000000..b164482234 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/library.json @@ -0,0 +1,8 @@ +{ + "name": "four_line_display_ALT", + "build": { "libArchive": false }, + "dependencies": { + "U8g2": "~2.34.4", + "Wire": "" + } +} \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini index e596374534..f4fa8c9d8b 100644 --- a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini @@ -1,18 +1,11 @@ [platformio] -default_envs = esp32dev +default_envs = esp32dev_fld -[env:esp32dev] -board = esp32dev -platform = ${esp32.platform} -build_unflags = ${common.build_unflags} +[env:esp32dev_fld] +extends = env:esp32dev_V4 +custom_usermods = ${env:esp32dev_V4.custom_usermods} four_line_display_ALT build_flags = - ${common.build_flags_esp32} - -D USERMOD_FOUR_LINE_DISPLAY + ${env:esp32dev_V4.build_flags} -D FLD_TYPE=SH1106 -D I2CSCLPIN=27 -D I2CSDAPIN=26 - -lib_deps = - ${esp32.lib_deps} - U8g2@~2.34.4 - Wire diff --git a/usermods/usermod_v2_four_line_display_ALT/readme.md b/usermods/usermod_v2_four_line_display_ALT/readme.md index 39bb5d28e4..663c93a4a6 100644 --- a/usermods/usermod_v2_four_line_display_ALT/readme.md +++ b/usermods/usermod_v2_four_line_display_ALT/readme.md @@ -5,6 +5,7 @@ This usermod could be used in compination with `usermod_v2_rotary_encoder_ui_ALT ## Functionalities Press the encoder to cycle through the options: + * Brightness * Speed * Intensity @@ -35,15 +36,15 @@ These options are configurable in Config > Usermods * `enabled` - enable/disable usermod * `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) - * 8 = SPI SSD1309 128x64 (4 double-height lines) - * 9 = I2C SSD1309 128x64 (4 double-height lines) + * 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) + * 8 = SPI SSD1309 128x64 (4 double-height lines) + * 9 = I2C SSD1309 128x64 (4 double-height lines) * `pin` - GPIO pins used for display; SPI displays can use SCK, MOSI, CS, DC & RST * `flip` - flip/rotate display 180° * `contrast` - set display contrast (higher contrast may reduce display lifetime) @@ -53,7 +54,6 @@ These options are configurable in Config > Usermods * `showSeconds` - Show seconds on the clock display * `i2c-freq-kHz` - I2C clock frequency in kHz (may help reduce dropped frames, range: 400-3400) - ### PlatformIO requirements Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. @@ -61,4 +61,5 @@ 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.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h new file mode 100644 index 0000000000..4fc963b9c1 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h @@ -0,0 +1,315 @@ +#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/ + +#pragma once + +#ifndef FLD_ESP32_NO_THREADS + #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! +#endif + +#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 + +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 + SSD1309_64 // U8X8_SSD1309_128X64_NONAME0_HW_I2C +} 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() override; + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() override; + + /** + * Da loop. + */ + void loop() override; + + //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 being changed + void drawArrow(); + + //Display the current effect or palette (desiredEntry) + // on the appropriate line (row). + void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); + + /** + * 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) override; + + /* + * 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) override; + + /* + * 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) override; + + /* + * 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) override; + + void appendConfigData() override; + + /* + * 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) override; + + /* + * 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) override; + + /* + * 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() override { + return USERMOD_ID_FOUR_LINE_DISP; + } + }; + \ No newline at end of file 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.cpp similarity index 78% rename from usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h rename to usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp index 684dd86e46..1808a39b5e 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp @@ -1,13 +1,5 @@ -#pragma once - -#include "wled.h" -#undef U8X8_NO_HW_I2C // borrowed from WLEDMM: we do want I2C hardware drivers - if possible -#include // from https://github.com/olikraus/u8g2/ -#include "4LD_wled_fonts.c" - -#ifndef FLD_ESP32_NO_THREADS - #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! -#endif +#include "usermod_v2_four_line_display.h" +#include "4LD_wled_fonts.h" // // Inspired by the usermod_v2_four_line_display @@ -22,330 +14,18 @@ // // 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 - SSD1309_64 // U8X8_SSD1309_128X64_NONAME0_HW_I2C -} 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() override; - - // gets called every time WiFi is (re-)connected. Initialize own network - // interfaces here - void connected() override; - - /** - * Da loop. - */ - void loop() override; - - //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 being changed - void drawArrow(); - - //Display the current effect or palette (desiredEntry) - // on the appropriate line (row). - void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); - - /** - * 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) override; - - /* - * 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) override; - - /* - * 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) override; - - /* - * 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) override; - - void appendConfigData() override; - - /* - * 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) override; - - /* - * 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) override; - - /* - * 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() override { - 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"; @@ -1069,12 +749,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) { yield(); if (!enabled || b // button 0 only - || buttonType[b] == BTN_TYPE_SWITCH - || buttonType[b] == BTN_TYPE_NONE - || buttonType[b] == BTN_TYPE_RESERVED - || buttonType[b] == BTN_TYPE_PIR_SENSOR - || buttonType[b] == BTN_TYPE_ANALOG - || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { + || buttons[b].type == BTN_TYPE_SWITCH + || buttons[b].type == BTN_TYPE_NONE + || buttons[b].type == BTN_TYPE_RESERVED + || buttons[b].type == BTN_TYPE_PIR_SENSOR + || buttons[b].type == BTN_TYPE_ANALOG + || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { return false; } @@ -1386,3 +1066,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_contrastFix)].isNull(); } + + +static FourLineDisplayUsermod usermod_v2_four_line_display_alt; +REGISTER_USERMOD(usermod_v2_four_line_display_alt); diff --git a/usermods/usermod_v2_klipper_percentage/library.json b/usermods/usermod_v2_klipper_percentage/library.json new file mode 100644 index 0000000000..962dda14e7 --- /dev/null +++ b/usermods/usermod_v2_klipper_percentage/library.json @@ -0,0 +1,4 @@ +{ + "name": "usermod_v2_klipper_percentage", + "build": { "libArchive": false } +} \ 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.cpp similarity index 97% rename from usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h rename to usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.cpp index bd4170dd26..71c5c45f3f 100644 --- a/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h +++ b/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" class klipper_percentage : public Usermod @@ -219,4 +217,7 @@ class klipper_percentage : public Usermod } }; const char klipper_percentage::_name[] PROGMEM = "Klipper_Percentage"; -const char klipper_percentage::_enabled[] PROGMEM = "enabled"; \ No newline at end of file +const char klipper_percentage::_enabled[] PROGMEM = "enabled"; + +static klipper_percentage usermod_v2_klipper_percentage; +REGISTER_USERMOD(usermod_v2_klipper_percentage); \ No newline at end of file diff --git a/usermods/usermod_v2_ping_pong_clock/library.json b/usermods/usermod_v2_ping_pong_clock/library.json new file mode 100644 index 0000000000..4b272eca47 --- /dev/null +++ b/usermods/usermod_v2_ping_pong_clock/library.json @@ -0,0 +1,4 @@ +{ + "name": "usermod_v2_ping_pong_clock", + "build": { "libArchive": false } +} \ No newline at end of file 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.cpp similarity index 97% rename from usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h rename to usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.cpp index 40ff675c08..c6632b53a0 100644 --- 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.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" class PingPongClockUsermod : public Usermod @@ -117,3 +115,7 @@ class PingPongClockUsermod : public Usermod } }; + + +static PingPongClockUsermod usermod_v2_ping_pong_clock; +REGISTER_USERMOD(usermod_v2_ping_pong_clock); \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json new file mode 100644 index 0000000000..7c828d087c --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json @@ -0,0 +1,7 @@ +{ + "name": "rotary_encoder_ui_ALT", + "build": { + "libArchive": false, + "extraScript": "setup_deps.py" + } +} \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini index 8a88fd6b5f..2511d2fa38 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini @@ -1,13 +1,11 @@ [platformio] -default_envs = esp32dev +default_envs = esp32dev_re -[env:esp32dev] -board = esp32dev -platform = ${esp32.platform} -build_unflags = ${common.build_unflags} +[env:esp32dev_re] +extends = env:esp32dev_V4 +custom_usermods = ${env:esp32dev_V4.custom_usermods} rotary_encoder_ui_ALT build_flags = - ${common.build_flags_esp32} - -D USERMOD_ROTARY_ENCODER_UI + ${env:esp32dev_V4.build_flags} -D USERMOD_ROTARY_ENCODER_GPIO=INPUT -D ENCODER_DT_PIN=21 -D ENCODER_CLK_PIN=23 diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md index c46e876636..cb6150a42e 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -5,6 +5,7 @@ This usermod supports the UI of the `usermod_v2_rotary_encoder_ui_ALT`. ## Functionalities Press the encoder to cycle through the options: + * Brightness * Speed * Intensity @@ -25,10 +26,6 @@ Copy the example `platformio_override.sample.ini` to the root directory of your ### Define Your Options -* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available - (see the Four Line Display usermod `readme.md` for more details) * `ENCODER_DT_PIN` - defaults to 18 * `ENCODER_CLK_PIN` - defaults to 5 * `ENCODER_SW_PIN` - defaults to 19 @@ -43,4 +40,5 @@ No special requirements. ## Change Log 2021-10 + * First public release diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py b/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py new file mode 100644 index 0000000000..ed579bc125 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py @@ -0,0 +1,8 @@ +from platformio.package.meta import PackageSpec +Import('env') + +libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] +# Check for partner usermod +# Allow both "usermod_v2" and unqualified syntax +if any(mod in ("four_line_display_ALT", "usermod_v2_four_line_display_ALT") for mod in libs): + env.Append(CPPDEFINES=[("USERMOD_FOUR_LINE_DISPLAY")]) 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.cpp similarity index 95% rename from usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h rename to usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp index 383c1193eb..f7e908651b 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" // @@ -29,6 +27,10 @@ // * display network (long press buttion) // +#ifdef USERMOD_FOUR_LINE_DISPLAY +#include "usermod_v2_four_line_display.h" +#endif + #ifdef USERMOD_MODE_SORT #error "Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini" #endif @@ -398,20 +400,20 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() { modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); - DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(strip.getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(strip.customPalettes.size()); - palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); - palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); - if (strip.customPalettes.size()) { - for (int i=0; i> 5; + currentCCT = (approximateKelvinFromRGB(RGBW32(colPri[0], colPri[1], colPri[2], colPri[3])) - 1900) >> 5; if (!initDone) sortModesAndPalettes(); @@ -700,7 +702,7 @@ void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { effectPaletteIndex = 0; DEBUG_PRINTLN(effectPalette); - for (unsigned i = 0; i < strip.getPaletteCount()+strip.customPalettes.size(); i++) { + for (unsigned i = 0; i < getPaletteCount()+customPalettes.size(); i++) { if (palettes_alpha_indexes[i] == effectPalette) { effectPaletteIndex = i; DEBUG_PRINTLN(F("Found palette.")); @@ -890,7 +892,7 @@ void RotaryEncoderUIUsermod::changePalette(bool increase) { } display->updateRedrawTime(); #endif - effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), strip.getPaletteCount()+strip.customPalettes.size()-1), 0U); + effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), getPaletteCount()+customPalettes.size()-1), 0U); effectPalette = palettes_alpha_indexes[effectPaletteIndex]; stateChanged = true; if (applyToAll) { @@ -920,17 +922,17 @@ void RotaryEncoderUIUsermod::changeHue(bool increase){ display->updateRedrawTime(); #endif currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); + colorHStoRGB(currentHue1*256, currentSat1, colPri); stateChanged = true; if (applyToAll) { for (unsigned i=0; iupdateRedrawTime(); #endif currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); + colorHStoRGB(currentHue1*256, currentSat1, colPri); if (applyToAll) { for (unsigned i=0; i #include "wled.h" @@ -124,4 +122,7 @@ class WireguardUsermod : public Usermod { int endpoint_port = 0; bool is_enabled = false; unsigned long lastTime = 0; -}; \ No newline at end of file +}; + +static WireguardUsermod wireguard; +REGISTER_USERMOD(wireguard); \ No newline at end of file diff --git a/usermods/wizlights/library.json b/usermods/wizlights/library.json new file mode 100644 index 0000000000..0bfc097c78 --- /dev/null +++ b/usermods/wizlights/library.json @@ -0,0 +1,4 @@ +{ + "name": "wizlights", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/wizlights/wizlights.h b/usermods/wizlights/wizlights.cpp similarity index 98% rename from usermods/wizlights/wizlights.h rename to usermods/wizlights/wizlights.cpp index 08d204934c..3ac756b12a 100644 --- a/usermods/wizlights/wizlights.h +++ b/usermods/wizlights/wizlights.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include @@ -156,3 +154,7 @@ class WizLightsUsermod : public Usermod { uint16_t getId(){return USERMOD_ID_WIZLIGHTS;} }; + + +static WizLightsUsermod wizlights; +REGISTER_USERMOD(wizlights); \ No newline at end of file diff --git a/usermods/word-clock-matrix/library.json b/usermods/word-clock-matrix/library.json new file mode 100644 index 0000000000..7bc3919de0 --- /dev/null +++ b/usermods/word-clock-matrix/library.json @@ -0,0 +1,4 @@ +{ + "name": "word-clock-matrix", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/word-clock-matrix/usermod_word_clock_matrix.h b/usermods/word-clock-matrix/usermod_word_clock_matrix.h deleted file mode 100644 index 506c1275ef..0000000000 --- a/usermods/word-clock-matrix/usermod_word_clock_matrix.h +++ /dev/null @@ -1,338 +0,0 @@ -#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[F("mdns")] = "wled-word-clock"; - modName[F("name")] = "WLED WORD CLOCK"; - } - - uint16_t getId() - { - return USERMOD_ID_WORD_CLOCK_MATRIX; - } - - -}; diff --git a/usermods/word-clock-matrix/word-clock-matrix.cpp b/usermods/word-clock-matrix/word-clock-matrix.cpp index 67c5b1e472..24f69aadb7 100644 --- a/usermods/word-clock-matrix/word-clock-matrix.cpp +++ b/usermods/word-clock-matrix/word-clock-matrix.cpp @@ -1,305 +1,340 @@ #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! + * Things to do... + * Turn on ntp clock 24h format + * 64 LEDS */ -uint8_t minuteLast = 99; -int dayBrightness = 128; -int nightBrightness = 16; +class WordClockMatrix : public Usermod +{ +private: + unsigned long lastTime = 0; + uint8_t minuteLast = 99; + int dayBrightness = 128; + int nightBrightness = 16; -//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) +public: + void setup() + { + Serial.println("Hello from my usermod!"); -//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(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); + //saveMacro(1, "&FX=0&R=255&G=255&B=255", false); -//strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); + //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++) + //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 &text_seg = strip.getSegment(i); + text_seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); + strip.getSegment(i).setOption(0, true); + strip.setBrightness(64); + } + } + + void connected() { - 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); + Serial.println("Connected to WiFi!"); } -} -//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++) + { + //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 selectWordSegments(bool state) -{ - for (int i = 1; i < 10; i++) + void hourChime() { - //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.resetSegments(); + selectWordSegments(true); + colorUpdated(CALL_MODE_FX_CHANGED); + savePreset(13); + selectWordSegments(false); + //strip.getSegment(0).setOption(0, true); + strip.getSegment(0).setOption(2, true); + applyPreset(12); + colorUpdated(CALL_MODE_FX_CHANGED); } - 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 + void displayTime(byte hour, byte minute) + { + bool isToHour = false; //true if minute > 30 + strip.getSegment(0).setGeometry(0, 64); // background + strip.getSegment(1).setGeometry(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 + strip.getSegment(2).setGeometry(0, 0); + strip.getSegment(3).setGeometry(0, 0); //disable minutes + strip.getSegment(4).setGeometry(0, 0); //past + strip.getSegment(6).setGeometry(0, 0); //to + strip.getSegment(8).setGeometry(0, 0); //disable o'clock - if (hour < 24) //valid time, display - { - if (minute == 30) + if (hour < 24) //valid time, display { - strip.setSegment(2, 3, 6); //half - strip.setSegment(3, 0, 0); //minutes + if (minute == 30) + { + strip.getSegment(2).setGeometry(3, 6); //half + strip.getSegment(3).setGeometry(0, 0); //minutes + } + else if (minute == 15 || minute == 45) + { + strip.getSegment(3).setGeometry(0, 0); //minutes + } + else if (minute == 10) + { + //strip.getSegment(5).setGeometry(6, 8); //ten + } + else if (minute == 5) + { + //strip.getSegment(5).setGeometry(16, 18); //five + } + else if (minute == 0) + { + strip.getSegment(3).setGeometry(0, 0); //minutes + //hourChime(); + } + else + { + strip.getSegment(3).setGeometry(18, 22); //minutes + } + + //past or to? + if (minute == 0) + { //full hour + strip.getSegment(3).setGeometry(0, 0); //disable minutes + strip.getSegment(4).setGeometry(0, 0); //disable past + strip.getSegment(6).setGeometry(0, 0); //disable to + strip.getSegment(8).setGeometry(60, 64); //o'clock + } + else if (minute > 34) + { + //strip.getSegment(6).setGeometry(22, 24); //to + //minute = 60 - minute; + isToHour = true; + } + else + { + //strip.getSegment(4).setGeometry(24, 27); //past + //isToHour = false; + } } - else if (minute == 15 || minute == 45) + + //byte minuteRem = minute %10; + + if (minute <= 4) { - strip.setSegment(3, 0, 0); //minutes + strip.getSegment(3).setGeometry(0, 0); //nothing + strip.getSegment(5).setGeometry(0, 0); //nothing + strip.getSegment(6).setGeometry(0, 0); //nothing + strip.getSegment(8).setGeometry(60, 64); //o'clock } - else if (minute == 10) + else if (minute <= 9) { - //strip.setSegment(5, 6, 8); //ten + strip.getSegment(5).setGeometry(16, 18); // five past + strip.getSegment(4).setGeometry(24, 27); //past } - else if (minute == 5) + else if (minute <= 14) { - //strip.setSegment(5, 16, 18); //five + strip.getSegment(5).setGeometry(6, 8); // ten past + strip.getSegment(4).setGeometry(24, 27); //past } - else if (minute == 0) + else if (minute <= 19) { - strip.setSegment(3, 0, 0); //minutes - //hourChime(); + strip.getSegment(5).setGeometry(8, 12); // quarter past + strip.getSegment(3).setGeometry(0, 0); //minutes + strip.getSegment(4).setGeometry(24, 27); //past } - else + else if (minute <= 24) { - strip.setSegment(3, 18, 22); //minutes + strip.getSegment(5).setGeometry(12, 16); // twenty past + strip.getSegment(4).setGeometry(24, 27); //past } - - //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 <= 29) + { + strip.getSegment(5).setGeometry(12, 18); // twenty-five past + strip.getSegment(4).setGeometry(24, 27); //past } - else if (minute > 34) + else if (minute <= 34) { - //strip.setSegment(6, 22, 24); //to - //minute = 60 - minute; - isToHour = true; + strip.getSegment(5).setGeometry(3, 6); // half past + strip.getSegment(3).setGeometry(0, 0); //minutes + strip.getSegment(4).setGeometry(24, 27); //past } - else + else if (minute <= 39) { - //strip.setSegment(4, 24, 27); //past - //isToHour = false; + strip.getSegment(5).setGeometry(12, 18); // twenty-five to + strip.getSegment(6).setGeometry(22, 24); //to + } + else if (minute <= 44) + { + strip.getSegment(5).setGeometry(12, 16); // twenty to + strip.getSegment(6).setGeometry(22, 24); //to + } + else if (minute <= 49) + { + strip.getSegment(5).setGeometry(8, 12); // quarter to + strip.getSegment(3).setGeometry(0, 0); //minutes + strip.getSegment(6).setGeometry(22, 24); //to + } + else if (minute <= 54) + { + strip.getSegment(5).setGeometry(6, 8); // ten to + strip.getSegment(6).setGeometry(22, 24); //to + } + else if (minute <= 59) + { + strip.getSegment(5).setGeometry(16, 18); // five to + strip.getSegment(6).setGeometry(22, 24); //to } - } - else - { //temperature display - } - //byte minuteRem = minute %10; + //hours + if (hour > 23) + return; + if (isToHour) + hour++; + if (hour > 12) + hour -= 12; + if (hour == 0) + hour = 12; - 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 - } + switch (hour) + { + case 1: + strip.getSegment(7).setGeometry(27, 29); + break; //one + case 2: + strip.getSegment(7).setGeometry(35, 37); + break; //two + case 3: + strip.getSegment(7).setGeometry(29, 32); + break; //three + case 4: + strip.getSegment(7).setGeometry(32, 35); + break; //four + case 5: + strip.getSegment(7).setGeometry(37, 40); + break; //five + case 6: + strip.getSegment(7).setGeometry(43, 45); + break; //six + case 7: + strip.getSegment(7).setGeometry(40, 43); + break; //seven + case 8: + strip.getSegment(7).setGeometry(45, 48); + break; //eight + case 9: + strip.getSegment(7).setGeometry(48, 50); + break; //nine + case 10: + strip.getSegment(7).setGeometry(54, 56); + break; //ten + case 11: + strip.getSegment(7).setGeometry(50, 54); + break; //eleven + case 12: + strip.getSegment(7).setGeometry(56, 60); + break; //twelve + } - //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); + applyPreset(1); } -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 + 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 + //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); + if ((weekday(localTime) == 6) | (weekday(localTime) == 7)) + { + if ((hour(localTime) > 0) | (hour(localTime) < 8)) + { + strip.setBrightness(nightBrightness); + } + else + { + strip.setBrightness(dayBrightness); + } } - 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) + //loop. You can use "if (WLED_CONNECTED)" to check for successful connection + void loop() { - updateLocalTime(); - //timeOfDay(); - minuteLast = minute(localTime); - displayTime(hour(localTime), minute(localTime)); - if (minute(localTime) == 0){ - hourChime(); - } - if (minute(localTime) == 1){ - //turn off background segment; + + 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(255); + //applyPreset(13); + } } } -} + + void addToConfig(JsonObject& root) + { + JsonObject modName = root.createNestedObject("id"); + modName[F("mdns")] = "wled-word-clock"; + modName[F("name")] = "WLED WORD CLOCK"; + } + + uint16_t getId() + { + return 500; + } + + +}; + + +static WordClockMatrix word_clock_matrix; +REGISTER_USERMOD(word_clock_matrix); \ No newline at end of file diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a0d0808183..bc5d895cd0 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -14,21 +14,80 @@ #include "FX.h" #include "fcn_declare.h" -#define IBN 5100 +#define FX_FALLBACK_STATIC { mode_static(); return; } + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) + #include "FXparticleSystem.h" // include particle system code only if at least one system is enabled + #ifdef WLED_DISABLE_PARTICLESYSTEM2D + #define WLED_PS_DONT_REPLACE_2D_FX + #endif + #ifdef WLED_DISABLE_PARTICLESYSTEM1D + #define WLED_PS_DONT_REPLACE_1D_FX + #endif + #ifdef ESP8266 + #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D) + #error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them. + #endif + #endif +#else + #define WLED_PS_DONT_REPLACE_1D_FX + #define WLED_PS_DONT_REPLACE_2D_FX +#endif +#ifdef WLED_PS_DONT_REPLACE_FX + #define WLED_PS_DONT_REPLACE_1D_FX + #define WLED_PS_DONT_REPLACE_2D_FX +#endif + + ////////////// + // DEV INFO // + ////////////// +/* + information for FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata + + Audio Reactive: use the following code to pass 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 = getAudioData(); + 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]; +*/ +#define IBN 5100 // 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)) +// 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 + // effect utility functions -uint8_t sin_gap(uint16_t in) { +static uint8_t sin_gap(uint16_t in) { if (in & 0x100) return 0; return sin8_t(in + 192); // correct phase shift of sine so that it starts and stops at 0 } -uint16_t triwave16(uint16_t in) { +static uint16_t triwave16(uint16_t in) { if (in < 0x8000) return in *2; return 0xFFFF - (in - 0x8000)*2; } @@ -40,7 +99,7 @@ uint16_t triwave16(uint16_t in) { * @param attdec attack & decay, max. pulsewidth / 2 * @returns signed waveform value */ -int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { +static int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { int8_t a = 127; if (x > 127) { a = -127; @@ -68,24 +127,73 @@ static um_data_t* getAudioData() { return um_data; } + // effect functions /* * No blinking. Just plain old static light. */ -uint16_t mode_static(void) { +void mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); - return strip.isOffRefreshRequired() ? FRAMETIME : 350; } static const char _data_FX_MODE_STATIC[] PROGMEM = "Solid"; +/* + * Copy a segment and perform (optional) color adjustments + */ +void mode_copy_segment(void) { + uint32_t sourceid = SEGMENT.custom3; + if (sourceid >= strip.getSegmentsNum() || sourceid == strip.getCurrSegmentId()) { // invalid source + SEGMENT.fadeToBlackBy(5); // fade out + } + Segment& sourcesegment = strip.getSegment(sourceid); + + if (sourcesegment.isActive()) { + uint32_t sourcecolor; + uint32_t destcolor; + if(sourcesegment.is2D()) { // 2D source, note: 2D to 1D just copies the first row (or first column if 'Switch axis' is checked in FX) + for (unsigned y = 0; y < SEGMENT.vHeight(); y++) { + for (unsigned x = 0; x < SEGMENT.vWidth(); x++) { + unsigned sx = x; // source coordinates + unsigned sy = y; + if(SEGMENT.check1) std::swap(sx, sy); // flip axis + if(SEGMENT.check2) { + sourcecolor = strip.getPixelColorXY(sx + sourcesegment.start, sy + sourcesegment.startY); // read from global buffer (reads the last rendered frame) + } + else { + sourcesegment.setDrawDimensions(); // set to source segment dimensions + sourcecolor = sourcesegment.getPixelColorXY(sx, sy); // read from segment buffer + } + destcolor = adjust_color(sourcecolor, SEGMENT.intensity, SEGMENT.custom1, SEGMENT.custom2); + SEGMENT.setDrawDimensions(); // reset to current segment dimensions + SEGMENT.setPixelColorXY(x, y, destcolor); + } + } + } else { // 1D source, source can be expanded into 2D + for (unsigned i = 0; i < SEGMENT.vLength(); i++) { + if(SEGMENT.check2) { + sourcecolor = strip.getPixelColorNoMap(i + sourcesegment.start); // read from global buffer (reads the last rendered frame) + } + else { + sourcesegment.setDrawDimensions(); // set to source segment dimensions + sourcecolor = sourcesegment.getPixelColor(i); + } + destcolor = adjust_color(sourcecolor, SEGMENT.intensity, SEGMENT.custom1, SEGMENT.custom2); + SEGMENT.setDrawDimensions(); // reset to current segment dimensions + SEGMENT.setPixelColor(i, destcolor); + } + } + } +} +static const char _data_FX_MODE_COPY[] PROGMEM = "Copy Segment@,Color shift,Lighten,Brighten,ID,Axis(2D),FullStack(last frame);;;12;ix=0,c1=0,c2=0,c3=0"; + /* * Blink/strobe function * Alternate between color1 and color2 * if(strobe == true) then create a strobe effect */ -uint16_t blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { +void blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { uint32_t cycleTime = (255 - SEGMENT.speed)*20; uint32_t onTime = FRAMETIME; if (!strobe) onTime += ((cycleTime * SEGMENT.intensity) >> 8); @@ -104,20 +212,18 @@ uint16_t blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { uint32_t color = on ? color1 : color2; if (color == color1 && do_palette) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } else SEGMENT.fill(color); - - return FRAMETIME; } /* * Normal blinking. Intensity sets duty cycle. */ -uint16_t mode_blink(void) { - return blink(SEGCOLOR(0), SEGCOLOR(1), false, true); +void mode_blink(void) { + blink(SEGCOLOR(0), SEGCOLOR(1), false, true); } static const char _data_FX_MODE_BLINK[] PROGMEM = "Blink@!,Duty cycle;!,!;!;01"; @@ -125,8 +231,8 @@ static const char _data_FX_MODE_BLINK[] PROGMEM = "Blink@!,Duty cycle;!,!;!;01"; /* * Classic Blink effect. Cycling through the rainbow. */ -uint16_t mode_blink_rainbow(void) { - return blink(SEGMENT.color_wheel(SEGENV.call & 0xFF), SEGCOLOR(1), false, false); +void mode_blink_rainbow(void) { + 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"; @@ -134,7 +240,7 @@ static const char _data_FX_MODE_BLINK_RAINBOW[] PROGMEM = "Blink Rainbow@Frequen /* * Classic Strobe effect. */ -uint16_t mode_strobe(void) { +void mode_strobe(void) { return blink(SEGCOLOR(0), SEGCOLOR(1), true, true); } static const char _data_FX_MODE_STROBE[] PROGMEM = "Strobe@!;!,!;!;01"; @@ -143,7 +249,7 @@ static const char _data_FX_MODE_STROBE[] PROGMEM = "Strobe@!;!,!;!;01"; /* * Classic Strobe effect. Cycling through the rainbow. */ -uint16_t mode_strobe_rainbow(void) { +void 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"; @@ -154,8 +260,8 @@ static const char _data_FX_MODE_STROBE_RAINBOW[] PROGMEM = "Strobe Rainbow@!;,!; * 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 color_wipe(bool rev, bool useRandomColors) { - if (SEGLEN == 1) return mode_static(); +void color_wipe(bool rev, bool useRandomColors) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; unsigned prog = (perc * 65535) / cycleTime; @@ -169,7 +275,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { if (useRandomColors) { if (SEGENV.call == 0) { - SEGENV.aux0 = random8(); + SEGENV.aux0 = hw_random8(); SEGENV.step = 3; } if (SEGENV.step == 1) { //if flag set, change to new random color @@ -183,8 +289,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { } unsigned ledIndex = (prog * SEGLEN) >> 15; - unsigned rem = 0; - rem = (prog * SEGLEN) * 2; //mod 0xFFFF + uint16_t rem = (prog * SEGLEN) * 2; //mod 0xFFFF by truncating rem /= (SEGMENT.intensity +1); if (rem > 255) rem = 255; @@ -200,18 +305,17 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { } else { SEGMENT.setPixelColor(index, back? col0 : col1); - if (i == ledIndex) SEGMENT.setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, rem)); + if (i == ledIndex) SEGMENT.setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, uint8_t(rem))); } } - return FRAMETIME; } /* * Lights all LEDs one after another. */ -uint16_t mode_color_wipe(void) { - return color_wipe(false, false); +void mode_color_wipe(void) { + color_wipe(false, false); } static const char _data_FX_MODE_COLOR_WIPE[] PROGMEM = "Wipe@!,!;!,!;!"; @@ -219,8 +323,8 @@ static const char _data_FX_MODE_COLOR_WIPE[] PROGMEM = "Wipe@!,!;!,!;!"; /* * Lights all LEDs one after another. Turns off opposite */ -uint16_t mode_color_sweep(void) { - return color_wipe(true, false); +void mode_color_sweep(void) { + color_wipe(true, false); } static const char _data_FX_MODE_COLOR_SWEEP[] PROGMEM = "Sweep@!,!;!,!;!"; @@ -229,8 +333,8 @@ 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 mode_color_wipe_random(void) { - return color_wipe(false, true); +void mode_color_wipe_random(void) { + color_wipe(false, true); } static const char _data_FX_MODE_COLOR_WIPE_RANDOM[] PROGMEM = "Wipe Random@!;;!"; @@ -238,8 +342,8 @@ static const char _data_FX_MODE_COLOR_WIPE_RANDOM[] PROGMEM = "Wipe Random@!;;!" /* * Random color introduced alternating from start and end of strip. */ -uint16_t mode_color_sweep_random(void) { - return color_wipe(true, true); +void mode_color_sweep_random(void) { + color_wipe(true, true); } static const char _data_FX_MODE_COLOR_SWEEP_RANDOM[] PROGMEM = "Sweep Random@!;;!"; @@ -248,7 +352,7 @@ static const char _data_FX_MODE_COLOR_SWEEP_RANDOM[] PROGMEM = "Sweep Random@!;; * Lights all LEDs up in one random color. Then switches them * to the next random color. */ -uint16_t mode_random_color(void) { +void mode_random_color(void) { uint32_t cycleTime = 200 + (255 - SEGMENT.speed)*50; uint32_t it = strip.now / cycleTime; uint32_t rem = strip.now % cycleTime; @@ -261,7 +365,7 @@ uint16_t mode_random_color(void) { } if (SEGENV.call == 0) { - SEGENV.aux0 = random8(); + SEGENV.aux0 = hw_random8(); SEGENV.step = 2; } if (it != SEGENV.step) //new color @@ -271,8 +375,7 @@ uint16_t mode_random_color(void) { SEGENV.step = it; } - SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), fade)); - return FRAMETIME; + SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), uint8_t(fade))); } static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade time;;!;01"; @@ -281,34 +384,33 @@ static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade t * Lights every LED in a random color. Changes all LED at the same time * to new random colors. */ -uint16_t mode_dynamic(void) { - if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed +void mode_dynamic(void) { + if (!SEGENV.allocateData(SEGLEN)) FX_FALLBACK_STATIC; //allocation failed if(SEGENV.call == 0) { //SEGMENT.fill(BLACK); - for (int i = 0; i < SEGLEN; i++) SEGENV.data[i] = random8(); + for (unsigned i = 0; i < SEGLEN; i++) SEGENV.data[i] = hw_random8(); } uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*15; uint32_t it = strip.now / cycleTime; if (it != SEGENV.step && SEGMENT.speed != 0) //new color { - for (int i = 0; i < SEGLEN; i++) { - if (random8() <= SEGMENT.intensity) SEGENV.data[i] = random8(); // random color index + for (unsigned i = 0; i < SEGLEN; i++) { + if (hw_random8() <= SEGMENT.intensity) SEGENV.data[i] = hw_random8(); // random color index } SEGENV.step = it; } if (SEGMENT.check1) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.blendPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i]), 16); } } else { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned 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;;!"; @@ -316,12 +418,11 @@ static const char _data_FX_MODE_DYNAMIC[] PROGMEM = "Dynamic@!,!,,,,Smooth;;!"; /* * effect "Dynamic" with smooth color-fading */ -uint16_t mode_dynamic_smooth(void) { +void 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@!,!;;!"; @@ -329,7 +430,7 @@ static const char _data_FX_MODE_DYNAMIC_SMOOTH[] PROGMEM = "Dynamic Smooth@!,!;; /* * Does the "standby-breathing" of well known i-Devices. */ -uint16_t mode_breath(void) { +void mode_breath(void) { unsigned var = 0; unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)) & 0xFFFFU; counter = (counter >> 2) + (counter >> 4); //0-16384 + 0-2048 @@ -338,12 +439,11 @@ uint16_t mode_breath(void) { var = sin16_t(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 } - unsigned lum = 30 + var; - for (int i = 0; i < SEGLEN; i++) { + uint8_t lum = 30 + var; + for (unsigned 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"; @@ -351,15 +451,13 @@ static const char _data_FX_MODE_BREATH[] PROGMEM = "Breathe@!;!,!;!;01"; /* * Fades the LEDs between two colors */ -uint16_t mode_fade(void) { +void mode_fade(void) { unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)); - unsigned lum = triwave16(counter) >> 8; + uint8_t lum = triwave16(counter) >> 8; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned 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"; @@ -367,8 +465,8 @@ static const char _data_FX_MODE_FADE[] PROGMEM = "Fade@!;!,!;!;01"; /* * Scan mode parent function */ -uint16_t scan(bool dual) { - if (SEGLEN == 1) return mode_static(); +void scan(bool dual) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; int prog = (perc * 65535) / cycleTime; @@ -390,16 +488,14 @@ uint16_t scan(bool dual) { 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; } /* * Runs a single pixel back and forth. */ -uint16_t mode_scan(void) { - return scan(false); +void mode_scan(void) { + scan(false); } static const char _data_FX_MODE_SCAN[] PROGMEM = "Scan@!,# of dots,,,,,Overlay;!,!,!;!"; @@ -407,8 +503,8 @@ static const char _data_FX_MODE_SCAN[] PROGMEM = "Scan@!,# of dots,,,,,Overlay;! /* * Runs two pixel back and forth in opposite directions. */ -uint16_t mode_dual_scan(void) { - return scan(true); +void mode_dual_scan(void) { + scan(true); } static const char _data_FX_MODE_DUAL_SCAN[] PROGMEM = "Scan Dual@!,# of dots,,,,,Overlay;!,!,!;!"; @@ -416,17 +512,15 @@ static const char _data_FX_MODE_DUAL_SCAN[] PROGMEM = "Scan Dual@!,# of dots,,,, /* * Cycles all LEDs at once through a rainbow. */ -uint16_t mode_rainbow(void) { +void mode_rainbow(void) { unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; if (SEGMENT.intensity < 128){ - SEGMENT.fill(color_blend(SEGMENT.color_wheel(counter),WHITE,128-SEGMENT.intensity)); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(counter),WHITE,uint8_t(128-SEGMENT.intensity))); } else { SEGMENT.fill(SEGMENT.color_wheel(counter)); } - - return FRAMETIME; } static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!;01"; @@ -434,17 +528,15 @@ static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!;0 /* * Cycles a rainbow over the entire string of LEDs. */ -uint16_t mode_rainbow_cycle(void) { +void mode_rainbow_cycle(void) { unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned 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; SEGMENT.setPixelColor(i, SEGMENT.color_wheel(index)); } - - return FRAMETIME; } static const char _data_FX_MODE_RAINBOW_CYCLE[] PROGMEM = "Rainbow@!,Size;;!"; @@ -452,13 +544,13 @@ static const char _data_FX_MODE_RAINBOW_CYCLE[] PROGMEM = "Rainbow@!,Size;;!"; /* * Alternating pixels running function. */ -static uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) { +static void running(uint32_t color1, uint32_t color2, bool theatre = false) { int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window uint32_t cycleTime = 50 + (255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; bool usePalette = color1 == SEGCOLOR(0); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint32_t col = color2; if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); if (theatre) { @@ -474,7 +566,6 @@ static uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1)); SEGENV.step = it; } - return FRAMETIME; } @@ -482,8 +573,8 @@ static uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) * Theatre-style crawling lights. * Inspired by the Adafruit examples. */ -uint16_t mode_theater_chase(void) { - return running(SEGCOLOR(0), SEGCOLOR(1), true); +void mode_theater_chase(void) { + running(SEGCOLOR(0), SEGCOLOR(1), true); } static const char _data_FX_MODE_THEATER_CHASE[] PROGMEM = "Theater@!,Gap size;!,!;!"; @@ -492,8 +583,8 @@ 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 mode_theater_chase_rainbow(void) { - return running(SEGMENT.color_wheel(SEGENV.step), SEGCOLOR(1), true); +void mode_theater_chase_rainbow(void) { + running(SEGMENT.color_wheel(SEGENV.step), SEGCOLOR(1), true); } static const char _data_FX_MODE_THEATER_CHASE_RAINBOW[] PROGMEM = "Theater Rainbow@!,Gap size;,!;!"; @@ -501,11 +592,11 @@ static const char _data_FX_MODE_THEATER_CHASE_RAINBOW[] PROGMEM = "Theater Rainb /* * Running lights effect with smooth sine transition base. */ -static uint16_t running_base(bool saw, bool dual=false) { +static void running_base(bool saw, bool dual=false) { unsigned x_scale = SEGMENT.intensity >> 2; uint32_t counter = (strip.now * SEGMENT.speed) >> 9; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned a = i*x_scale - counter; if (saw) { a &= 0xFF; @@ -523,12 +614,10 @@ static uint16_t running_base(bool saw, bool dual=false) { unsigned 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); + ca = color_blend(ca, cb, uint8_t(127)); } SEGMENT.setPixelColor(i, ca); } - - return FRAMETIME; } @@ -536,8 +625,8 @@ static uint16_t running_base(bool saw, bool dual=false) { * Running lights in opposite directions. * Idea: Make the gap width controllable with a third slider in the future */ -uint16_t mode_running_dual(void) { - return running_base(false, true); +void mode_running_dual(void) { + running_base(false, true); } static const char _data_FX_MODE_RUNNING_DUAL[] PROGMEM = "Running Dual@!,Wave width;L,!,R;!"; @@ -545,8 +634,8 @@ static const char _data_FX_MODE_RUNNING_DUAL[] PROGMEM = "Running Dual@!,Wave wi /* * Running lights effect with smooth sine transition. */ -uint16_t mode_running_lights(void) { - return running_base(false); +void mode_running_lights(void) { + running_base(false); } static const char _data_FX_MODE_RUNNING_LIGHTS[] PROGMEM = "Running@!,Wave width;!,!;!"; @@ -554,8 +643,8 @@ static const char _data_FX_MODE_RUNNING_LIGHTS[] PROGMEM = "Running@!,Wave width /* * Running lights effect with sawtooth transition. */ -uint16_t mode_saw(void) { - return running_base(true); +void mode_saw(void) { + running_base(true); } static const char _data_FX_MODE_SAW[] PROGMEM = "Saw@!,Width;!,!;!"; @@ -564,7 +653,7 @@ 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 mode_twinkle(void) { +void mode_twinkle(void) { SEGMENT.fade_out(224); uint32_t cycleTime = 20 + (255 - SEGMENT.speed)*5; @@ -575,13 +664,13 @@ uint16_t mode_twinkle(void) { if (SEGENV.aux0 >= maxOn) { SEGENV.aux0 = 0; - SEGENV.aux1 = random16(); //new seed for our PRNG + SEGENV.aux1 = hw_random(); //new seed for our PRNG } SEGENV.aux0++; SEGENV.step = it; } - unsigned PRNG16 = SEGENV.aux1; + uint16_t PRNG16 = SEGENV.aux1; for (unsigned i = 0; i < SEGENV.aux0; i++) { @@ -590,8 +679,6 @@ uint16_t mode_twinkle(void) { unsigned j = p >> 16; 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 @@ -599,111 +686,112 @@ static const char _data_FX_MODE_TWINKLE[] PROGMEM = "Twinkle@!,!;!,!;!;;m12=0"; /* * Dissolve function */ -uint16_t dissolve(uint32_t color) { - unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed +void dissolve(uint32_t color) { + unsigned dataSize = sizeof(uint32_t) * SEGLEN; + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed + uint32_t* pixels = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) { - memset(SEGMENT.data, 0xFF, dataSize); // start by fading pixels up + for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = SEGCOLOR(1); SEGENV.aux0 = 1; } - for (int j = 0; j <= SEGLEN / 15; j++) { - if (random8() <= SEGMENT.intensity) { + for (unsigned j = 0; j <= SEGLEN / 15; j++) { + if (hw_random8() <= SEGMENT.intensity) { for (size_t times = 0; times < 10; times++) { //attempt to spawn a new pixel 10 times - unsigned i = random16(SEGLEN); - unsigned index = i >> 3; - unsigned bitNum = i & 0x07; - bool fadeUp = bitRead(SEGENV.data[index], bitNum); + unsigned i = hw_random16(SEGLEN); if (SEGENV.aux0) { //dissolve to primary/palette - if (fadeUp) { - if (color == SEGCOLOR(0)) { - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); - } else { - SEGMENT.setPixelColor(i, color); - } - bitWrite(SEGENV.data[index], bitNum, false); - break; //only spawn 1 new pixel per frame per 50 LEDs + if (pixels[i] == SEGCOLOR(1)) { + pixels[i] = color == SEGCOLOR(0) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : color; + break; //only spawn 1 new pixel per frame } } else { //dissolve to secondary - if (!fadeUp) { - SEGMENT.setPixelColor(i, SEGCOLOR(1)); break; - bitWrite(SEGENV.data[index], bitNum, true); + if (pixels[i] != SEGCOLOR(1)) { + pixels[i] = SEGCOLOR(1); + break; } } } } } + unsigned incompletePixels = 0; + for (unsigned i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, pixels[i]); // fix for #4401 + if (SEGMENT.check2) { + if (SEGENV.aux0) { + if (pixels[i] == SEGCOLOR(1)) incompletePixels++; + } else { + if (pixels[i] != SEGCOLOR(1)) incompletePixels++; + } + } + } if (SEGENV.step > (255 - SEGMENT.speed) + 15U) { SEGENV.aux0 = !SEGENV.aux0; SEGENV.step = 0; - memset(SEGMENT.data, (SEGENV.aux0 ? 0xFF : 0), dataSize); // switch fading } else { - SEGENV.step++; + if (SEGMENT.check2) { + if (incompletePixels == 0) + SEGENV.step++; // only advance step once all pixels have changed + } else + SEGENV.step++; } - - return FRAMETIME; } /* * Blink several LEDs on and then off */ -uint16_t mode_dissolve(void) { - return dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(random8()) : SEGCOLOR(0)); +void mode_dissolve(void) { + dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(hw_random8()) : SEGCOLOR(0)); } -static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random;!,!;!"; +static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random,Complete;!,!;!"; /* * Blink several LEDs on and then off in random colors */ -uint16_t mode_dissolve_random(void) { - return dissolve(SEGMENT.color_wheel(random8())); +void mode_dissolve_random(void) { + dissolve(SEGMENT.color_wheel(hw_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 mode_sparkle(void) { - if (!SEGMENT.check2) for(int i = 0; i < SEGLEN; i++) { +void mode_sparkle(void) { + if (!SEGMENT.check2) for (unsigned 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 = strip.now / cycleTime; if (it != SEGENV.step) { - SEGENV.aux0 = random16(SEGLEN); // aux0 stores the random led index + SEGENV.aux0 = hw_random16(SEGLEN); // aux0 stores the random led index SEGENV.step = it; } SEGMENT.setPixelColor(SEGENV.aux0, SEGCOLOR(0)); - return FRAMETIME; } static const char _data_FX_MODE_SPARKLE[] PROGMEM = "Sparkle@!,,,,,,Overlay;!,!;!;;m12=0"; - /* * Lights all LEDs in the color. Flashes single col 1 pixels randomly. (List name: Sparkle Dark) * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ -uint16_t mode_flash_sparkle(void) { - if (!SEGMENT.check2) for (int i = 0; i < SEGLEN; i++) { +void mode_flash_sparkle(void) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } if (strip.now - SEGENV.aux0 > SEGENV.step) { - if(random8((255-SEGMENT.intensity) >> 4) == 0) { - SEGMENT.setPixelColor(random16(SEGLEN), SEGCOLOR(1)); //flash + if(hw_random8((255-SEGMENT.intensity) >> 4) == 0) { + SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1)); //flash } SEGENV.step = strip.now; SEGENV.aux0 = 255-SEGMENT.speed; } - return FRAMETIME; } static const char _data_FX_MODE_FLASH_SPARKLE[] PROGMEM = "Sparkle Dark@!,!,,,,,Overlay;Bg,Fx;!;;m12=0"; @@ -712,21 +800,21 @@ static const char _data_FX_MODE_FLASH_SPARKLE[] PROGMEM = "Sparkle Dark@!,!,,,,, * Like flash sparkle. With more flash. * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ -uint16_t mode_hyper_sparkle(void) { - if (!SEGMENT.check2) for (int i = 0; i < SEGLEN; i++) { +void mode_hyper_sparkle(void) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } 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)); + if (hw_random8((255-SEGMENT.intensity) >> 4) == 0) { + int len = max(1, (int)SEGLEN/3); + for (int i = 0; i < len; i++) { + SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1)); } } SEGENV.step = strip.now; SEGENV.aux0 = 255-SEGMENT.speed; } - return FRAMETIME; } static const char _data_FX_MODE_HYPER_SPARKLE[] PROGMEM = "Sparkle+@!,!,,,,,Overlay;Bg,Fx;!;;m12=0"; @@ -734,8 +822,8 @@ static const char _data_FX_MODE_HYPER_SPARKLE[] PROGMEM = "Sparkle+@!,!,,,,,Over /* * Strobe effect with different strobe count and pause, controlled by speed. */ -uint16_t mode_multi_strobe(void) { - for (int i = 0; i < SEGLEN; i++) { +void mode_multi_strobe(void) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } @@ -755,70 +843,55 @@ uint16_t mode_multi_strobe(void) { 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 + * Android loading circle, refactored by @dedehai */ -uint16_t mode_android(void) { - - for (int i = 0; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); - } - - if (SEGENV.aux1 > (SEGMENT.intensity*SEGLEN)/255) - { - SEGENV.aux0 = 1; - } else - { - if (SEGENV.aux1 < 2) SEGENV.aux0 = 0; - } - - unsigned a = SEGENV.step & 0xFFFFU; - - if (SEGENV.aux0 == 0) - { - if (SEGENV.call %3 == 1) {a++;} - else {SEGENV.aux1++;} - } else - { - a++; - if (SEGENV.call %3 != 1) SEGENV.aux1--; - } - - if (a >= SEGLEN) a = 0; - - if (a + SEGENV.aux1 < SEGLEN) - { - for (unsigned i = a; i < a+SEGENV.aux1; i++) { - SEGMENT.setPixelColor(i, SEGCOLOR(0)); - } - } else - { - for (unsigned i = a; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, SEGCOLOR(0)); - } - for (unsigned i = 0; i < SEGENV.aux1 - (SEGLEN -a); i++) { +void mode_android(void) { + if (!SEGENV.allocateData(sizeof(uint32_t))) FX_FALLBACK_STATIC; + uint32_t* counter = reinterpret_cast(SEGENV.data); + unsigned size = SEGENV.aux1 >> 1; // upper 15 bit + unsigned shrinking = SEGENV.aux1 & 0x01; // lowest bit + if(strip.now >= SEGENV.step) { + SEGENV.step = strip.now + 3 + ((8 * (uint32_t)(255 - SEGMENT.speed)) / SEGLEN); + if (size > (SEGMENT.intensity * SEGLEN) / 255) + shrinking = 1; + else if (size < 2) + shrinking = 0; + if (!shrinking) { // growing + if ((*counter % 3) == 1) + SEGENV.aux0++; // advance start position + else + size++; + } else { // shrinking + SEGENV.aux0++; + if ((*counter % 3) != 1) + size--; + } + SEGENV.aux1 = size << 1 | shrinking; // save back + (*counter)++; + if (SEGENV.aux0 >= SEGLEN) SEGENV.aux0 = 0; + } + uint32_t start = SEGENV.aux0; + uint32_t end = (SEGENV.aux0 + size) % SEGLEN; + for (unsigned i = 0; i < SEGLEN; i++) { + if ((start < end && i >= start && i < end) || (start >= end && (i >= start || i < end))) SEGMENT.setPixelColor(i, SEGCOLOR(0)); - } + else + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } - 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 */ -static uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palette) { +static void 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; @@ -880,16 +953,14 @@ static uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do for (unsigned i = 0; i < c; i++) //fill from start until c SEGMENT.setPixelColor(i, color3); } - - return FRAMETIME; } /* * Bicolor chase, more primary color. */ -uint16_t mode_chase_color(void) { - return chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), true); +void mode_chase_color(void) { + chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), true); } static const char _data_FX_MODE_CHASE_COLOR[] PROGMEM = "Chase@!,Width;!,!,!;!"; @@ -897,8 +968,8 @@ static const char _data_FX_MODE_CHASE_COLOR[] PROGMEM = "Chase@!,Width;!,!,!;!"; /* * Primary running followed by random color. */ -uint16_t mode_chase_random(void) { - return chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), false); +void mode_chase_random(void) { + chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), false); } static const char _data_FX_MODE_CHASE_RANDOM[] PROGMEM = "Chase Random@!,Width;!,,!;!"; @@ -906,13 +977,13 @@ static const char _data_FX_MODE_CHASE_RANDOM[] PROGMEM = "Chase Random@!,Width;! /* * Primary, secondary running on rainbow. */ -uint16_t mode_chase_rainbow(void) { +void mode_chase_rainbow(void) { unsigned color_sep = 256 / SEGLEN; if (color_sep == 0) color_sep = 1; // correction for segments longer than 256 LEDs unsigned color_index = SEGENV.call & 0xFF; uint32_t color = SEGMENT.color_wheel(((SEGENV.step * color_sep) + color_index) & 0xFF); - return chase(color, SEGCOLOR(0), SEGCOLOR(1), false); + chase(color, SEGCOLOR(0), SEGCOLOR(1), false); } static const char _data_FX_MODE_CHASE_RAINBOW[] PROGMEM = "Chase Rainbow@!,Width;!,!;!"; @@ -920,13 +991,13 @@ static const char _data_FX_MODE_CHASE_RAINBOW[] PROGMEM = "Chase Rainbow@!,Width /* * Primary running on rainbow. */ -uint16_t mode_chase_rainbow_white(void) { +void mode_chase_rainbow_white(void) { uint16_t n = SEGENV.step; uint16_t m = (SEGENV.step + 1) % SEGLEN; 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); + chase(SEGCOLOR(0), color2, color3, false); } static const char _data_FX_MODE_CHASE_RAINBOW_WHITE[] PROGMEM = "Rainbow Runner@!,Size;Bg;!"; @@ -934,7 +1005,7 @@ static const char _data_FX_MODE_CHASE_RAINBOW_WHITE[] PROGMEM = "Rainbow Runner@ /* * Red - Amber - Green - Blue lights running */ -uint16_t mode_colorful(void) { +void mode_colorful(void) { unsigned numColors = 4; //3, 4, or 5 uint32_t cols[9]{0x00FF0000,0x00EEBB00,0x0000EE00,0x000077CC}; if (SEGMENT.intensity > 160 || SEGMENT.palette) { //palette or color @@ -970,8 +1041,6 @@ uint16_t mode_colorful(void) { { for (unsigned 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;!"; @@ -979,12 +1048,12 @@ static const char _data_FX_MODE_COLORFUL[] PROGMEM = "Colorful@!,Saturation;1,2, /* * Emulates a traffic light. */ -uint16_t mode_traffic_light(void) { - if (SEGLEN == 1) return mode_static(); - for (int i=0; i < SEGLEN; i++) +void mode_traffic_light(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + for (unsigned 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) + for (unsigned i = 0; i < SEGLEN-2 ; i+=3) { switch (SEGENV.aux0) { @@ -1002,8 +1071,6 @@ uint16_t mode_traffic_light(void) { if (SEGENV.aux0 > 3) SEGENV.aux0 = 0; SEGENV.step = strip.now; } - - return FRAMETIME; } static const char _data_FX_MODE_TRAFFIC_LIGHT[] PROGMEM = "Traffic Light@!,US style;,!;!"; @@ -1012,29 +1079,37 @@ static const char _data_FX_MODE_TRAFFIC_LIGHT[] PROGMEM = "Traffic Light@!,US st * Sec flashes running on prim. */ #define FLASH_COUNT 4 -uint16_t mode_chase_flash(void) { - if (SEGLEN == 1) return mode_static(); - unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); +void mode_chase_flash(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + unsigned now = strip.now; // save time for delay calculation + bool advance = true; + unsigned flash_step = SEGENV.aux1 % ((FLASH_COUNT * 2) + 1); + if (now < SEGENV.step) + advance = false; // limit update rate but render every frame for smooth transitions + else + SEGENV.aux1++; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } + unsigned index = SEGENV.aux0; + unsigned n = index; + unsigned m = (index + 1) % SEGLEN; unsigned delay = 10 + ((30 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); if(flash_step < (FLASH_COUNT * 2)) { if(flash_step % 2 == 0) { - unsigned n = SEGENV.step; - unsigned m = (SEGENV.step + 1) % SEGLEN; SEGMENT.setPixelColor( n, SEGCOLOR(1)); SEGMENT.setPixelColor( m, SEGCOLOR(1)); delay = 20; } else { delay = 30; } - } else { - SEGENV.step = (SEGENV.step + 1) % SEGLEN; + } else if (advance) { + SEGENV.aux0 = m; // advance to next position } - return delay; + if (advance) + SEGENV.step = now + delay; // set next update time } static const char _data_FX_MODE_CHASE_FLASH[] PROGMEM = "Chase Flash@!;Bg,Fx;!"; @@ -1042,8 +1117,14 @@ static const char _data_FX_MODE_CHASE_FLASH[] PROGMEM = "Chase Flash@!;Bg,Fx;!"; /* * Prim flashes running, followed by random color. */ -uint16_t mode_chase_flash_random(void) { - if (SEGLEN == 1) return mode_static(); +void mode_chase_flash_random(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + unsigned now = strip.now; // save time for delay calculation + bool advance = true; + if (now < SEGENV.step) { + SEGENV.call--; // revert increment to skip moving the animation forward and just render the same frame again + advance = false; + } unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); for (int i = 0; i < SEGENV.aux1; i++) { @@ -1063,14 +1144,15 @@ uint16_t mode_chase_flash_random(void) { SEGMENT.setPixelColor( m, SEGCOLOR(1)); delay = 30; } - } else { + } else if (advance) { SEGENV.aux1 = (SEGENV.aux1 + 1) % SEGLEN; if (SEGENV.aux1 == 0) { SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); } } - return delay; + if (advance) + SEGENV.step = now + delay; // set next update time } static const char _data_FX_MODE_CHASE_FLASH_RANDOM[] PROGMEM = "Chase Flash Rnd@!;!,!;!"; @@ -1078,8 +1160,8 @@ static const char _data_FX_MODE_CHASE_FLASH_RANDOM[] PROGMEM = "Chase Flash Rnd@ /* * Alternating color/sec pixels running. */ -uint16_t mode_running_color(void) { - return running(SEGCOLOR(0), SEGCOLOR(1)); +void mode_running_color(void) { + running(SEGCOLOR(0), SEGCOLOR(1)); } static const char _data_FX_MODE_RUNNING_COLOR[] PROGMEM = "Chase 2@!,Width;!,!;!"; @@ -1087,17 +1169,17 @@ static const char _data_FX_MODE_RUNNING_COLOR[] PROGMEM = "Chase 2@!,Width;!,!;! /* * Random colored pixels running. ("Stream") */ -uint16_t mode_running_random(void) { +void mode_running_random(void) { uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; - if (SEGENV.call == 0) SEGENV.aux0 = random16(); // random seed for PRNG on start + if (SEGENV.call == 0) SEGENV.aux0 = hw_random(); // random seed for PRNG on start unsigned zoneSize = ((255-SEGMENT.intensity) >> 4) +1; uint16_t PRNG16 = SEGENV.aux0; unsigned z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); - for (unsigned i=SEGLEN-1; i > 0; i--) { + for (int i=SEGLEN-1; i >= 0; i--) { if (nzone || z >= zoneSize) { unsigned lastrand = PRNG16 >> 8; int16_t diff = 0; @@ -1116,7 +1198,6 @@ uint16_t mode_running_random(void) { } SEGENV.aux1 = it; - return FRAMETIME; } static const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = "Stream@!,Zone size;;!"; @@ -1124,21 +1205,21 @@ static const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = "Stream@!,Zone size;; /* * K.I.T.T. */ -uint16_t mode_larson_scanner(void) { - if (SEGLEN == 1) return mode_static(); +void mode_larson_scanner(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; const unsigned speed = FRAMETIME * map(SEGMENT.speed, 0, 255, 96, 2); // map into useful range const unsigned pixels = SEGLEN / speed; // how many pixels to advance per frame SEGMENT.fade_out(255-SEGMENT.intensity); - if (SEGENV.step > strip.now) return FRAMETIME; // we have a pause + if (SEGENV.step > strip.now) return; // we have a pause unsigned index = SEGENV.aux1 + pixels; // are we slow enough to use frames per pixel? if (pixels == 0) { const unsigned frames = speed / SEGLEN; // how many frames per 1 pixel - if (SEGENV.step++ < frames) return FRAMETIME; + if (SEGENV.step++ < frames) return; SEGENV.step = 0; index++; } @@ -1164,7 +1245,6 @@ uint16_t mode_larson_scanner(void) { } SEGENV.aux1 = index; } - return FRAMETIME; } static const char _data_FX_MODE_LARSON_SCANNER[] PROGMEM = "Scanner@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; @@ -1172,18 +1252,17 @@ static const char _data_FX_MODE_LARSON_SCANNER[] PROGMEM = "Scanner@!,Trail,Dela * 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){ +void mode_dual_larson_scanner(void){ SEGMENT.check1 = true; - return mode_larson_scanner(); + mode_larson_scanner(); } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; - /* * Firing comets from one end. "Lighthouse" */ -uint16_t mode_comet(void) { - if (SEGLEN == 1) return mode_static(); +void mode_comet(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned counter = (strip.now * ((SEGMENT.speed >>2) +1)) & 0xFFFF; unsigned index = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) SEGENV.aux0 = index; @@ -1201,19 +1280,16 @@ uint16_t mode_comet(void) { } } SEGENV.aux0 = index++; - - return FRAMETIME; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; - /* * Fireworks function. */ -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(); +void mode_fireworks() { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + const uint16_t width = SEGMENT.is2D() ? SEG_W : SEGLEN; + const uint16_t height = SEG_H; if (SEGENV.call == 0) { SEGENV.aux0 = UINT16_MAX; @@ -1235,27 +1311,25 @@ uint16_t mode_fireworks() { } for (int i=0; i> 1)) == 0) { - uint16_t index = random16(width*height); + if (hw_random8(129 - (SEGMENT.intensity >> 1)) == 0) { + uint16_t index = hw_random16(width*height); x = index % width; y = index / width; - uint32_t col = SEGMENT.color_from_palette(random8(), false, false, 0); + uint32_t col = SEGMENT.color_from_palette(hw_random8(), false, false, 0); if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, col); else SEGMENT.setPixelColor(index, col); SEGENV.aux1 = SEGENV.aux0; // old spark SEGENV.aux0 = index; // remember where spark occurred } } - return FRAMETIME; } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; - //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h -uint16_t mode_rain() { - if (SEGLEN == 1) return mode_static(); - const unsigned width = SEGMENT.virtualWidth(); - const unsigned height = SEGMENT.virtualHeight(); +void mode_rain() { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + const unsigned width = SEG_W; + const unsigned height = SEG_H; SEGENV.step += FRAMETIME; if (SEGENV.call && SEGENV.step > SPEED_FORMULA_L) { SEGENV.step = 1; @@ -1269,7 +1343,7 @@ uint16_t mode_rain() { } else { //shift all leds left uint32_t ctemp = SEGMENT.getPixelColor(0); - for (int i = 0; i < SEGLEN - 1; i++) { + for (unsigned i = 0; i < SEGLEN - 1; i++) { SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); } SEGMENT.setPixelColor(SEGLEN -1, ctemp); // wrap around @@ -1281,18 +1355,17 @@ uint16_t mode_rain() { if (SEGENV.aux0 >= width*height) SEGENV.aux0 = 0; // ignore if (SEGENV.aux1 >= width*height) SEGENV.aux1 = 0; } - return mode_fireworks(); + mode_fireworks(); } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; - /* * Fire flicker function */ -uint16_t mode_fire_flicker(void) { +void mode_fire_flicker(void) { uint32_t cycleTime = 40 + (255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; - if (SEGENV.step == it) return FRAMETIME; + if (SEGENV.step == it) return; byte w = (SEGCOLOR(0) >> 24); byte r = (SEGCOLOR(0) >> 16); @@ -1300,8 +1373,8 @@ uint16_t mode_fire_flicker(void) { byte b = (SEGCOLOR(0) ); byte lum = (SEGMENT.palette == 0) ? MAX(w, MAX(r, MAX(g, b))) : 255; lum /= (((256-SEGMENT.intensity)/16)+1); - for (int i = 0; i < SEGLEN; i++) { - byte flicker = random8(lum); + for (unsigned i = 0; i < SEGLEN; i++) { + byte flicker = hw_random8(lum); if (SEGMENT.palette == 0) { SEGMENT.setPixelColor(i, MAX(r - flicker, 0), MAX(g - flicker, 0), MAX(b - flicker, 0), MAX(w - flicker, 0)); } else { @@ -1310,7 +1383,6 @@ uint16_t mode_fire_flicker(void) { } SEGENV.step = it; - return FRAMETIME; } static const char _data_FX_MODE_FIRE_FLICKER[] PROGMEM = "Fire Flicker@!,!;!;!;01"; @@ -1318,8 +1390,8 @@ static const char _data_FX_MODE_FIRE_FLICKER[] PROGMEM = "Fire Flicker@!,!;!;!;0 /* * Gradient run base function */ -uint16_t gradient_base(bool loading) { - if (SEGLEN == 1) return mode_static(); +void gradient_base(bool loading) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1); uint16_t pp = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) pp = 0; @@ -1329,25 +1401,23 @@ uint16_t gradient_base(bool loading) { int p1 = pp-SEGLEN; int p2 = pp+SEGLEN; - for (int i = 0; i < SEGLEN; i++) { + for (int i = 0; i < (int)SEGLEN; i++) { if (loading) { val = abs(((i>pp) ? p2:pp) - i); } else { val = min(abs(pp-i),min(abs(p1-i),abs(p2-i))); } val = (brd > val) ? (val * 255) / brd : 255; - SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.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), uint8_t(val))); } - - return FRAMETIME; } /* * Gradient run */ -uint16_t mode_gradient(void) { - return gradient_base(false); +void mode_gradient(void) { + gradient_base(false); } static const char _data_FX_MODE_GRADIENT[] PROGMEM = "Gradient@!,Spread;!,!;!;;ix=16"; @@ -1355,45 +1425,30 @@ static const char _data_FX_MODE_GRADIENT[] PROGMEM = "Gradient@!,Spread;!,!;!;;i /* * Gradient run with hard transition */ -uint16_t mode_loading(void) { - return gradient_base(true); +void mode_loading(void) { + 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 police_base(uint32_t color1, uint32_t color2) { - if (SEGLEN == 1) return mode_static(); +/* + * Two dots running + */ +void mode_two_dots() { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned delay = 1 + (FRAMETIME<<3) / SEGLEN; // longer segments should change faster uint32_t it = strip.now / map(SEGMENT.speed, 0, 255, delay<<4, delay); unsigned offset = it % SEGLEN; - unsigned width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip if (!width) width = 1; + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2)); + const uint32_t color1 = SEGCOLOR(0); + const uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? color1 : SEGCOLOR(1); for (unsigned i = 0; i < width; i++) { unsigned indexR = (offset + i) % SEGLEN; unsigned indexB = (offset + i + (SEGLEN>>1)) % SEGLEN; SEGMENT.setPixelColor(indexR, color1); SEGMENT.setPixelColor(indexB, color2); } - return FRAMETIME; -} - - -//Police Lights Red and Blue -//uint16_t mode_police() -//{ -// SEGMENT.fill(SEGCOLOR(1)); -// return police_base(RED, BLUE); -//} -//static const char _data_FX_MODE_POLICE[] PROGMEM = "Police@!,Width;,Bg;0"; - - -//Police Lights with custom colors -uint16_t mode_two_dots() { - if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2)); - uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? SEGCOLOR(0) : SEGCOLOR(1); - return police_base(SEGCOLOR(0), color2); } static const char _data_FX_MODE_TWO_DOTS[] PROGMEM = "Two Dots@!,Dot size,,,,,Overlay;1,2,Bg;!"; @@ -1411,21 +1466,21 @@ typedef struct Flasher { #define FLASHERS_PER_ZONE 6 #define MAX_SHIMMER 92 -uint16_t mode_fairy() { +void 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++) { + for (unsigned 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; + if (SEGMENT.intensity == 0) return; unsigned flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 unsigned numFlashers = (SEGLEN / flasherDistance) +1; unsigned dataSize = sizeof(flasher) * numFlashers; - if (!SEGENV.allocateData(dataSize)) return FRAMETIME; //allocation failed + if (!SEGENV.allocateData(dataSize)) return; //allocation failed Flasher* flashers = reinterpret_cast(SEGENV.data); unsigned now16 = strip.now & 0xFFFF; @@ -1441,16 +1496,16 @@ uint16_t mode_fairy() { if (z == zones-1) flashersInZone = numFlashers-(flashersInZone*(zones-1)); for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - unsigned stateTime = now16 - flashers[f].stateStart; + unsigned stateTime = uint16_t(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 + flashers[f].stateDur = 12 + hw_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 = 20 + hw_random8(6 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms } - //flashers[f].stateDur = 51 + random8(2 + ((255 - SEGMENT.speed) >> 1)); + //flashers[f].stateDur = 51 + hw_random8(2 + ((255 - SEGMENT.speed) >> 1)); flashers[f].stateStart = now16; if (stateTime < 255) { flashers[f].stateStart -= 255 -stateTime; //start early to get correct bri @@ -1470,7 +1525,7 @@ uint16_t mode_fairy() { unsigned globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 8); //183-255, suitable for 1/5th of LEDs flashers for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - unsigned bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; + uint8_t bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number unsigned flasherPos = f*flasherDistance; SEGMENT.setPixelColor(flasherPos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), bri)); @@ -1480,7 +1535,6 @@ uint16_t mode_fairy() { } } } - return FRAMETIME; } static const char _data_FX_MODE_FAIRY[] PROGMEM = "Fairy@!,# of flashers;!,!;!"; @@ -1489,9 +1543,9 @@ 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() { +void mode_fairytwinkle() { unsigned dataSize = sizeof(flasher) * SEGLEN; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Flasher* flashers = reinterpret_cast(SEGENV.data); unsigned now16 = strip.now & 0xFFFF; uint16_t PRNG16 = 5100 + strip.getCurrSegmentId(); @@ -1499,29 +1553,29 @@ uint16_t mode_fairytwinkle() { unsigned riseFallTime = 400 + (255-SEGMENT.speed)*3; unsigned maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1); - for (int f = 0; f < SEGLEN; f++) { - unsigned stateTime = now16 - flashers[f].stateStart; + for (unsigned 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; + flashers[f].stateDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + hw_random8(12 + ((255 - SEGMENT.intensity) >> 1)) +1; } else { - flashers[f].stateDur = riseFallTime/100 + random8(3 + ((255 - SEGMENT.speed) >> 6)) +1; + flashers[f].stateDur = riseFallTime/100 + hw_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 + flashers[f].stateDur = riseFallTime/100 + hw_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 unsigned fadeprog = 255 - ((stateTime * 255) / riseFallTime); - unsigned flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); + uint8_t flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); unsigned lastR = PRNG16; unsigned diff = 0; while (diff < 0x4000) { //make sure colors of two adjacent LEDs differ enough @@ -1530,7 +1584,6 @@ uint16_t mode_fairytwinkle() { } 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 @@ -1538,13 +1591,13 @@ static const char _data_FX_MODE_FAIRYTWINKLE[] PROGMEM = "Fairytwinkle@!,!;!,!;! /* * Tricolor chase function */ -uint16_t tricolor_chase(uint32_t color1, uint32_t color2) { +void tricolor_chase(uint32_t color1, uint32_t color2) { uint32_t cycleTime = 50 + ((255 - SEGMENT.speed)<<1); uint32_t it = strip.now / cycleTime; // iterator unsigned width = (1 + (SEGMENT.intensity>>4)); // value of 1-16 for each colour unsigned index = it % (width*3); - for (int i = 0; i < SEGLEN; i++, index++) { + for (unsigned i = 0; i < SEGLEN; i++, index++) { if (index > (width*3)-1) index = 0; uint32_t color = color1; @@ -1553,15 +1606,14 @@ uint16_t tricolor_chase(uint32_t color1, uint32_t color2) { SEGMENT.setPixelColor(SEGLEN - i -1, color); } - return FRAMETIME; } /* * Tricolor chase mode */ -uint16_t mode_tricolor_chase(void) { - return tricolor_chase(SEGCOLOR(2), SEGCOLOR(0)); +void mode_tricolor_chase(void) { + tricolor_chase(SEGCOLOR(2), SEGCOLOR(0)); } static const char _data_FX_MODE_TRICOLOR_CHASE[] PROGMEM = "Chase 3@!,Size;1,2,3;!"; @@ -1569,40 +1621,71 @@ static const char _data_FX_MODE_TRICOLOR_CHASE[] PROGMEM = "Chase 3@!,Size;1,2,3 /* * ICU mode */ -uint16_t mode_icu(void) { - unsigned dest = SEGENV.step & 0xFFFF; - unsigned space = (SEGMENT.intensity >> 3) +2; +void mode_icu(void) { + // states: 0 = pause1, 1 = blink, 2 = pause2, 3 = move - if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); + uint16_t now = strip.now; // save time for delay calculation, use low16 bits only + unsigned dest = SEGENV.aux1; + unsigned space = (SEGMENT.intensity >> 3) +2; + uint16_t state = SEGENV.step >> 16; // upper bytes of step store current state + uint16_t nextUpdate = SEGENV.step & 0xFFFF; // lower bytes store time for next update byte pindex = map(dest, 0, SEGLEN-SEGLEN/space, 0, 255); uint32_t col = SEGMENT.color_from_palette(pindex, false, false, 0); - - 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 - SEGMENT.setPixelColor(dest, SEGCOLOR(1)); - SEGMENT.setPixelColor(dest + SEGLEN/space, SEGCOLOR(1)); - return 200; + uint32_t bgcol = SEGMENT.check2 ? BLACK : SEGCOLOR(1); + SEGMENT.fill(bgcol); // apply background color or clear + // draw eyes if not blinking + if (state != 1) { + SEGMENT.setPixelColor(dest, col); + SEGMENT.setPixelColor(dest + SEGLEN/space, col); + // render next position if moving + if (state == 3) { + if(SEGENV.aux0 > SEGENV.aux1) { + dest++; + } else if (SEGENV.aux0 < SEGENV.aux1) { + dest--; + } + SEGMENT.setPixelColor(dest, col); + SEGMENT.setPixelColor(dest + SEGLEN/space, col); } - SEGENV.aux0 = random16(SEGLEN-SEGLEN/space); - return 1000 + random16(2000); } - if(SEGENV.aux0 > SEGENV.step) { - SEGENV.step++; - dest++; - } else if (SEGENV.aux0 < SEGENV.step) { - SEGENV.step--; - dest--; + // update state + if ((int16_t)(now - nextUpdate) >= 0) { // time to update, cast to int to handle wraparound properly + switch (state) { + case 0: // pause part 1 + // first pause part finished, blink or pause some more + state++; + if(hw_random8(6) == 0) { // blink once in a while + nextUpdate = uint16_t(now + 200); + break; + } + // fall through if not blinking + case 1: // blink + // not blinking or finished blinking -> pause part 2 + nextUpdate = uint16_t(now + 500 + hw_random16(1000)); + state++; + break; + case 2: // pause part 2 + // pause finished, move + SEGENV.aux0 = hw_random16(SEGLEN-SEGLEN/space); // choose a new destination + nextUpdate = now; + state++; + break; + default: // move (state 3) + SEGENV.aux1 = dest; // update destination to moved position + nextUpdate = uint16_t(now + SPEED_FORMULA_L); + if (SEGENV.aux0 == dest) { + // reached destination + nextUpdate = uint16_t(now + 500 + hw_random16(1000)); + state = 0; + } + break; + } } - SEGMENT.setPixelColor(dest, col); - SEGMENT.setPixelColor(dest + SEGLEN/space, col); - - return SPEED_FORMULA_L; + // use upper bits of SEGENV.step to store current state, lower bits for next update time + SEGENV.step = (state << 16) | nextUpdate; } static const char _data_FX_MODE_ICU[] PROGMEM = "ICU@!,!,,,,,Overlay;!,!;!"; @@ -1610,14 +1693,14 @@ static const char _data_FX_MODE_ICU[] PROGMEM = "ICU@!,!,,,,,Overlay;!,!;!"; /* * Custom mode by Aircoookie. Color Wipe, but with 3 colors */ -uint16_t mode_tricolor_wipe(void) { +void mode_tricolor_wipe(void) { uint32_t cycleTime = 1000 + (255 - SEGMENT.speed)*200; uint32_t perc = strip.now % cycleTime; unsigned prog = (perc * 65535) / cycleTime; unsigned ledIndex = (prog * SEGLEN * 3) >> 16; unsigned ledOffset = ledIndex; - for (int i = 0; i < SEGLEN; i++) + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2)); } @@ -1641,8 +1724,6 @@ uint16_t mode_tricolor_wipe(void) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } } - - return FRAMETIME; } static const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = "Tri Wipe@!;1,2,3;!"; @@ -1652,9 +1733,9 @@ static const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = "Tri Wipe@!;1,2,3;!"; * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/TriFade.h * Modified by Aircoookie */ -uint16_t mode_tricolor_fade(void) { - unsigned counter = strip.now * ((SEGMENT.speed >> 3) +1); - uint16_t prog = (counter * 768) >> 16; +void 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; unsigned stage = 0; @@ -1685,22 +1766,19 @@ uint16_t mode_tricolor_fade(void) { } 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 */ #define MAX_COMETS 8 -uint16_t mode_multi_comet(void) { +void mode_multi_comet(void) { uint32_t cycleTime = 10 + (uint32_t)(255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; - if (SEGENV.step == it) return FRAMETIME; - if (!SEGENV.allocateData(sizeof(uint16_t) * MAX_COMETS)) return mode_static(); //allocation failed + if (SEGENV.step == it) return; + if (!SEGENV.allocateData(sizeof(uint16_t) * MAX_COMETS)) FX_FALLBACK_STATIC; //allocation failed SEGMENT.fade_out(SEGMENT.intensity/2 + 128); @@ -1718,14 +1796,13 @@ uint16_t mode_multi_comet(void) { } comets[i]++; } else { - if(!random16(SEGLEN)) { + if(!hw_random16(SEGLEN)) { comets[i] = 0; } } } SEGENV.step = it; - return FRAMETIME; } static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1"; #undef MAX_COMETS @@ -1734,7 +1811,7 @@ static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!; * Running random pixels ("Stream 2") * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/RandomChase.h */ -uint16_t mode_random_chase(void) { +void mode_random_chase(void) { if (SEGENV.call == 0) { SEGENV.step = RGBW32(random8(), random8(), random8(), 0); SEGENV.aux0 = random16(); @@ -1745,7 +1822,7 @@ uint16_t mode_random_chase(void) { uint32_t color = SEGENV.step; random16_set_seed(SEGENV.aux0); - for (unsigned i = SEGLEN -1; i > 0; i--) { + for (int i = SEGLEN -1; i >= 0; i--) { uint8_t r = random8(6) != 0 ? (color >> 16 & 0xFF) : random8(); uint8_t g = random8(6) != 0 ? (color >> 8 & 0xFF) : random8(); uint8_t b = random8(6) != 0 ? (color & 0xFF) : random8(); @@ -1760,7 +1837,6 @@ uint16_t mode_random_chase(void) { 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@!;;"; @@ -1776,11 +1852,11 @@ typedef struct Oscillator { /* / Oscillating bars of color, updated with standard framerate */ -uint16_t mode_oscillate(void) { +void mode_oscillate(void) { constexpr unsigned numOscillators = 3; constexpr unsigned dataSize = sizeof(oscillator) * numOscillators; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Oscillator* oscillators = reinterpret_cast(SEGENV.data); @@ -1798,45 +1874,43 @@ uint16_t mode_oscillate(void) { // 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); - if((oscillators[i].dir == -1) && (oscillators[i].pos <= 0)) { + if((oscillators[i].dir == -1) && (oscillators[i].pos > SEGLEN << 1)) { // use integer overflow oscillators[i].pos = 0; oscillators[i].dir = 1; // make bigger steps for faster speeds - oscillators[i].speed = SEGMENT.speed > 100 ? random8(2, 4):random8(1, 3); + oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3); } if((oscillators[i].dir == 1) && (oscillators[i].pos >= (SEGLEN - 1))) { oscillators[i].pos = SEGLEN - 1; oscillators[i].dir = -1; - oscillators[i].speed = SEGMENT.speed > 100 ? random8(2, 4):random8(1, 3); + oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3); } } for (unsigned i = 0; i < SEGLEN; i++) { uint32_t color = BLACK; for (unsigned j = 0; j < numOscillators; j++) { - if(i >= (unsigned)oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { - color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), 128); + if((int)i >= (int)oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { + color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), uint8_t(128)); } } SEGMENT.setPixelColor(i, color); } SEGENV.step = it; - return FRAMETIME; } static const char _data_FX_MODE_OSCILLATE[] PROGMEM = "Oscillate"; -//TODO -uint16_t mode_lightning(void) { - if (SEGLEN == 1) return mode_static(); - unsigned ledstart = random16(SEGLEN); // Determine starting location of flash - unsigned ledlen = 1 + random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) - uint8_t bri = 255/random8(1, 3); +void mode_lightning(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + unsigned ledstart = hw_random16(SEGLEN); // Determine starting location of flash + unsigned ledlen = 1 + hw_random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) + uint8_t bri = 255/hw_random8(1, 3); if (SEGENV.aux1 == 0) //init, leader flash { - SEGENV.aux1 = random8(4, 4 + SEGMENT.intensity/20); //number of flashes + SEGENV.aux1 = hw_random8(4, 4 + SEGMENT.intensity/20); //number of flashes SEGENV.aux1 *= 2; bri = 52; //leader has lower brightness @@ -1853,69 +1927,91 @@ uint16_t mode_lightning(void) { SEGENV.aux1--; SEGENV.step = strip.now; - //return random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds + //return hw_random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds } else { if (strip.now - SEGENV.step > SEGENV.aux0) { SEGENV.aux1--; if (SEGENV.aux1 < 2) SEGENV.aux1 = 0; - SEGENV.aux0 = (50 + random8(100)); //delay between flashes + SEGENV.aux0 = (50 + hw_random8(100)); //delay between flashes if (SEGENV.aux1 == 2) { - SEGENV.aux0 = (random8(255 - SEGMENT.speed) * 100); // delay between strikes + SEGENV.aux0 = (hw_random8(255 - SEGMENT.speed) * 100); // delay between strikes } SEGENV.step = strip.now; } } - 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 mode_pride_2015(void) { +// combined function from original pride and colorwaves +void mode_colorwaves_pride_base(bool isPride2015) { unsigned duration = 10 + SEGMENT.speed; unsigned sPseudotime = SEGENV.step; unsigned sHue16 = SEGENV.aux0; - uint8_t sat8 = beatsin88_t( 87, 220, 250); - uint8_t brightdepth = beatsin88_t( 341, 96, 224); - unsigned brightnessthetainc16 = beatsin88_t( 203, (25 * 256), (40 * 256)); + uint8_t sat8 = isPride2015 ? beatsin88_t(87, 220, 250) : 255; + unsigned brightdepth = beatsin88_t(341, 96, 224); + unsigned brightnessthetainc16 = beatsin88_t(203, (25 * 256), (40 * 256)); unsigned msmultiplier = beatsin88_t(147, 23, 60); - unsigned hue16 = sHue16;//gHue * 256; - unsigned hueinc16 = beatsin88_t(113, 1, 3000); + unsigned hue16 = sHue16; + unsigned hueinc16 = isPride2015 ? beatsin88_t(113, 1, 3000) : + beatsin88_t(113, 60, 300) * SEGMENT.intensity * 10 / 255; sPseudotime += duration * msmultiplier; - sHue16 += duration * beatsin88_t( 400, 5,9); + sHue16 += duration * beatsin88_t(400, 5, 9); unsigned brightnesstheta16 = sPseudotime; - for (unsigned i = 0 ; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { hue16 += hueinc16; - uint8_t hue8 = hue16 >> 8; + uint8_t hue8; - brightnesstheta16 += brightnessthetainc16; - unsigned b16 = sin16_t( brightnesstheta16 ) + 32768; + if (isPride2015) { + hue8 = hue16 >> 8; + } else { + unsigned h16_128 = hue16 >> 7; + hue8 = (h16_128 & 0x100) ? (255 - (h16_128 >> 1)) : (h16_128 >> 1); + } + brightnesstheta16 += brightnessthetainc16; + unsigned b16 = sin16_t(brightnesstheta16) + 32768; unsigned 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 = CHSV(hue8, sat8, bri8); - SEGMENT.blendPixelColor(i, newcolor, 64); + if (isPride2015) { + CRGBW newcolor = CRGB(CHSV(hue8, sat8, bri8)); + newcolor.color32 = gamma32inv(newcolor.color32); + SEGMENT.blendPixelColor(i, newcolor, 64); + } else { + SEGMENT.blendPixelColor(i, SEGMENT.color_from_palette(hue8, false, PALETTE_SOLID_WRAP, 0, bri8), 128); + } } + SEGENV.step = sPseudotime; SEGENV.aux0 = sHue16; +} - return FRAMETIME; +// Pride2015 +// Animated, ever-changing rainbows. +// by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5 +void mode_pride_2015(void) { + mode_colorwaves_pride_base(true); } static const char _data_FX_MODE_PRIDE_2015[] PROGMEM = "Pride 2015@!;;"; +// 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. +void mode_colorwaves() { + mode_colorwaves_pride_base(false); +} +static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!;;pal=26"; + //eight colored dots, weaving in and out of sync with each other -uint16_t mode_juggle(void) { - if (SEGLEN == 1) return mode_static(); +void mode_juggle(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; SEGMENT.fadeToBlackBy(192 - (3*SEGMENT.intensity/4)); CRGB fastled_col; @@ -1923,16 +2019,15 @@ uint16_t mode_juggle(void) { for (int i = 0; i < 8; i++) { int index = 0 + beatsin88_t((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); + fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):CRGB(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 mode_palette() { +void mode_palette() { // Set up some compile time constants so that we can handle integer and float based modes using the same code base. #ifdef ESP8266 using mathType = int32_t; @@ -1956,8 +2051,8 @@ uint16_t mode_palette() { constexpr float (*cosFunction)(float) = &cos_t; #endif const bool isMatrix = strip.isMatrix; - const int cols = SEGMENT.virtualWidth(); - const int rows = isMatrix ? SEGMENT.virtualHeight() : strip.getActiveSegmentsNum(); + const int cols = SEG_W; + const int rows = isMatrix ? SEG_H : strip.getActiveSegmentsNum(); const int inputShift = SEGMENT.speed; const int inputSize = SEGMENT.intensity; @@ -2003,7 +2098,7 @@ uint16_t mode_palette() { const mathType sourceX = xtSinTheta + ytCosTheta + centerX; // The computation was scaled just right so that the result should always be in range [0, maxXOut], but enforce this anyway // to account for imprecision. Then scale it so that the range is [0, 255], which we can use with the palette. - int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * 255) / (sInt16Scale * maxXOut); + int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * wideMathType(255)) / (sInt16Scale * maxXOut); // inputSize determines by how much we want to scale the palette: // values < 128 display a fraction of a palette, // values > 128 display multiple palettes. @@ -2026,11 +2121,10 @@ uint16_t mode_palette() { } } } - return FRAMETIME; } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; - +#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX) // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2059,10 +2153,10 @@ static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation // There are two main parameters you can play with to control the look and // feel of your fire: COOLING (used in step 1 above) (Speed = COOLING), and SPARKING (used // in step 3 above) (Effect Intensity = Sparking). -uint16_t mode_fire_2012() { - if (SEGLEN == 1) return mode_static(); +void mode_fire_2012() { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; const unsigned strips = SEGMENT.nrOfVStrips(); - if (!SEGENV.allocateData(strips * SEGLEN)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(strips * SEGLEN)) FX_FALLBACK_STATIC; //allocation failed byte* heat = SEGENV.data; const uint32_t it = strip.now >> 5; //div 32 @@ -2070,11 +2164,11 @@ uint16_t mode_fire_2012() { 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 + 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) : random8(4); + for (unsigned i = 0; i < SEGLEN; i++) { + uint8_t cool = (it != SEGENV.step) ? hw_random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : hw_random8(4); uint8_t minTemp = (i> 8; - unsigned h16_128 = hue16 >> 7; - if ( h16_128 & 0x100) { - hue8 = 255 - (h16_128 >> 1); - } else { - hue8 = h16_128 >> 1; - } - - brightnesstheta16 += brightnessthetainc16; - unsigned b16 = sin16_t(brightnesstheta16) + 32768; - - unsigned bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; - uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; - bri8 += (255 - brightdepth); - - 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;!;!"; - +static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars +#endif // WLED_PS_DONT_REPLACE_x_FX // colored stripes pulsing at a defined Beats-Per-Minute (BPM) -uint16_t mode_bpm() { +void mode_bpm() { uint32_t stp = (strip.now / 20) & 0xFF; uint8_t beat = beatsin8_t(SEGMENT.speed, 64, 255); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(stp + (i * 2), false, PALETTE_SOLID_WRAP, 0, beat - stp + (i * 10))); } - - return FRAMETIME; } static const char _data_FX_MODE_BPM[] PROGMEM = "Bpm@!;!;!;;sx=64"; -uint16_t mode_fillnoise8() { - if (SEGENV.call == 0) SEGENV.step = random16(12345); - for (int i = 0; i < SEGLEN; i++) { - unsigned index = inoise8(i * SEGLEN, SEGENV.step + i * SEGLEN); +void mode_fillnoise8() { + if (SEGENV.call == 0) SEGENV.step = hw_random(); + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned index = perlin8(i * SEGLEN, SEGENV.step + i * SEGLEN); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } SEGENV.step += beatsin8_t(SEGMENT.speed, 1, 6); //10,1,4 - - return FRAMETIME; } static const char _data_FX_MODE_FILLNOISE8[] PROGMEM = "Fill Noise@!;!;!"; -uint16_t mode_noise16_1() { +void mode_noise16_1() { unsigned scale = 320; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed/16); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned shift_x = beatsin8_t(11); // the x position of the noise field swings @ 17 bpm unsigned shift_y = SEGENV.step/42; // the y position becomes slowly incremented unsigned real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm unsigned real_y = (i + shift_y) * scale; // the y position becomes slowly incremented uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented - unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned noise = perlin16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map LED color based on noise data 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@!;!;!"; +static const char _data_FX_MODE_NOISE16_1[] PROGMEM = "Noise 1@!;!;!;;pal=20"; -uint16_t mode_noise16_2() { +void mode_noise16_2() { unsigned scale = 1000; // the "zoom factor" for the noise SEGENV.step += (1 + (SEGMENT.speed >> 1)); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned 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 - unsigned noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down + unsigned noise = perlin16(real_x, 0, 4223) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map led color based on noise data 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@!;!;!"; +static const char _data_FX_MODE_NOISE16_2[] PROGMEM = "Noise 2@!;!;!;;pal=43"; -uint16_t mode_noise16_3() { +void mode_noise16_3() { unsigned scale = 800; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned shift_x = 4223; // no movement along x and y unsigned 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; - unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned noise = perlin16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map led color based on noise data 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@!;!;!"; +static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!;;pal=35"; //https://github.com/aykevl/ledstrip-spark/blob/master/ledstrip.ino -uint16_t mode_noise16_4() { +void mode_noise16_4() { uint32_t stp = (strip.now * SEGMENT.speed) >> 7; - for (int i = 0; i < SEGLEN; i++) { - int index = inoise16(uint32_t(i) << 12, stp); + for (unsigned i = 0; i < SEGLEN; i++) { + int index = perlin16(uint32_t(i) << 12, stp); 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@!;!;!"; +static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!;;pal=26"; //based on https://gist.github.com/kriegsman/5408ecd397744ba0393e -uint16_t mode_colortwinkle() { +void mode_colortwinkle() { unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed - CRGB fastled_col, prev; + //limit update rate + if (strip.now - SEGENV.step < FRAMETIME_FIXED) return; + SEGENV.step = strip.now; + + CRGBW col, prev; 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 (int i = 0; i < SEGLEN; i++) { - fastled_col = SEGMENT.getPixelColor(i); - prev = fastled_col; + for (unsigned i = 0; i < SEGLEN; i++) { + CRGBW cur = SEGMENT.getPixelColor(i); + prev = cur; unsigned index = i >> 3; unsigned bitNum = i & 0x07; bool fadeUp = bitRead(SEGENV.data[index], bitNum); if (fadeUp) { - CRGB incrementalColor = fastled_col; - incrementalColor.nscale8_video(fadeUpAmount); - fastled_col += incrementalColor; + CRGBW incrementalColor = color_fade(cur, fadeUpAmount, true); + col = color_add(cur, incrementalColor); - if (fastled_col.red == 255 || fastled_col.green == 255 || fastled_col.blue == 255) { + if (col.r == 255 || col.g == 255 || col.b == 255) { bitWrite(SEGENV.data[index], bitNum, false); } - SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); - if (SEGMENT.getPixelColor(i) == RGBW32(prev.r, prev.g, prev.b, 0)) { //fix "stuck" pixels - fastled_col += fastled_col; - SEGMENT.setPixelColor(i, fastled_col); + if (cur == prev) { //fix "stuck" pixels + col = color_add(col, col); + SEGMENT.setPixelColor(i, col); } - } else { - fastled_col.nscale8(255 - fadeDownAmount); - SEGMENT.setPixelColor(i, fastled_col); + else SEGMENT.setPixelColor(i, col); + } + else { + col = color_fade(cur, 255 - fadeDownAmount); + SEGMENT.setPixelColor(i, col); } } for (unsigned j = 0; j <= SEGLEN / 50; j++) { - if (random8() <= SEGMENT.intensity) { + if (hw_random8() <= SEGMENT.intensity) { for (unsigned times = 0; times < 5; times++) { //attempt to spawn a new pixel 5 times - int i = random16(SEGLEN); + int i = hw_random16(SEGLEN); if (SEGMENT.getPixelColor(i) == 0) { - fastled_col = ColorFromPalette(SEGPALETTE, random8(), 64, NOBLEND); unsigned index = i >> 3; unsigned bitNum = i & 0x07; bitWrite(SEGENV.data[index], bitNum, true); - SEGMENT.setPixelColor(i, fastled_col); + SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, hw_random8(), 64, NOBLEND)); break; //only spawn 1 new pixel per frame per 50 LEDs } } } } - 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 mode_lake() { +void mode_lake() { unsigned sp = SEGMENT.speed/10; int wave1 = beatsin8_t(sp +2, -64,64); int wave2 = beatsin8_t(sp +1, -64,64); int wave3 = beatsin8_t(sp +2, 0,80); - for (int i = 0; i < SEGLEN; i++) + for (unsigned i = 0; i < SEGLEN; i++) { int index = cos8_t((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; uint8_t lum = (index > wave3) ? index - wave3 : 0; 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 +// meteor effect & meteor smooth (merged by @dedehai) // 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 mode_meteor() { - if (SEGLEN == 1) return mode_static(); - if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed - +void mode_meteor() { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + if (!SEGENV.allocateData(SEGLEN)) FX_FALLBACK_STATIC; //allocation failed + const bool meteorSmooth = SEGMENT.check3; byte* trail = SEGENV.data; const unsigned meteorSize = 1 + SEGLEN / 20; // 5% - unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8); - uint16_t in = counter * SEGLEN >> 16; + uint16_t meteorstart; + if(meteorSmooth) meteorstart = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); + else { + unsigned counter = strip.now * ((SEGMENT.speed >> 2) + 8); + meteorstart = (counter * SEGLEN) >> 16; + } - const int max = SEGMENT.palette==5 ? 239 : 255; // "* Colors only" palette blends end with start - // fade all leds to colors[1] in LEDs one step - for (int i = 0; i < SEGLEN; i++) { - if (random8() <= 255 - SEGMENT.intensity) { - int meteorTrailDecay = 128 + random8(127); - trail[i] = scale8(trail[i], meteorTrailDecay); - int index = trail[i]; - int idx = 255; - int bri = SEGMENT.palette==35 || SEGMENT.palette==36 ? 255 : trail[i]; - if (!SEGMENT.check1) { - idx = 0; - index = map(i,0,SEGLEN,0,max); - bri = trail[i]; - } - uint32_t col = SEGMENT.color_from_palette(index, false, false, idx, bri); // full brightness for Fire - SEGMENT.setPixelColor(i, col); - } - } - - // draw meteor - for (unsigned j = 0; j < meteorSize; j++) { - int index = (in + j) % SEGLEN; - int idx = 255; - int i = trail[index] = max; - if (!SEGMENT.check1) { - i = map(index,0,SEGLEN,0,max); - idx = 0; - } - uint32_t col = SEGMENT.color_from_palette(i, false, false, idx, 255); // full brightness - SEGMENT.setPixelColor(index, col); - } - - return FRAMETIME; -} -static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient;!;!;1"; - - -// smooth meteor effect -// send a meteor from begining to to the end of the strip with a trail that randomly decays. -// adapted from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectMeteorRain -uint16_t mode_meteor_smooth() { - if (SEGLEN == 1) return mode_static(); - if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed - - byte* trail = SEGENV.data; - - const unsigned meteorSize = 1+ SEGLEN / 20; // 5% - uint16_t in = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); - - const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; + const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // fade all leds to colors[1] in LEDs one step for (unsigned i = 0; i < SEGLEN; i++) { - if (/*trail[i] != 0 &&*/ random8() <= 255 - SEGMENT.intensity) { - int change = trail[i] + 4 - random8(24); //change each time between -20 and +4 - trail[i] = constrain(change, 0, max); - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + uint32_t col; + if (hw_random8() <= 255 - SEGMENT.intensity) { + if(meteorSmooth) { + if (trail[i] > 0) { + int change = trail[i] + 4 - hw_random8(24); //change each time between -20 and +4 + trail[i] = constrain(change, 0, max); + } + col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + } + else { + trail[i] = scale8(trail[i], 128 + hw_random8(127)); + int index = trail[i]; + int idx = 255; + int bri = SEGMENT.palette==35 || SEGMENT.palette==36 ? 255 : trail[i]; + if (!SEGMENT.check1) { + idx = 0; + index = map(i,0,SEGLEN,0,max); + bri = trail[i]; + } + col = SEGMENT.color_from_palette(index, false, false, idx, bri); // full brightness for Fire + } SEGMENT.setPixelColor(i, col); } } // draw meteor for (unsigned j = 0; j < meteorSize; j++) { - unsigned index = in + j; - if (index >= SEGLEN) { - index -= SEGLEN; + unsigned index = (meteorstart + j) % SEGLEN; + if(meteorSmooth) { + trail[index] = max; + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); + SEGMENT.setPixelColor(index, col); + } + else{ + int idx = 255; + int i = trail[index] = max; + if (!SEGMENT.check1) { + i = map(index,0,SEGLEN,0,max); + idx = 0; + } + uint32_t col = SEGMENT.color_from_palette(i, false, false, idx, 255); // full brightness + SEGMENT.setPixelColor(index, col); } - trail[index] = max; - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); - SEGMENT.setPixelColor(index, col); } SEGENV.step += SEGMENT.speed +1; - return FRAMETIME; } -static const char _data_FX_MODE_METEOR_SMOOTH[] PROGMEM = "Meteor Smooth@!,Trail,,,,Gradient;;!;1"; +static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient,,Smooth;;!;1"; //Railway Crossing / Christmas Fairy lights -uint16_t mode_railway() { - if (SEGLEN == 1) return mode_static(); +void mode_railway() { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned dur = (256 - SEGMENT.speed) * 40; uint16_t rampdur = (dur * SEGMENT.intensity) >> 8; if (SEGENV.step > dur) @@ -2445,7 +2460,7 @@ uint16_t mode_railway() { if (p0 < 255) pos = p0; } if (SEGENV.aux0) pos = 255 - pos; - for (int i = 0; i < SEGLEN; i += 2) + for (unsigned i = 0; i < SEGLEN; i += 2) { 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) @@ -2454,9 +2469,8 @@ uint16_t mode_railway() { } } SEGENV.step += FRAMETIME; - return FRAMETIME; } -static const char _data_FX_MODE_RAILWAY[] PROGMEM = "Railway@!,Smoothness;1,2;!"; +static const char _data_FX_MODE_RAILWAY[] PROGMEM = "Railway@!,Smoothness;1,2;!;;pal=3"; //Water ripple @@ -2475,11 +2489,11 @@ typedef struct Ripple { #else #define MAX_RIPPLES 100 #endif -static uint16_t ripple_base() { - unsigned maxRipples = min(1 + (SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 +static void ripple_base(uint8_t blurAmount = 0) { + unsigned maxRipples = min(1 + (int)(SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 unsigned dataSize = sizeof(ripple) * maxRipples; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); @@ -2508,7 +2522,7 @@ static uint16_t ripple_base() { int left = rippleorigin - propI -1; int right = rippleorigin + propI +2; for (int v = 0; v < 4; v++) { - unsigned mag = scale8(cubicwave8((propF>>2) + v * 64), amp); + uint8_t mag = scale8(cubicwave8((propF>>2) + v * 64), amp); SEGMENT.setPixelColor(left + v, color_blend(SEGMENT.getPixelColor(left + v), col, mag)); // TODO SEGMENT.setPixelColor(right - v, color_blend(SEGMENT.getPixelColor(right - v), col, mag)); // TODO } @@ -2516,43 +2530,45 @@ static uint16_t ripple_base() { ripplestate += rippledecay; ripples[i].state = (ripplestate > 254) ? 0 : ripplestate; } else {//randomly create new wave - if (random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) { + if (hw_random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) { ripples[i].state = 1; - ripples[i].pos = SEGMENT.is2D() ? ((random8(SEGENV.virtualWidth())<<8) | (random8(SEGENV.virtualHeight()))) : random16(SEGLEN); - ripples[i].color = random8(); //color + ripples[i].pos = SEGMENT.is2D() ? ((hw_random8(SEG_W)<<8) | (hw_random8(SEG_H))) : hw_random16(SEGLEN); + ripples[i].color = hw_random8(); //color } } } - - return FRAMETIME; + SEGMENT.blur(blurAmount); } #undef MAX_RIPPLES -uint16_t mode_ripple(void) { - if (SEGLEN == 1) return mode_static(); - if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - else SEGMENT.fade_out(250); - return ripple_base(); +void mode_ripple(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + if(SEGMENT.custom1 || SEGMENT.check2) // blur or overlay + SEGMENT.fade_out(250); + else + SEGMENT.fill(SEGCOLOR(1)); + + ripple_base(SEGMENT.custom1>>1); } -static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,,,,,Overlay;,!;!;12"; +static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,Blur,,,,Overlay;,!;!;12;c1=0"; -uint16_t mode_ripple_rainbow(void) { - if (SEGLEN == 1) return mode_static(); +void mode_ripple_rainbow(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; if (SEGENV.call ==0) { - SEGENV.aux0 = random8(); - SEGENV.aux1 = random8(); + SEGENV.aux0 = hw_random8(); + SEGENV.aux1 = hw_random8(); } if (SEGENV.aux0 == SEGENV.aux1) { - SEGENV.aux1 = random8(); + SEGENV.aux1 = hw_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(); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux0),BLACK,uint8_t(235))); + ripple_base(); } static const char _data_FX_MODE_RIPPLE_RAINBOW[] PROGMEM = "Ripple Rainbow@!,Wave #;;!;12"; @@ -2565,11 +2581,11 @@ static CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) { // Overall twinkle speed (changed) unsigned ticks = ms / SEGENV.aux0; - unsigned fastcycle8 = ticks; - unsigned slowcycle16 = (ticks >> 8) + salt; + unsigned fastcycle8 = uint8_t(ticks); + uint16_t slowcycle16 = (ticks >> 8) + salt; slowcycle16 += sin8_t(slowcycle16); slowcycle16 = (slowcycle16 * 2053) + 1384; - unsigned slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); + uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); // Overall twinkle density. // 0 (NONE lit) to 8 (ALL lit at once). @@ -2582,9 +2598,11 @@ static CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) // This is like 'triwave8', which produces a // symmetrical up-and-down triangle sawtooth waveform, except that this // function produces a triangle wave with a faster attack and a slower decay - if (cat) //twinklecat, variant where the leds instantly turn on - { + if (cat) { //twinklecat, variant where the leds instantly turn on and fade off bright = 255 - ph; + if (SEGMENT.check2) { //reverse checkbox, reverses the leds to fade on and instantly turn off + bright = ph; + } } else { //vanilla twinklefox if (ph < 86) { bright = ph * 3; @@ -2621,7 +2639,7 @@ static CRGB 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. -static uint16_t twinklefox_base(bool cat) +static void twinklefox_base(bool cat) { // "PRNG16" is the pseudorandom number generator // It MUST be reset to the same starting value each time @@ -2646,7 +2664,7 @@ static uint16_t twinklefox_base(bool cat) unsigned backgroundBrightness = bg.getAverageLight(); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number unsigned myclockoffset16= PRNG16; // use that number as clock offset @@ -2666,36 +2684,34 @@ static uint16_t twinklefox_base(bool cat) if (deltabright >= 32 || (!bg)) { // If the new pixel is significantly brighter than the background color, // use the new color. - SEGMENT.setPixelColor(i, c.red, c.green, c.blue); + SEGMENT.setPixelColor(i, c); } 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 - SEGMENT.setPixelColor(i, color_blend(RGBW32(bg.r,bg.g,bg.b,0), RGBW32(c.r,c.g,c.b,0), deltabright * 8)); + SEGMENT.setPixelColor(i, color_blend(RGBW32(bg.r,bg.g,bg.b,0), RGBW32(c.r,c.g,c.b,0), uint8_t(deltabright * 8))); } else { // if the new pixel is not at all brighter than the background color, // just use the background color. - SEGMENT.setPixelColor(i, bg.r, bg.g, bg.b); + SEGMENT.setPixelColor(i, bg); } } - return FRAMETIME; } - -uint16_t mode_twinklefox() +void mode_twinklefox() { - return twinklefox_base(false); + twinklefox_base(false); } static const char _data_FX_MODE_TWINKLEFOX[] PROGMEM = "Twinklefox@!,Twinkle rate,,,,Cool;!,!;!"; -uint16_t mode_twinklecat() +void mode_twinklecat() { - return twinklefox_base(true); + twinklefox_base(true); } -static const char _data_FX_MODE_TWINKLECAT[] PROGMEM = "Twinklecat@!,Twinkle rate,,,,Cool;!,!;!"; +static const char _data_FX_MODE_TWINKLECAT[] PROGMEM = "Twinklecat@!,Twinkle rate,,,,Cool,Reverse;!,!;!"; -uint16_t mode_halloween_eyes() +void mode_halloween_eyes() { enum eyeState : uint8_t { initializeOn = 0, @@ -2717,14 +2733,14 @@ uint16_t mode_halloween_eyes() uint32_t blinkEndTime; }; - if (SEGLEN == 1) return mode_static(); - const unsigned maxWidth = strip.isMatrix ? SEGMENT.virtualWidth() : SEGLEN; - const unsigned HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEGMENT.virtualWidth()>>4: SEGLEN>>5); + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + const unsigned maxWidth = strip.isMatrix ? SEG_W : SEGLEN; + const unsigned HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEG_W>>4: SEGLEN>>5); const unsigned HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2; unsigned eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE; - if (eyeLength >= maxWidth) return mode_static(); //bail if segment too short + if (eyeLength >= maxWidth) FX_FALLBACK_STATIC; //bail if segment too short - if (!SEGENV.allocateData(sizeof(EyeData))) return mode_static(); //allocation failed + if (!SEGENV.allocateData(sizeof(EyeData))) FX_FALLBACK_STATIC; //allocation failed EyeData& data = *reinterpret_cast(SEGENV.data); if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); //fill background @@ -2740,10 +2756,10 @@ uint16_t mode_halloween_eyes() // - select a duration // - immediately switch to eyes on state. - data.startPos = random16(0, maxWidth - eyeLength - 1); - data.color = random8(); - if (strip.isMatrix) SEGMENT.offset = random16(SEGMENT.virtualHeight()-1); // a hack: reuse offset since it is not used in matrices - duration = 128u + random16(SEGMENT.intensity*64u); + data.startPos = hw_random16(0, maxWidth - eyeLength - 1); + data.color = hw_random8(); + if (strip.isMatrix) SEGMENT.offset = hw_random16(SEG_H-1); // a hack: reuse offset since it is not used in matrices + duration = 128u + hw_random16(SEGMENT.intensity*64u); data.duration = duration; data.state = eyeState::on; [[fallthrough]]; @@ -2766,15 +2782,15 @@ uint16_t mode_halloween_eyes() const uint32_t eyeColor = SEGMENT.color_from_palette(data.color, false, false, 0); uint32_t c = eyeColor; if (fadeInAnimationState < 256u) { - c = color_blend(backgroundColor, eyeColor, fadeInAnimationState); + c = color_blend(backgroundColor, eyeColor, uint8_t(fadeInAnimationState)); } else if (elapsedTime > minimumOnTimeBegin) { const uint32_t remainingTime = (elapsedTime >= duration) ? 0u : (duration - elapsedTime); if (remainingTime > minimumOnTimeEnd) { - if (random8() < 4u) + if (hw_random8() < 4u) { c = backgroundColor; data.state = eyeState::blink; - data.blinkEndTime = strip.now + random8(8, 128); + data.blinkEndTime = strip.now + hw_random8(8, 128); } } } @@ -2808,7 +2824,7 @@ uint16_t mode_halloween_eyes() // - immediately switch to eyes-off state const unsigned eyeOffTimeBase = SEGMENT.speed*128u; - duration = eyeOffTimeBase + random16(eyeOffTimeBase); + duration = eyeOffTimeBase + hw_random16(eyeOffTimeBase); data.duration = duration; data.state = eyeState::off; [[fallthrough]]; @@ -2846,21 +2862,19 @@ uint16_t mode_halloween_eyes() } data.startTime = strip.now; } - - return FRAMETIME; } static const char _data_FX_MODE_HALLOWEEN_EYES[] PROGMEM = "Halloween Eyes@Eye off time,Eye on time,,,,,Overlay;!,!;!;12"; //Speed slider sets amount of LEDs lit, intensity sets unlit -uint16_t mode_static_pattern() +void mode_static_pattern() { unsigned lit = 1 + SEGMENT.speed; unsigned unlit = 1 + SEGMENT.intensity; bool drawingLit = true; unsigned cnt = 0; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned 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)) { @@ -2868,19 +2882,17 @@ uint16_t mode_static_pattern() drawingLit = !drawingLit; } } - - return FRAMETIME; } static const char _data_FX_MODE_STATIC_PATTERN[] PROGMEM = "Solid Pattern@Fg size,Bg size;Fg,!;!;;pal=0"; -uint16_t mode_tri_static_pattern() +void mode_tri_static_pattern() { unsigned segSize = (SEGMENT.intensity >> 5) +1; unsigned currSeg = 0; unsigned currSegCount = 0; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { if ( currSeg % 3 == 0 ) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } else if( currSeg % 3 == 1) { @@ -2894,15 +2906,13 @@ uint16_t mode_tri_static_pattern() currSegCount = 0; } } - - return FRAMETIME; } static const char _data_FX_MODE_TRI_STATIC_PATTERN[] PROGMEM = "Solid Pattern Tri@,Size;1,2,3;;;pal=0"; -static uint16_t spots_base(uint16_t threshold) +static void spots_base(uint16_t threshold) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) FX_FALLBACK_STATIC; if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); unsigned maxZones = SEGLEN >> 2; @@ -2919,34 +2929,31 @@ static uint16_t spots_base(uint16_t threshold) if (wave > threshold) { unsigned index = 0 + pos + i; unsigned s = (wave - threshold)*255 / (0xFFFF - threshold); - SEGMENT.setPixelColor(index, color_blend(SEGMENT.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), uint8_t(255-s))); } } } - - return FRAMETIME; } //Intensity slider sets number of "lights", speed sets LEDs per light -uint16_t mode_spots() +void mode_spots() { - return spots_base((255 - SEGMENT.speed) << 8); + 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 mode_spots_fade() +void mode_spots_fade() { unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8); unsigned t = triwave16(counter); unsigned tr = (t >> 1) + (t >> 2); - return spots_base(tr); + spots_base(tr); } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; - //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; @@ -2957,13 +2964,13 @@ typedef struct Ball { /* * Bouncing Balls Effect */ -uint16_t mode_bouncing_balls(void) { - if (SEGLEN == 1) return mode_static(); +void mode_bouncing_balls(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; //allocate segment data const unsigned strips = SEGMENT.nrOfVStrips(); // adapt for 2D const size_t maxNumBalls = 16; unsigned dataSize = sizeof(ball) * maxNumBalls; - if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed Ball* balls = reinterpret_cast(SEGENV.data); @@ -2998,7 +3005,7 @@ uint16_t mode_bouncing_balls(void) { balls[i].lastBounceTime = time; if (balls[i].impactVelocity < 0.015f) { - float impactVelocityStart = sqrtf(-2.0f * gravity) * random8(5,11)/10.0f; // randomize impact velocity + float impactVelocityStart = sqrtf(-2.0f * gravity) * hw_random8(5,11)/10.0f; // randomize impact velocity balls[i].impactVelocity = impactVelocityStart; } } else if (balls[i].height > 1.0f) { @@ -3025,16 +3032,14 @@ uint16_t mode_bouncing_balls(void) { for (unsigned stripNr=0; stripNr(SEGENV.data); @@ -3061,10 +3066,10 @@ static uint16_t rolling_balls(void) { SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); // start clean for (unsigned i = 0; i < maxNumBalls; i++) { balls[i].lastBounceUpdate = strip.now; - balls[i].velocity = 20.0f * float(random16(1000, 10000))/10000.0f; // number from 1 to 10 - if (random8()<128) balls[i].velocity = -balls[i].velocity; // 50% chance of reverse direction - balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. - balls[i].mass = (float(random16(1000, 10000)) / 10000.0f); // from .1 to 1. + balls[i].velocity = 20.0f * float(hw_random16(1000, 10000))/10000.0f; // number from 1 to 10 + if (hw_random8()<128) balls[i].velocity = -balls[i].velocity; // 50% chance of reverse direction + balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f); // from 0. to 1. + balls[i].mass = (float(hw_random16(1000, 10000)) / 10000.0f); // from .1 to 1. } } @@ -3080,7 +3085,7 @@ static uint16_t rolling_balls(void) { float thisHeight = balls[i].height + balls[i].velocity * timeSinceLastUpdate; // this method keeps higher resolution // test if intensity level was increased and some balls are way off the track then put them back if (thisHeight < -0.5f || thisHeight > 1.5f) { - thisHeight = balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. + thisHeight = balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f); // from 0. to 1. balls[i].lastBounceUpdate = strip.now; } // check if reached ends of the strip @@ -3126,17 +3131,210 @@ static uint16_t rolling_balls(void) { balls[i].lastBounceUpdate = strip.now; balls[i].height = thisHeight; } +} +static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar +#endif // WLED_PS_DONT_REPLACE_1D_FX + + +/* +/ Pac-Man by Bob Loeffler with help from @dedehai and @blazoncek +* speed slider is for speed. +* intensity slider is for selecting the number of power dots. +* custom1 slider is for selecting the LED where the ghosts will start blinking blue. +* custom2 slider is for blurring the LEDs in the segment. +* custom3 slider is for selecting the # of ghosts (between 2 and 8). +* check1 is for displaying White Dots that PacMan eats. Enabled will show white dots. Disabled will not show any white dots (all leds will be black). +* check2 is for Smear mode (enabled will smear/persist the LED colors, disabled will not). +* check3 is for the Compact Dots mode of displaying white dots. Enabled will show white dots in every LED. Disabled will show black LEDs between the white dots. +* aux0 is used to keep track of the previous number of power dots in case the user selects a different number with the intensity slider. +* aux1 is the main counter for timing. +*/ +typedef struct PacManChars { + signed pos; + signed topPos; // LED position of farthest PacMan has moved + uint32_t color; + bool direction; // true = moving away from first LED + bool blue; // used for ghosts only + bool eaten; // used for power dots only +} pacmancharacters_t; + +static void mode_pacman(void) { + constexpr unsigned ORANGEYELLOW = 0xFFCC00; + constexpr unsigned PURPLEISH = 0xB000B0; + constexpr unsigned ORANGEISH = 0xFF8800; + constexpr unsigned WHITEISH = 0x999999; + constexpr unsigned PACMAN = 0; // PacMan is character[0] + constexpr uint32_t ghostColors[] = {RED, PURPLEISH, CYAN, ORANGEISH}; + + unsigned maxPowerDots = min(SEGLEN / 10U, 255U); // cap the max so packed state fits in 8 bits + unsigned numPowerDots = map(SEGMENT.intensity, 0, 255, 1, maxPowerDots); + unsigned numGhosts = map(SEGMENT.custom3, 0, 31, 2, 8); + bool smearMode = SEGMENT.check2; + + // Pack two 8-bit values into one 16-bit field (stored in SEGENV.aux0) + uint16_t combined_value = uint16_t(((numPowerDots & 0xFF) << 8) | (numGhosts & 0xFF)); + if (combined_value != SEGENV.aux0) SEGENV.call = 0; // Reinitialize on setting change + SEGENV.aux0 = combined_value; + + // Allocate segment data + unsigned dataSize = sizeof(pacmancharacters_t) * (numGhosts + maxPowerDots + 1); // +1 is the PacMan character + if (SEGLEN <= 16 + (2*numGhosts) || !SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; + pacmancharacters_t *character = reinterpret_cast(SEGENV.data); + + // Calculate when blue ghosts start blinking. + // On first call (or after settings change), `topPos` is not known yet, so fall back to the full segment length in that case. + int maxBlinkPos = (SEGENV.call == 0) ? (int)SEGLEN - 1 : character[PACMAN].topPos; + if (maxBlinkPos < 20) maxBlinkPos = 20; + int startBlinkingGhostsLED = (SEGLEN < 64) + ? (int)SEGLEN / 3 + : map(SEGMENT.custom1, 0, 255, 20, maxBlinkPos); + + // Initialize characters on first call + if (SEGENV.call == 0) { + // Initialize PacMan + character[PACMAN].color = YELLOW; + character[PACMAN].pos = 0; + character[PACMAN].topPos = 0; + character[PACMAN].direction = true; + character[PACMAN].blue = false; + + // Initialize ghosts with alternating colors + for (int i = 1; i <= numGhosts; i++) { + character[i].color = ghostColors[(i-1) % 4]; + character[i].pos = -2 * (i + 1); + character[i].direction = true; + character[i].blue = false; + } + + // Initialize power dots + for (int i = 0; i < numPowerDots; i++) { + character[i + numGhosts + 1].color = ORANGEYELLOW; + character[i + numGhosts + 1].eaten = false; + } + character[numGhosts + 1].pos = SEGLEN - 1; // Last power dot at end + } + + if (strip.now > SEGENV.step) { + SEGENV.step = strip.now; + SEGENV.aux1++; + } + + // Clear background if not in smear mode + if (!smearMode) SEGMENT.fill(BLACK); + + // Draw white dots in front of PacMan if option selected + if (SEGMENT.check1) { + int step = SEGMENT.check3 ? 1 : 2; // Compact or spaced dots + for (int i = SEGLEN - 1; i > character[PACMAN].topPos; i -= step) { + SEGMENT.setPixelColor(i, WHITEISH); + } + } + + // Update power dot positions dynamically + uint32_t everyXLeds = (((uint32_t)SEGLEN - 10U) << 8) / numPowerDots; // Fixed-point spacing for power dots: use 32-bit math to avoid overflow on long segments. + for (int i = 1; i < numPowerDots; i++) { + character[i + numGhosts + 1].pos = 10 + ((i * everyXLeds) >> 8); + } + + // Blink power dots every 10 ticks + if (SEGENV.aux1 % 10 == 0) { + uint32_t dotColor = (character[numGhosts + 1].color == ORANGEYELLOW) ? BLACK : ORANGEYELLOW; + for (int i = 0; i < numPowerDots; i++) { + character[i + numGhosts + 1].color = dotColor; + } + } + + // Blink blue ghosts when nearing start + if (SEGENV.aux1 % 15 == 0 && character[1].blue && character[PACMAN].pos <= startBlinkingGhostsLED) { + uint32_t ghostColor = (character[1].color == BLUE) ? WHITEISH : BLUE; + for (int i = 1; i <= numGhosts; i++) { + character[i].color = ghostColor; + } + } + + // Draw uneaten power dots + for (int i = 0; i < numPowerDots; i++) { + if (!character[i + numGhosts + 1].eaten && (unsigned)character[i + numGhosts + 1].pos < SEGLEN) { + SEGMENT.setPixelColor(character[i + numGhosts + 1].pos, character[i + numGhosts + 1].color); + } + } + + // Check if PacMan ate a power dot + for (int j = 0; j < numPowerDots; j++) { + auto &dot = character[j + numGhosts + 1]; + if (character[PACMAN].pos == dot.pos && !dot.eaten) { + // Reverse all characters - PacMan now chases ghosts + for (int i = 0; i <= numGhosts; i++) { + character[i].direction = false; + } + // Turn ghosts blue + for (int i = 1; i <= numGhosts; i++) { + character[i].color = BLUE; + character[i].blue = true; + } + dot.eaten = true; + break; // only one power dot per frame + } + } + + // Reset when PacMan reaches start with blue ghosts + if (character[1].blue && character[PACMAN].pos <= 0) { + // Reverse direction back + for (int i = 0; i <= numGhosts; i++) { + character[i].direction = true; + } + // Reset ghost colors + for (int i = 1; i <= numGhosts; i++) { + character[i].color = ghostColors[(i-1) % 4]; + character[i].blue = false; + } + // Reset power dots if last one was eaten + if (character[numGhosts + 1].eaten) { + for (int i = 0; i < numPowerDots; i++) { + character[i + numGhosts + 1].eaten = false; + } + character[PACMAN].topPos = 0; // set the top position of PacMan to LED 0 (beginning of the segment) + } + } + + // Update and draw characters based on speed setting + bool updatePositions = (SEGENV.aux1 % map(SEGMENT.speed, 0, 255, 15, 1) == 0); + + // update positions of characters if it's time to do so + if (updatePositions) { + character[PACMAN].pos += character[PACMAN].direction ? 1 : -1; + for (int i = 1; i <= numGhosts; i++) { + character[i].pos += character[i].direction ? 1 : -1; + } + } - return FRAMETIME; + // Draw PacMan + if ((unsigned)character[PACMAN].pos < SEGLEN) { + SEGMENT.setPixelColor(character[PACMAN].pos, character[PACMAN].color); + } + + // Draw ghosts + for (int i = 1; i <= numGhosts; i++) { + if ((unsigned)character[i].pos < SEGLEN) { + SEGMENT.setPixelColor(character[i].pos, character[i].color); + } + } + + // Track farthest position of PacMan + if (character[PACMAN].topPos < character[PACMAN].pos) { + character[PACMAN].topPos = character[PACMAN].pos; + } + + SEGMENT.blur(SEGMENT.custom2>>1); } -static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar +static const char _data_FX_MODE_PACMAN[] PROGMEM = "PacMan@Speed,# of PowerDots,Blink distance,Blur,# of Ghosts,Dots,Smear,Compact;;!;1;m12=0,sx=192,ix=64,c1=64,c2=0,c3=12,o1=1,o2=0"; /* * Sinelon stolen from FASTLED examples */ -static uint16_t sinelon_base(bool dual, bool rainbow=false) { - if (SEGLEN == 1) return mode_static(); +static void sinelon_base(bool dual, bool rainbow=false) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; SEGMENT.fade_out(SEGMENT.intensity); unsigned pos = beatsin16_t(SEGMENT.speed/10,0,SEGLEN-1); if (SEGENV.call == 0) SEGENV.aux0 = pos; @@ -3165,36 +3363,33 @@ static uint16_t sinelon_base(bool dual, bool rainbow=false) { } SEGENV.aux0 = pos; } - - return FRAMETIME; } -uint16_t mode_sinelon(void) { - return sinelon_base(false); +void mode_sinelon(void) { + sinelon_base(false); } static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!"; -uint16_t mode_sinelon_dual(void) { - return sinelon_base(true); +void mode_sinelon_dual(void) { + sinelon_base(true); } static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!"; -uint16_t mode_sinelon_rainbow(void) { - return sinelon_base(false, true); +void mode_sinelon_rainbow(void) { + 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()) SEGMENT.setPixelColor(random16(SEGLEN), col); + if (intensity > hw_random8()) SEGMENT.setPixelColor(hw_random16(SEGLEN), col); } //Glitter with palette background, inspired by https://gist.github.com/kriegsman/062e10f7f07ba8518af6 -uint16_t mode_glitter() +void mode_glitter() { if (!SEGMENT.check2) { // use "* Color 1" palette for solid background (replacing "Solid glitter") unsigned counter = 0; @@ -3211,21 +3406,18 @@ uint16_t mode_glitter() } } glitter_base(SEGMENT.intensity, SEGCOLOR(2) ? SEGCOLOR(2) : ULTRAWHITE); - return FRAMETIME; } -static const char _data_FX_MODE_GLITTER[] PROGMEM = "Glitter@!,!,,,,,Overlay;,,Glitter color;!;;pal=0,m12=0"; //pixels +static const char _data_FX_MODE_GLITTER[] PROGMEM = "Glitter@!,!,,,,,Overlay;,,Glitter color;!;;pal=11,m12=0"; //pixels //Solid colour background with glitter (can be replaced by Glitter) -uint16_t mode_solid_glitter() +void 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 20 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { @@ -3240,14 +3432,14 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ -uint16_t mode_popcorn(void) { - if (SEGLEN == 1) return mode_static(); +void mode_popcorn(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; //allocate segment data unsigned strips = SEGMENT.nrOfVStrips(); unsigned usablePopcorns = maxNumPopcorn; if (usablePopcorns * strips * sizeof(spark) > FAIR_DATA_PER_SEG) usablePopcorns = FAIR_DATA_PER_SEG / (strips * sizeof(spark)) + 1; // at least 1 popcorn per vstrip unsigned dataSize = sizeof(spark) * usablePopcorns; // on a matrix 64x64 this could consume a little less than 27kB when Bar expansion is used - if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed Spark* popcorn = reinterpret_cast(SEGENV.data); @@ -3262,23 +3454,23 @@ uint16_t mode_popcorn(void) { unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255; if (numPopcorn == 0) numPopcorn = 1; - for(unsigned i = 0; i < numPopcorn; i++) { + for (unsigned 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!!! + if (hw_random8() < 2) { // POP!!! popcorn[i].pos = 0.01f; - unsigned peakHeight = 128 + random8(128); //0-255 + unsigned peakHeight = 128 + hw_random8(128); //0-255 peakHeight = (peakHeight * (SEGLEN -1)) >> 8; popcorn[i].vel = sqrtf(-2.0f * gravity * peakHeight); if (SEGMENT.palette) { - popcorn[i].colIndex = random8(); + popcorn[i].colIndex = hw_random8(); } else { - byte col = random8(0, NUM_COLORS); + byte col = hw_random8(0, NUM_COLORS); if (!SEGCOLOR(2) || !SEGCOLOR(col)) col = 0; popcorn[i].colIndex = col; } @@ -3296,23 +3488,26 @@ uint16_t mode_popcorn(void) { for (unsigned stripNr=0; stripNr 1) { //allocate segment data - unsigned dataSize = max(1, SEGLEN -1) *3; //max. 1365 pixels (ESP8266) - if (!SEGENV.allocateData(dataSize)) return candle(false); //allocation failed + unsigned dataSize = sizeof(uint32_t) + max(1, (int)SEGLEN -1) *3; //max. 1365 pixels (ESP8266) + if (!SEGENV.allocateData(dataSize)) candle(false); //allocation failed } + uint32_t* lastcall = reinterpret_cast(SEGENV.data); + uint8_t* candleData = reinterpret_cast(SEGENV.data + sizeof(uint32_t)); + + //limit update rate + if (strip.now - *lastcall < FRAMETIME_FIXED) return; + *lastcall = strip.now; //max. flicker range controlled by intensity unsigned valrange = SEGMENT.intensity; @@ -3337,10 +3532,10 @@ uint16_t candle(bool multi) unsigned s = SEGENV.aux0, s_target = SEGENV.aux1, fadeStep = SEGENV.step; if (i > 0) { d = (i-1) *3; - s = SEGENV.data[d]; s_target = SEGENV.data[d+1]; fadeStep = SEGENV.data[d+2]; + s = candleData[d]; s_target = candleData[d+1]; fadeStep = candleData[d+2]; } if (fadeStep == 0) { //init vals - s = 128; s_target = 130 + random8(4); fadeStep = 1; + s = 128; s_target = 130 + hw_random8(4); fadeStep = 1; } bool newTarget = false; @@ -3353,8 +3548,8 @@ uint16_t candle(bool multi) } if (newTarget) { - s_target = random8(rndval) + random8(rndval); //between 0 and rndval*2 -2 = 252 - if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + random8(rndval); + s_target = hw_random8(rndval) + hw_random8(rndval); //between 0 and rndval*2 -2 = 252 + if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + hw_random8(rndval); unsigned offset = (255 - valrange); s_target += offset; @@ -3365,36 +3560,33 @@ uint16_t candle(bool multi) } if (i > 0) { - 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; + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), uint8_t(s))); + candleData[d] = s; candleData[d+1] = s_target; candleData[d+2] = fadeStep; } else { - 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)); + for (unsigned j = 0; j < SEGLEN; j++) { + SEGMENT.setPixelColor(j, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0), uint8_t(s))); } SEGENV.aux0 = s; SEGENV.aux1 = s_target; SEGENV.step = fadeStep; } } - - return FRAMETIME_FIXED; } -uint16_t mode_candle() +void mode_candle() { - return candle(false); + candle(false); } static const char _data_FX_MODE_CANDLE[] PROGMEM = "Candle@!,!;!,!;!;01;sx=96,ix=224,pal=0"; -uint16_t mode_candle_multi() +void mode_candle_multi() { - return candle(true); + candle(true); } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; - +#ifdef WLED_PS_DONT_REPLACE_1D_FX /* / Fireworks in starburst effect / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ @@ -3415,8 +3607,8 @@ typedef struct particle { float fragment[STARBURST_MAX_FRAG]; } star; -uint16_t mode_starburst(void) { - if (SEGLEN == 1) return mode_static(); +void mode_starburst(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 unsigned segs = strip.getActiveSegmentsNum(); if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs @@ -3427,7 +3619,7 @@ uint16_t mode_starburst(void) { if (numStars > maxStars) numStars = maxStars; unsigned dataSize = sizeof(star) * numStars; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed uint32_t it = strip.now; @@ -3440,19 +3632,19 @@ uint16_t mode_starburst(void) { for (unsigned j = 0; j < numStars; j++) { // speed to adjust chance of a burst, max is nearly always. - if (random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0) + if (hw_random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0) { // Pick a random color and location. - unsigned startPos = random16(SEGLEN-1); - float multiplier = (float)(random8())/255.0f * 1.0f; + unsigned startPos = hw_random16(SEGLEN-1); + float multiplier = (float)(hw_random8())/255.0f * 1.0f; - stars[j].color = CRGB(SEGMENT.color_wheel(random8())); + stars[j].color = CRGB(SEGMENT.color_wheel(hw_random8())); stars[j].pos = startPos; - stars[j].vel = maxSpeed * (float)(random8())/255.0f * multiplier; + stars[j].vel = maxSpeed * (float)(hw_random8())/255.0f * multiplier; stars[j].birth = it; stars[j].last = it; // more fragments means larger burst effect - int num = random8(3,6 + (SEGMENT.intensity >> 5)); + int num = hw_random8(3,6 + (SEGMENT.intensity >> 5)); for (int i=0; i < STARBURST_MAX_FRAG; i++) { if (i < num) stars[j].fragment[i] = startPos; @@ -3488,7 +3680,7 @@ uint16_t mode_starburst(void) { float age = it-stars[j].birth; if (age < particleIgnition) { - c = CRGB(color_blend(WHITE, RGBW32(c.r,c.g,c.b,0), 254.5f*((age / particleIgnition)))); + c = CRGB(color_blend(WHITE, RGBW32(c.r,c.g,c.b,0), uint8_t(254.5f*((age / particleIgnition))))); } else { // Figure out how much to fade and shrink the star based on // its age relative to its lifetime @@ -3499,8 +3691,7 @@ uint16_t mode_starburst(void) { } else { age -= particleIgnition; fade = (age / particleFadeTime); // Fading star - byte f = 254.5f*fade; - c = CRGB(color_blend(RGBW32(c.r,c.g,c.b,0), SEGCOLOR(1), f)); + c = CRGB(color_blend(RGBW32(c.r,c.g,c.b,0), SEGCOLOR(1), uint8_t(254.5f*fade))); } } @@ -3512,33 +3703,33 @@ uint16_t mode_starburst(void) { if (stars[j].fragment[i] > 0) { float loc = stars[j].fragment[i]; if (mirrored) loc -= (loc-stars[j].pos)*2; - int start = loc - particleSize; - int end = loc + particleSize; + unsigned start = loc - particleSize; + unsigned end = loc + particleSize; if (start < 0) start = 0; if (start == end) end++; if (end > SEGLEN) end = SEGLEN; - for (int p = start; p < end; p++) { - SEGMENT.setPixelColor(p, c.r, c.g, c.b); + for (unsigned p = start; p < end; p++) { + SEGMENT.setPixelColor(p, c); } } } } - return FRAMETIME; } #undef STARBURST_MAX_FRAG static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; +#endif // WLED_PS_DONT_REPLACE_1DFX - +#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX) /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ * adapted for 2D WLED by blazoncek (Blaz Kristan (AKA blazoncek)) */ -uint16_t mode_exploding_fireworks(void) +void mode_exploding_fireworks(void) { - if (SEGLEN == 1) return mode_static(); - const int cols = SEGMENT.is2D() ? SEGMENT.virtualWidth() : 1; - const int rows = SEGMENT.is2D() ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + const int cols = SEGMENT.is2D() ? SEG_W : 1; + const int rows = SEGMENT.is2D() ? SEG_H : SEGLEN; //allocate segment data unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 @@ -3549,7 +3740,7 @@ uint16_t mode_exploding_fireworks(void) unsigned numSparks = min(5 + ((rows*cols) >> 1), maxSparks); unsigned dataSize = sizeof(spark) * numSparks; - if (!SEGENV.allocateData(dataSize + sizeof(float))) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize + sizeof(float))) FX_FALLBACK_STATIC; //allocation failed float *dying_gravity = reinterpret_cast(SEGENV.data + dataSize); if (dataSize != SEGENV.aux1) { //reset to flare if sparks were reallocated (it may be good idea to reset segment if bounds change) @@ -3569,11 +3760,11 @@ uint16_t mode_exploding_fireworks(void) if (SEGENV.aux0 < 2) { //FLARE if (SEGENV.aux0 == 0) { //init flare flare->pos = 0; - flare->posX = SEGMENT.is2D() ? random16(2,cols-3) : (SEGMENT.intensity > random8()); // will enable random firing side on 1D - unsigned peakHeight = 75 + random8(180); //0-255 + flare->posX = SEGMENT.is2D() ? hw_random16(2,cols-3) : (SEGMENT.intensity > hw_random8()); // will enable random firing side on 1D + unsigned peakHeight = 75 + hw_random8(180); //0-255 peakHeight = (peakHeight * (rows -1)) >> 8; flare->vel = sqrtf(-2.0f * gravity * peakHeight); - flare->velX = SEGMENT.is2D() ? (random8(9)-4)/64.0f : 0; // no X velocity on 1D + flare->velX = SEGMENT.is2D() ? (hw_random8(9)-4)/64.0f : 0; // no X velocity on 1D flare->col = 255; //brightness SEGENV.aux0 = 1; } @@ -3601,7 +3792,7 @@ uint16_t mode_exploding_fireworks(void) * Explosion happens where the flare ended. * Size is proportional to the height. */ - unsigned nSparks = flare->pos + random8(4); + unsigned nSparks = flare->pos + hw_random8(4); nSparks = std::max(nSparks, 4U); // This is not a standard constrain; numSparks is not guaranteed to be at least 4 nSparks = std::min(nSparks, numSparks); @@ -3610,12 +3801,12 @@ uint16_t mode_exploding_fireworks(void) for (unsigned i = 1; i < nSparks; i++) { sparks[i].pos = flare->pos; sparks[i].posX = flare->posX; - sparks[i].vel = (float(random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1 + sparks[i].vel = (float(hw_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 = SEGMENT.is2D() ? (float(random16(20001)) / 10000.0f) - 1.0f : 0; // from -1 to 1 + sparks[i].velX = SEGMENT.is2D() ? (float(hw_random16(20001)) / 10000.0f) - 1.0f : 0; // from -1 to 1 sparks[i].col = 345;//abs(sparks[i].vel * 750.0); // set colors before scaling velocity to keep them bright //sparks[i].col = constrain(sparks[i].col, 0, 345); - sparks[i].colIndex = random8(); + sparks[i].colIndex = hw_random8(); sparks[i].vel *= flare->pos/rows; // proportional to height sparks[i].velX *= SEGMENT.is2D() ? flare->posX/cols : 0; // proportional to width sparks[i].vel *= -gravity *50; @@ -3637,23 +3828,23 @@ uint16_t mode_exploding_fireworks(void) if (SEGMENT.is2D() && !(sparks[i].posX >= 0 && sparks[i].posX < cols)) continue; unsigned prog = sparks[i].col; uint32_t spColor = (SEGMENT.palette) ? SEGMENT.color_wheel(sparks[i].colIndex) : SEGCOLOR(0); - CRGB c = CRGB::Black; //HeatColor(sparks[i].col); + CRGBW c = BLACK; //HeatColor(sparks[i].col); if (prog > 300) { //fade from white to spark color - c = CRGB(color_blend(spColor, WHITE, (prog - 300)*5)); + c = color_blend(spColor, WHITE, uint8_t((prog - 300)*5)); } else if (prog > 45) { //fade from spark color to black - c = CRGB(color_blend(BLACK, spColor, prog - 45)); + c = color_blend(BLACK, spColor, uint8_t(prog - 45)); unsigned cooling = (300 - prog) >> 5; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } - if (SEGMENT.is2D()) 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); + if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c); + else SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c); } } if (SEGMENT.check3) SEGMENT.blur(16); *dying_gravity *= .8f; // as sparks burn out they fall slower } else { - SEGENV.aux0 = 6 + random8(10); //wait for this many frames + SEGENV.aux0 = 6 + hw_random8(10); //wait for this many frames } } else { SEGENV.aux0--; @@ -3661,25 +3852,23 @@ uint16_t mode_exploding_fireworks(void) SEGENV.aux0 = 0; //back to flare } } - - return FRAMETIME; } #undef MAX_SPARKS -static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side,,,,,,Blur;!,!;!;12;pal=11,ix=128"; - +static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; +#endif // WLED_PS_DONT_REPLACE_x_FX /* * Drip Effect * ported of: https://www.youtube.com/watch?v=sru2fXh4r7k */ -uint16_t mode_drip(void) +void mode_drip(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) FX_FALLBACK_STATIC; //allocate segment data unsigned strips = SEGMENT.nrOfVStrips(); const int maxNumDrops = 4; unsigned dataSize = sizeof(spark) * maxNumDrops; - if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed Spark* drops = reinterpret_cast(SEGENV.data); if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); @@ -3690,7 +3879,7 @@ uint16_t mode_drip(void) unsigned numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 float gravity = -0.0005f - (SEGMENT.speed/50000.0f); - gravity *= max(1, SEGLEN-1); + gravity *= max(1, (int)SEGLEN-1); int sourcedrop = 12; for (unsigned j=0;j255) drops[j].col=255; - SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col)); + SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),uint8_t(drops[j].col))); drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling - if (random8() < drops[j].col/10) { // random drop + if (hw_random8() < drops[j].col/10) { // random drop drops[j].colIndex=2; //fall drops[j].col=255; } @@ -3720,12 +3909,12 @@ uint16_t mode_drip(void) 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 - unsigned 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 + unsigned pos = constrain(unsigned(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),uint8_t(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)); + SEGMENT.setPixelColor(indexToVStrip(0, stripNr), color_blend(SEGCOLOR(0),BLACK,uint8_t(drops[j].col))); } } else { // we hit bottom if (drops[j].colIndex > 2) { // already hit once, so back to forming @@ -3749,12 +3938,9 @@ uint16_t mode_drip(void) for (unsigned 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 @@ -3794,17 +3980,17 @@ uint16_t mode_tetrix(void) { // speed calculation: a single brick should reach bottom of strip in X seconds // if the speed is set to 1 this should take 5s and at 255 it should take 0.25s // as this is dependant on SEGLEN it should be taken into account and the fact that effect runs every FRAMETIME s - int speed = SEGMENT.speed ? SEGMENT.speed : random8(1,255); + int speed = SEGMENT.speed ? SEGMENT.speed : hw_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 + if (!SEGMENT.check1) drop->col = hw_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 + drop->brick = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : hw_random8(1,5)) * (1+(SEGLEN>>6)); // size of brick } if (drop->step == 1) { // forming - if (random8()>>6) { // random drop + if (hw_random8()>>6) { // random drop drop->step = 2; // fall } } @@ -3813,8 +3999,8 @@ uint16_t mode_tetrix(void) { 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); + for (unsigned i = unsigned(drop->pos); i < SEGLEN; i++) { + uint32_t col = i < unsigned(drop->pos)+drop->brick ? SEGMENT.color_from_palette(drop->col, false, false, 0) : SEGCOLOR(1); SEGMENT.setPixelColor(indexToVStrip(i, stripNr), col); } } else { // we hit bottom @@ -3828,7 +4014,7 @@ uint16_t mode_tetrix(void) { drop->brick = 0; // reset brick size (no more growing) if (drop->step > strip.now) { // allow fading of virtual strip - for (int i = 0; i < SEGLEN; i++) SEGMENT.blendPixelColor(indexToVStrip(i, stripNr), SEGCOLOR(1), 25); // 10% blend + for (unsigned 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 @@ -3840,8 +4026,6 @@ uint16_t mode_tetrix(void) { for (unsigned stripNr=0; stripNr 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;!,!;!"; +static const char _data_FX_MODE_PERCENT[] PROGMEM = "Percent@!,% of fill,,,,One color;!,!;!"; /* * Modulates the brightness similar to a heartbeat * (unimplemented?) tries to draw an ECG approximation on a 2D matrix */ -uint16_t mode_heartbeat(void) { +void mode_heartbeat(void) { unsigned bpm = 40 + (SEGMENT.speed >> 3); uint32_t msPerBeat = (60000L / bpm); uint32_t secondBeat = (msPerBeat / 3); @@ -3947,11 +4127,9 @@ uint16_t mode_heartbeat(void) { SEGENV.step = strip.now; } - 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))); + for (unsigned i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), uint8_t(255 - (SEGENV.aux1 >> 8)))); } - - return FRAMETIME; } static const char _data_FX_MODE_HEARTBEAT[] PROGMEM = "Heartbeat@!,!;!,!;!;01;m12=1"; @@ -3981,7 +4159,7 @@ static const char _data_FX_MODE_HEARTBEAT[] PROGMEM = "Heartbeat@!,!;!,!;!;01;m1 // Modified for WLED, based on https://github.com/FastLED/FastLED/blob/master/examples/Pacifica/Pacifica.ino // // Add one layer of waves into the led array -static CRGB pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) +static CRGB pacifica_one_layer(uint16_t i, const CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) { unsigned ci = cistart; unsigned waveangle = ioff; @@ -3993,10 +4171,10 @@ static CRGB pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, u ci += (cs * i); unsigned sindex16 = sin16_t(ci) + 32768; unsigned sindex8 = scale16(sindex16, 240); - return ColorFromPalette(p, sindex8, bri, LINEARBLEND); + return CRGB(ColorFromPalette(p, sindex8, bri, LINEARBLEND)); } -uint16_t mode_pacifica() +void mode_pacifica() { uint32_t nowOld = strip.now; @@ -4041,7 +4219,7 @@ uint16_t mode_pacifica() unsigned basethreshold = beatsin8_t( 9, 55, 65); unsigned wave = beat8( 7 ); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned 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_t(3, 11 * 256, 14 * 256), beatsin8_t(10, 70, 130), 0-beat16(301)); @@ -4068,7 +4246,6 @@ uint16_t mode_pacifica() } strip.now = nowOld; - return FRAMETIME; } static const char _data_FX_MODE_PACIFICA[] PROGMEM = "Pacifica@!,Angle;;!;;pal=51"; @@ -4076,8 +4253,8 @@ static const char _data_FX_MODE_PACIFICA[] PROGMEM = "Pacifica@!,Angle;;!;;pal=5 /* * Mode simulates a gradual sunrise */ -uint16_t mode_sunrise() { - if (SEGLEN == 1) return mode_static(); +void mode_sunrise() { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; //speed 0 - static sun //speed 1 - 60: sunrise time in minutes //speed 60 - 120 : sunset time in minutes - 60; @@ -4104,15 +4281,12 @@ uint16_t mode_sunrise() { if (SEGMENT.speed > 60) stage = 0xFFFF - stage; //sunset } - for (int i = 0; i <= SEGLEN/2; i++) + for (unsigned i = 0; i <= SEGLEN/2; i++) { - //default palette is Fire - uint32_t c = SEGMENT.color_from_palette(0, false, true, 255); //background - + //default palette is Fire unsigned wave = triwave16((i * stage) / SEGLEN); - wave = (wave >> 8) + ((wave * SEGMENT.intensity) >> 15); - + uint32_t c; if (wave > 240) { //clipped, full white sun c = SEGMENT.color_from_palette( 240, false, true, 255); } else { //transition @@ -4121,16 +4295,14 @@ uint16_t mode_sunrise() { 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"; +static const char _data_FX_MODE_SUNRISE[] PROGMEM = "Sunrise@Time [min],Width;;!;;pal=35,sx=60"; /* * Effects by Andrew Tuline */ -static uint16_t phased_base(uint8_t moder) { // We're making sine waves here. By Andrew Tuline. +static void phased_base(uint8_t moder) { // We're making sine waves here. By Andrew Tuline. unsigned allfreq = 16; // Base frequency. float *phase = reinterpret_cast(&SEGENV.step); // Phase change value gets calculated (float fits into unsigned long). @@ -4140,39 +4312,37 @@ static uint16_t phased_base(uint8_t moder) { // We're making si unsigned 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. + for (unsigned i = 0; i < SEGLEN; i++) { + if (moder == 1) modVal = (perlin8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. unsigned 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. unsigned b = cubicwave8(val); // Now we make an 8 bit sinewave. b = (b > cutOff) ? (b - cutOff) : 0; // A ternary operator to cutoff the light. - SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), b)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), uint8_t(b))); index += 256 / SEGLEN; if (SEGLEN > 256) index ++; // Correction for segments longer than 256 LEDs } - - return FRAMETIME; } -uint16_t mode_phased(void) { - return phased_base(0); +void mode_phased(void) { + phased_base(0); } static const char _data_FX_MODE_PHASED[] PROGMEM = "Phased@!,!;!,!;!"; -uint16_t mode_phased_noise(void) { - return phased_base(1); +void mode_phased_noise(void) { + 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. +void mode_twinkleup(void) { // A very short twinkle routine with fade-in and dual controls. By Andrew Tuline. unsigned prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function 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 < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned ranstart = random8(); // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work. unsigned pixBri = sin8_t(ranstart + 16 * strip.now/(256-SEGMENT.speed)); if (random8() > SEGMENT.intensity) pixBri = 0; @@ -4180,18 +4350,17 @@ uint16_t mode_twinkleup(void) { // A very short twinkle routine } random16_set_seed(prevSeed); // restore original seed so other effects can use "random" PRNG - 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 mode_noisepal(void) { // Slow noise palette by Andrew Tuline. +void mode_noisepal(void) { // Slow noise palette by Andrew Tuline. unsigned scale = 15 + (SEGMENT.intensity >> 2); //default was 30 //#define scale 30 unsigned dataSize = sizeof(CRGBPalette16) * 2; //allocate space for 2 Palettes (2 * 16 * 3 = 96 bytes) - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed CRGBPalette16* palettes = reinterpret_cast(SEGENV.data); @@ -4200,26 +4369,21 @@ uint16_t mode_noisepal(void) { // Slow noise { SEGENV.step = strip.now; - unsigned baseI = random8(); - palettes[1] = CRGBPalette16(CHSV(baseI+random8(64), 255, random8(128,255)), CHSV(baseI+128, 255, random8(128,255)), CHSV(baseI+random8(92), 192, random8(128,255)), CHSV(baseI+random8(92), 255, random8(128,255))); + unsigned baseI = hw_random8(); + palettes[1] = CRGBPalette16(CHSV(baseI+hw_random8(64), 255, hw_random8(128,255)), CHSV(baseI+128, 255, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 192, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 255, hw_random8(128,255))); } - CRGB color; - //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] = SEGPALETTE; - for (int i = 0; i < SEGLEN; i++) { - unsigned 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. - SEGMENT.setPixelColor(i, color.red, color.green, color.blue); + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned index = perlin8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. + SEGMENT.setPixelColor(i, ColorFromPalette(palettes[0], index, 255, LINEARBLEND)); // Use my own palette. } SEGENV.aux0 += beatsin8_t(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;;!"; @@ -4227,7 +4391,7 @@ 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 mode_sinewave(void) { // Adjustable sinewave. By Andrew Tuline +void 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 unsigned colorIndex = strip.now /32;//(256 - SEGMENT.fft1); // Amount of colour change. @@ -4235,13 +4399,11 @@ uint16_t mode_sinewave(void) { // Adjustable sinewave. By Andrew Tul SEGENV.step += SEGMENT.speed/16; // Speed of animation. unsigned freq = SEGMENT.intensity/4;//SEGMENT.fft2/8; // Frequency of the signal. - 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: - int pixBri = cubicwave8((i*freq)+SEGENV.step);//qsuba(cubicwave8((i*freq)+SEGENV.step), (255-SEGMENT.intensity)); // qsub sets a minimum value called thiscutoff. If < thiscutoff, then bright = 0. Otherwise, bright = 128 (as defined in qsub).. + for (unsigned i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows: + uint8_t pixBri = cubicwave8((i*freq)+SEGENV.step);//qsuba(cubicwave8((i*freq)+SEGENV.step), (255-SEGMENT.intensity)); // qsub sets a minimum value called thiscutoff. If < thiscutoff, then bright = 0. Otherwise, bright = 128 (as defined in qsub).. //setPixCol(i, i*colorIndex/255, pixBri); SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i*colorIndex/255, false, PALETTE_SOLID_WRAP, 0), pixBri)); } - - return FRAMETIME; } static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine@!,Scale;;!"; @@ -4249,7 +4411,7 @@ static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine@!,Scale;;!"; /* * Best of both worlds from Palette and Spot effects. By Aircoookie */ -uint16_t mode_flow(void) +void mode_flow(void) { unsigned counter = 0; if (SEGMENT.speed != 0) @@ -4259,27 +4421,24 @@ uint16_t mode_flow(void) } unsigned maxZones = SEGLEN / 6; //only looks good if each zone has at least 6 LEDs - unsigned zones = (SEGMENT.intensity * maxZones) >> 8; + int zones = (SEGMENT.intensity * maxZones) >> 8; if (zones & 0x01) zones++; //zones must be even if (zones < 2) zones = 2; - unsigned zoneLen = SEGLEN / zones; - unsigned offset = (SEGLEN - zones * zoneLen) >> 1; - - SEGMENT.fill(SEGMENT.color_from_palette(-counter, false, true, 255)); + int zoneLen = SEGLEN / zones; + zones += 2; //add two extra zones to cover beginning and end of segment (compensate integer truncation) + int offset = ((int)SEGLEN - (zones * zoneLen)) / 2; // center the zones on the segment (can not use bit shift on negative number) - for (unsigned z = 0; z < zones; z++) + for (int z = 0; z < zones; z++) { - unsigned pos = offset + z * zoneLen; - for (unsigned i = 0; i < zoneLen; i++) + int pos = offset + z * zoneLen; + for (int i = 0; i < zoneLen; i++) { unsigned colorIndex = (i * 255 / zoneLen) - counter; - unsigned led = (z & 0x01) ? i : (zoneLen -1) -i; + int led = (z & 0x01) ? i : (zoneLen -1) -i; 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 @@ -4288,9 +4447,9 @@ static const char _data_FX_MODE_FLOW[] PROGMEM = "Flow@!,Zones;;!;;m12=1"; //ver * Dots waving around in a sine/pendulum motion. * Little pixel birds flying in a circle. By Aircoookie */ -uint16_t mode_chunchun(void) +void mode_chunchun(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) FX_FALLBACK_STATIC; SEGMENT.fade_out(254); // add a bit of trail unsigned counter = strip.now * (6 + (SEGMENT.speed >> 4)); unsigned numBirds = 2 + (SEGLEN >> 3); // 2 + 1/8 of a segment @@ -4301,25 +4460,12 @@ uint16_t mode_chunchun(void) counter -= span; unsigned megumin = sin16_t(counter) + 0x8000; unsigned 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, 0U, SEGLEN-1U); - SEGMENT.setPixelColor(bird, c); + SEGMENT.setPixelColor(bird, SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0)); // no palette wrapping } - 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 @@ -4333,6 +4479,17 @@ typedef struct Spotlight { #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif +#ifdef WLED_PS_DONT_REPLACE_1D_FX +//13 bytes +typedef struct Spotlight { + float speed; + uint8_t colorIdx; + int16_t position; + unsigned long lastUpdateTime; + uint8_t width; + uint8_t type; +} spotlight; + /* * Spotlights moving back and forth that cast dancing shadows. * Shine this through tree branches/leaves or other close-up objects that cast @@ -4340,15 +4497,15 @@ typedef struct Spotlight { * * By Steve Pomeroy @xxv */ -uint16_t mode_dancing_shadows(void) +void mode_dancing_shadows(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned 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; unsigned dataSize = sizeof(spotlight) * numSpotlights; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Spotlight* spotlights = reinterpret_cast(SEGENV.data); SEGMENT.fill(BLACK); @@ -4367,21 +4524,21 @@ uint16_t mode_dancing_shadows(void) spotlights[i].lastUpdateTime = time; } - respawn = (spotlights[i].speed > 0.0 && spotlights[i].position > (SEGLEN + 2)) + respawn = (spotlights[i].speed > 0.0 && spotlights[i].position > (int)(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].colorIdx = hw_random8(); + spotlights[i].width = hw_random8(1, 10); - spotlights[i].speed = 1.0/random8(4, 50); + spotlights[i].speed = 1.0/hw_random8(4, 50); if (initialize) { - spotlights[i].position = random16(SEGLEN); - spotlights[i].speed *= random8(2) ? 1.0 : -1.0; + spotlights[i].position = hw_random16(SEGLEN); + spotlights[i].speed *= hw_random8(2) ? 1.0 : -1.0; } else { - if (random8(2)) { + if (hw_random8(2)) { spotlights[i].position = SEGLEN + spotlights[i].width; spotlights[i].speed *= -1.0; }else { @@ -4390,14 +4547,14 @@ uint16_t mode_dancing_shadows(void) } spotlights[i].lastUpdateTime = time; - spotlights[i].type = random8(SPOT_TYPES_COUNT); + spotlights[i].type = hw_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) { + if (start >= 0 && start < (int)SEGLEN) { SEGMENT.blendPixelColor(start, color, 128); } } else { @@ -4452,41 +4609,54 @@ uint16_t mode_dancing_shadows(void) } } } - - return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; - +#endif // WLED_PS_DONT_REPLACE_1D_FX /* Imitates a washing machine, rotating same waves forward, then pause, then backward. By Stefan Seegel */ -uint16_t mode_washing_machine(void) { +void 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++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint8_t col = sin8_t(((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@!,!;;!"; +/* + Image effect + Draws a .gif image from filesystem on the matrix/strip +*/ +void mode_image(void) { + #ifndef WLED_ENABLE_GIF + FX_FALLBACK_STATIC; + #else + renderImageToSegment(SEGMENT); + #endif + // if (status != 0 && status != 254 && status != 255) { + // Serial.print("GIF renderer return: "); + // Serial.println(status); + // } +} +static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,Blur,;;;12;sx=128,ix=0"; + /* Blends random colors across palette Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e */ -uint16_t mode_blends(void) { +void mode_blends(void) { unsigned pixelLen = SEGLEN > UINT8_MAX ? UINT8_MAX : SEGLEN; unsigned 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 + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed uint32_t* pixels = reinterpret_cast(SEGENV.data); - unsigned blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); + uint8_t blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); unsigned shift = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8; for (unsigned i = 0; i < pixelLen; i++) { @@ -4497,10 +4667,8 @@ uint16_t mode_blends(void) { unsigned offset = 0; for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, pixels[offset++]); - if (offset > pixelLen) offset = 0; + if (offset >= pixelLen) offset = 0; } - - return FRAMETIME; } static const char _data_FX_MODE_BLENDS[] PROGMEM = "Blends@Shift speed,Blend speed;;!"; @@ -4530,11 +4698,11 @@ typedef struct TvSim { uint16_t pb = 0; } tvSim; -uint16_t mode_tv_simulator(void) { +void mode_tv_simulator(void) { int nr, ng, nb, r, g, b, i, hue; uint8_t sat, bri, j; - if (!SEGENV.allocateData(sizeof(tvSim))) return mode_static(); //allocation failed + if (!SEGENV.allocateData(sizeof(tvSim))) FX_FALLBACK_STATIC; //allocation failed TvSim* tvSimulator = reinterpret_cast(SEGENV.data); uint8_t colorSpeed = map(SEGMENT.speed, 0, UINT8_MAX, 1, 20); @@ -4549,10 +4717,10 @@ uint16_t mode_tv_simulator(void) { // create a new sceene if (((strip.now - tvSimulator->sceeneStart) >= tvSimulator->sceeneDuration) || SEGENV.aux1 == 0) { tvSimulator->sceeneStart = strip.now; // 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 + tvSimulator->sceeneDuration = hw_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 = hw_random16( 0, 768); // random start color-tone for the sceene + tvSimulator->sceeneColorSat = hw_random8 ( 100, 130 + colorIntensity); // random start color-saturation for the sceene + tvSimulator->sceeneColorBri = hw_random8 ( 200, 240); // random start color-brightness for the sceene SEGENV.aux1 = 1; SEGENV.aux0 = 0; } @@ -4560,16 +4728,16 @@ uint16_t mode_tv_simulator(void) { // 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 = hw_random8(4 * colorIntensity); + hue = (hw_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); + j = hw_random8(2 * colorIntensity); sat = (tvSimulator->sceeneColorSat - j) < 0 ? 0 : tvSimulator->sceeneColorSat - j; // brightness - j = random8(100); + j = hw_random8(100); bri = (tvSimulator->sceeneColorBri - j) < 0 ? 0 : tvSimulator->sceeneColorBri - j; // calculate R,G,B from HSV @@ -4595,9 +4763,9 @@ uint16_t mode_tv_simulator(void) { 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->totalTime = hw_random16(250, 2500); // Semi-random pixel-to-pixel time + tvSimulator->fadeTime = hw_random16(0, tvSimulator->totalTime); // Pixel-to-pixel transition time + if (hw_random8(10) < 3) tvSimulator->fadeTime = 0; // Force scene cut 30% of time tvSimulator->startTime = strip.now; } // end of initialization @@ -4617,7 +4785,7 @@ uint16_t mode_tv_simulator(void) { } // set strip color - for (i = 0; i < SEGLEN; i++) { + for (i = 0; i < (int)SEGLEN; i++) { SEGMENT.setPixelColor(i, r >> 8, g >> 8, b >> 8); // Quantize to 8-bit } @@ -4628,14 +4796,13 @@ uint16_t mode_tv_simulator(void) { tvSimulator->pb = nb; SEGENV.aux0 = 0; } - - return FRAMETIME; } static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01"; /* - Aurora effect + Aurora effect by @Mazen + improved and converted to integer math by @dedehai */ //CONFIG @@ -4647,153 +4814,137 @@ static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01 #define W_MAX_SPEED 6 //Higher number, higher speed #define W_WIDTH_FACTOR 6 //Higher number, smaller waves -//24 bytes +// fixed-point math scaling +#define AW_SHIFT 16 +#define AW_SCALE (1 << AW_SHIFT) // 65536 representing 1.0 + +// 32 bytes class AuroraWave { private: + int32_t center; // scaled by AW_SCALE + uint32_t ageFactor_cached; // cached age factor scaled by AW_SCALE uint16_t ttl; - CRGB basecolor; - float basealpha; uint16_t age; uint16_t width; - float center; + uint16_t basealpha; // scaled by AW_SCALE + uint16_t speed_factor; // scaled by AW_SCALE + int16_t wave_start; // wave start LED index + int16_t wave_end; // wave end LED index bool goingleft; - float speed_factor; bool alive = true; + CRGBW basecolor; public: - void init(uint32_t segment_length, CRGB color) { - ttl = random16(500, 1501); + void init(uint32_t segment_length, CRGBW color) { + ttl = hw_random16(500, 1501); basecolor = color; - basealpha = random8(60, 101) / (float)100; + basealpha = hw_random8(60, 100) * AW_SCALE / 100; // 0-99% note: if using 100% there is risk of integer overflow age = 0; - width = random16(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier - if (!width) width = 1; - center = random8(101) / (float)100 * segment_length; - goingleft = random8(0, 2) == 0; - speed_factor = (random8(10, 31) / (float)100 * W_MAX_SPEED / 255); + width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR) + 1; + center = (((uint32_t)hw_random8(101) << AW_SHIFT) / 100) * segment_length; // 0-100% + goingleft = hw_random8() & 0x01; // 50/50 chance + speed_factor = (((uint32_t)hw_random8(10, 31) * W_MAX_SPEED) << AW_SHIFT) / (100 * 255); alive = true; } - CRGB getColorForLED(int ledIndex) { - if(ledIndex < center - width || ledIndex > center + width) return 0; //Position out of range of this wave - - CRGB rgb; - - //Offset of this led from center of wave - //The further away from the center, the dimmer the LED - float offset = ledIndex - center; - if (offset < 0) offset = -offset; - float offsetFactor = offset / width; - - //The age of the wave determines it brightness. - //At half its maximum age it will be the brightest. - float ageFactor = 0.1; - if((float)age / ttl < 0.5) { - ageFactor = (float)age / (ttl / 2); + void updateCachedValues() { + uint32_t half_ttl = ttl >> 1; + if (age < half_ttl) { + ageFactor_cached = ((uint32_t)age << AW_SHIFT) / half_ttl; } else { - ageFactor = (float)(ttl - age) / ((float)ttl * 0.5); + ageFactor_cached = ((uint32_t)(ttl - age) << AW_SHIFT) / half_ttl; } + if (ageFactor_cached >= AW_SCALE) ageFactor_cached = AW_SCALE - 1; // prevent overflow + + uint32_t center_led = center >> AW_SHIFT; + wave_start = (int16_t)center_led - (int16_t)width; + wave_end = (int16_t)center_led + (int16_t)width; + } - //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; + CRGBW getColorForLED(int ledIndex) { + // linear brightness falloff from center to edge of wave + if (ledIndex < wave_start || ledIndex > wave_end) return 0; + int32_t ledIndex_scaled = (int32_t)ledIndex << AW_SHIFT; + int32_t offset = ledIndex_scaled - center; + if (offset < 0) offset = -offset; + uint32_t offsetFactor = offset / width; // scaled by AW_SCALE + if (offsetFactor > AW_SCALE) return 0; // outside of wave + uint32_t brightness_factor = (AW_SCALE - offsetFactor); + brightness_factor = (brightness_factor * ageFactor_cached) >> AW_SHIFT; + brightness_factor = (brightness_factor * basealpha) >> AW_SHIFT; + + CRGBW rgb; + rgb.r = (basecolor.r * brightness_factor) >> AW_SHIFT; + rgb.g = (basecolor.g * brightness_factor) >> AW_SHIFT; + rgb.b = (basecolor.b * brightness_factor) >> AW_SHIFT; + rgb.w = (basecolor.w * brightness_factor) >> AW_SHIFT; return rgb; }; //Change position and age of wave - //Determine if its sill "alive" + //Determine if its still "alive" void update(uint32_t segment_length, uint32_t speed) { - if(goingleft) { - center -= speed_factor * speed; - } else { - center += speed_factor * speed; - } - + int32_t step = speed_factor * speed; + center += goingleft ? -step : step; age++; - if(age > ttl) { + if (age > ttl) { alive = false; } else { - if(goingleft) { - if(center + width < 0) { - alive = false; - } - } else { - if(center - width > segment_length) { - alive = false; - } - } + uint32_t width_scaled = (uint32_t)width << AW_SHIFT; + uint32_t segment_length_scaled = segment_length << AW_SHIFT; + + if (goingleft) { + if (center < - (int32_t)width_scaled) { + alive = false; + } + } else { + if (center > (int32_t)segment_length_scaled + (int32_t)width_scaled) { + alive = false; + } + } } }; - bool stillAlive() { - return alive; - }; + bool stillAlive() { return alive; } }; -uint16_t mode_aurora(void) { - //aux1 = Wavecount - //aux2 = Intensity in last loop - +void mode_aurora(void) { 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, random8(0, 3)))); - } - } else { - waves = reinterpret_cast(SEGENV.data); + SEGENV.aux1 = map(SEGMENT.intensity, 0, 255, 2, W_MAX_COUNT); // aux1 = Wavecount + if (!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) { + FX_FALLBACK_STATIC; } + waves = reinterpret_cast(SEGENV.data); + // note: on first call, SEGENV.data is zero -> all waves are dead and will be initialized for (int i = 0; i < SEGENV.aux1; i++) { - //Update values of wave waves[i].update(SEGLEN, SEGMENT.speed); - - if(!(waves[i].stillAlive())) { - //If a wave dies, reinitialize it starts over. - waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random8(0, 3)))); + if (!(waves[i].stillAlive())) { + waves[i].init(SEGLEN, SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3))); } + waves[i].updateCachedValues(); } - uint8_t backlight = 1; //dimmer backlight if less active colors + uint8_t backlight = 0; // note: original code used 1, with inverse gamma applied background would never be black if (SEGCOLOR(0)) backlight++; if (SEGCOLOR(1)) backlight++; if (SEGCOLOR(2)) backlight++; - //Loop through LEDs to determine color - for (int i = 0; i < SEGLEN; i++) { - CRGB mixedRgb = CRGB(backlight, backlight, backlight); + backlight = gamma8inv(backlight); // preserve backlight when using gamma correction - //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); + for (unsigned i = 0; i < SEGLEN; i++) { + CRGBW mixedRgb = CRGBW(backlight, backlight, backlight); - if(rgb != CRGB(0)) { - mixedRgb += rgb; - } + for (int j = 0; j < SEGENV.aux1; j++) { + CRGBW rgb = waves[j].getColorForLED(i); + mixedRgb = color_add(mixedRgb, rgb); // sum all waves influencing this pixel } - SEGMENT.setPixelColor(i, mixedRgb[0], mixedRgb[1], mixedRgb[2]); + SEGMENT.setPixelColor(i, mixedRgb); } - - return FRAMETIME; } + static const char _data_FX_MODE_AURORA[] PROGMEM = "Aurora@!,!;1,2,3;!;;sx=24,pal=50"; // WLED-SR effects @@ -4803,16 +4954,14 @@ static const char _data_FX_MODE_AURORA[] PROGMEM = "Aurora@!,!;1,2,3;!;;sx=24,pa ///////////////////////// // 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(); +void mode_perlinmove(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; SEGMENT.fade_out(255-SEGMENT.custom1); for (int i = 0; i < SEGMENT.intensity/16 + 1; i++) { - unsigned locn = inoise16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. + unsigned locn = perlin16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. unsigned 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;!,!;!"; @@ -4821,16 +4970,14 @@ static const char _data_FX_MODE_PERLINMOVE[] PROGMEM = "Perlin Move@!,# of pixel // Waveins // ///////////////////////// // Uses beatsin8() + phase shifting. By: Andrew Tuline -uint16_t mode_wavesins(void) { +void mode_wavesins(void) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint8_t bri = sin8_t(strip.now/4 + i * SEGMENT.intensity); uint8_t index = beatsin8_t(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;!;!"; @@ -4838,38 +4985,105 @@ static const char _data_FX_MODE_WAVESINS[] PROGMEM = "Wavesins@!,Brightness vari ////////////////////////////// // Flow Stripe // ////////////////////////////// -// By: ldirko https://editor.soulmatelights.com/gallery/392-flow-led-stripe , modifed by: Andrew Tuline -uint16_t mode_FlowStripe(void) { - if (SEGLEN == 1) return mode_static(); +// By: ldirko https://editor.soulmatelights.com/gallery/392-flow-led-stripe , modifed by: Andrew Tuline, fixed by @DedeHai +void mode_FlowStripe(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; const int hl = SEGLEN * 10 / 13; uint8_t hue = strip.now / (SEGMENT.speed+1); uint32_t t = strip.now / (SEGMENT.intensity/8+1); - for (int i = 0; i < SEGLEN; i++) { - int c = (abs(i - hl) / hl) * 127; + for (unsigned i = 0; i < SEGLEN; i++) { + int c = ((abs((int)i - hl) * 127) / hl); c = sin8_t(c); c = sin8_t(c / 2 + t); byte b = sin8_t(c + t/8); - SEGMENT.setPixelColor(i, CHSV(b + hue, 255, 255)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(b + hue, false, true, 3)); } - - return FRAMETIME; } // mode_FlowStripe() -static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Effect speed;;"; +static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Effect speed;;!;pal=11"; + +/* + Shimmer effect: moves a gradient with optional modulators across the strip at a given interval, up to 60 seconds + It can be used as an overlay to other effects or standalone + by DedeHai (Damian Schneider), based on idea from @Charming-Lime (#4905) +*/ +void mode_shimmer() { + if(!SEGENV.allocateData(sizeof(uint32_t))) { FX_FALLBACK_STATIC; } + uint32_t* lastTime = reinterpret_cast(SEGENV.data); + + uint32_t radius = (SEGMENT.custom1 * SEGLEN >> 7) + 1; // [1, 2*SEGLEN+1] pixels + uint32_t traversalDistance = (SEGLEN + 2 * radius) << 8; // total subpixels to cross, 1 pixel = 256 subpixels + uint32_t traversalTime = 200 + (255 - SEGMENT.speed) * 80; // [200, 20600] ms + uint32_t speed = ((traversalDistance << 5) / traversalTime); // subpixels/512ms + int32_t position = static_cast(SEGENV.step); // current position in subpixels + uint16_t inputstate = (uint16_t(SEGMENT.intensity) << 8) | uint16_t(SEGMENT.custom1); // current user input state + + // init + if (SEGENV.call == 0 || inputstate != SEGENV.aux1) { + position = -(radius << 8); + SEGENV.aux0 = 0; // aux0 is pause timer + *lastTime = strip.now; + SEGENV.aux1 = inputstate; // save user input state + } + + if(SEGMENT.speed) { + uint32_t deltaTime = (strip.now - *lastTime) & 0x7F; // clamp to 127ms to avoid overflows. note: speed*deltaTime can still overflow for segments > ~10k pixels + *lastTime = strip.now; + + if (SEGENV.aux0 > 0) { + SEGENV.aux0 = (SEGENV.aux0 > deltaTime) ? SEGENV.aux0 - deltaTime : 0; + } else { + // calculate movement step and update position + int32_t step = 1 + ((speed * deltaTime) >> 5); // subpixels moved this frame. note >>5 as speed is in subpixels/512ms + position += step; + int endposition = (SEGLEN + radius) << 8; + if (position > endposition) { + SEGENV.aux0 = SEGMENT.intensity * 236; // [0, 60180] ms pause + if(SEGMENT.check3) SEGENV.aux0 = hw_random(SEGENV.aux0 + 1000); // randomise interval, +1 second to affect low intensity values + position = -(radius << 8); // reset to start position (out of frame) + } + SEGENV.step = (uint32_t)position; // save back + } + + if (SEGMENT.check2) + position = (SEGLEN << 8) - position; // invert position (and direction) + } else { + position = (SEGLEN << 7); // at speed=0, make it static in the center (this enables to use modulators only) + } + for (int i = 0; i < SEGLEN; i++) { + uint32_t dist = abs(position - (i << 8)); + if (dist < (radius << 8)) { + uint32_t color = SEGMENT.color_from_palette(i * 255 / SEGLEN, false, false, 0); + uint8_t blend = dist / radius; // linear gradient note: dist is in subpixels, radius in pixels, result is [0, 255] since dist < radius*256 + if (SEGMENT.custom2) { + uint8_t modVal; // modulation value + if (SEGMENT.check1) { + modVal = (sin16_t((i * SEGMENT.custom2 << 6) + (strip.now * SEGMENT.custom3 << 5)) >> 8) + 128; // sine modulation: regular "Zebra" stripes + } else { + modVal = perlin16((i * SEGMENT.custom2 << 7), strip.now * SEGMENT.custom3 << 5) >> 8; // perlin noise modulation + } + color = color_fade(color, modVal, true); // dim by modulator value + } + SEGMENT.setPixelColor(i, color_blend(color, SEGCOLOR(1), blend)); // blend to background color + } else { + SEGMENT.setPixelColor(i, SEGCOLOR(1)); + } + } +} +static const char _data_FX_MODE_SHIMMER[] PROGMEM = "Shimmer@Speed,Interval,Size,Granular,Flow,Zebra,Reverse,Sporadic;Fx,Bg,Cx;!;1;pal=15,sx=220,ix=10,c2=0,c3=0"; #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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DBlackHole(void) { // By: Stepko https://editor.soulmatelights.com/gallery/1012 , Modified by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; int x, y; SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails @@ -4890,8 +5104,6 @@ uint16_t mode_2DBlackHole(void) { // By: Stepko https://editor.soulma SEGMENT.setPixelColorXY(cols/2, rows/2, WHITE); // blur everything a bit if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100); - - 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,,Blur;!;!;2;pal=11"; @@ -4899,11 +5111,11 @@ static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Ou //////////////////////////// // 2D Colored Bursts // //////////////////////////// -uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.soulmatelights.com/gallery/819-colored-bursts , modified by: Andrew Tuline - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DColoredBursts() { // By: ldirko https://editor.soulmatelights.com/gallery/819-colored-bursts , modified by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGENV.aux0 = 0; // start with red hue @@ -4915,13 +5127,13 @@ uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.so byte numLines = SEGMENT.intensity/16 + 1; SEGENV.aux0++; // hue - SEGMENT.fadeToBlackBy(40); + SEGMENT.fadeToBlackBy(40 - SEGMENT.check2 * 8); for (size_t i = 0; i < numLines; i++) { byte x1 = beatsin8_t(2 + SEGMENT.speed/16, 0, (cols - 1)); byte x2 = beatsin8_t(1 + SEGMENT.speed/16, 0, (rows - 1)); byte y1 = beatsin8_t(5 + SEGMENT.speed/16, 0, (cols - 1), 0, i * 24); byte y2 = beatsin8_t(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64); - CRGB color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); + uint32_t color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); byte xsteps = abs8(x1 - y1) + 1; byte ysteps = abs8(x2 - y2) + 1; @@ -4941,42 +5153,37 @@ uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.so SEGMENT.setPixelColorXY(y1, y2, DARKSLATEGRAY); } } - if (SEGMENT.custom3) SEGMENT.blur(SEGMENT.custom3/2); - - return FRAMETIME; + SEGMENT.blur(SEGMENT.custom3>>1, SEGMENT.check2); } // mode_2DColoredBursts() -static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Speed,# of lines,,,Blur,Gradient,,Dots;;!;2;c3=16"; +static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Speed,# of lines,,,Blur,Gradient,Smear,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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Ddna(void) { // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline. + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(64); for (int i = 0; i < cols; i++) { SEGMENT.setPixelColorXY(i, beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, i*4 ), ColorFromPalette(SEGPALETTE, i*5+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10), LINEARBLEND)); SEGMENT.setPixelColorXY(i, beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, i*4+128), ColorFromPalette(SEGPALETTE, i*5+128+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10+128), LINEARBLEND)); } - SEGMENT.blur(SEGMENT.intensity>>3); - - return FRAMETIME; + SEGMENT.blur(SEGMENT.intensity / (8 - (SEGMENT.check1 * 2)), SEGMENT.check1); } // mode_2Ddna() -static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur;;!;2"; - +static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur,,,,Smear;;!;2;ix=0"; ///////////////////////// // 2D DNA Spiral // ///////////////////////// -uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulmatelights.com/gallery/512-dna-spiral-variation , modified by: Andrew Tuline - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DDNASpiral() { // By: ldirko https://editor.soulmatelights.com/gallery/512-dna-spiral-variation , modified by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5010,20 +5217,19 @@ uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulma SEGMENT.setPixelColorXY(x1, i, WHITE); } } - - return FRAMETIME; + SEGMENT.blur(((uint16_t)SEGMENT.custom1 * 3) / (6 + SEGMENT.check1), SEGMENT.check1); } // mode_2DDNASpiral() -static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed,Y frequency;;!;2"; +static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed,Y frequency,Blur,,,Smear;;!;2;c1=0"; ///////////////////////// // 2D Drift // ///////////////////////// -uint16_t mode_2DDrift() { // By: Stepko https://editor.soulmatelights.com/gallery/884-drift , Modified by: Andrew Tuline - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DDrift() { // By: Stepko https://editor.soulmatelights.com/gallery/884-drift , Modified by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const int colsCenter = (cols>>1) + (cols%2); const int rowsCenter = (rows>>1) + (rows%2); @@ -5039,21 +5245,19 @@ uint16_t mode_2DDrift() { // By: Stepko https://editor.soulmateli SEGMENT.setPixelColorXY(colsCenter + mySin, rowsCenter + myCos, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); if (SEGMENT.check1) SEGMENT.setPixelColorXY(colsCenter + myCos, rowsCenter + mySin, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); } - SEGMENT.blur(SEGMENT.intensity>>3); - - return FRAMETIME; + SEGMENT.blur(SEGMENT.intensity>>(3 - SEGMENT.check2), SEGMENT.check2); } // mode_2DDrift() -static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur amount,,,,Twin;;!;2"; +static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur,,,,Twin,Smear;;!;2;ix=0"; ////////////////////////// // 2D Firenoise // ////////////////////////// -uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline. Yet another short routine. - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline. Yet another short routine. + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5063,19 +5267,17 @@ uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline unsigned yscale = SEGMENT.speed*8; unsigned indexx = 0; + //CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : SEGMENT.loadPalette(pal, 35); CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : CRGBPalette16(CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); - for (int j=0; j < cols; j++) { for (int i=0; i < rows; i++) { - indexx = inoise8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. - SEGMENT.setPixelColorXY(j, i, ColorFromPalette(pal, min(i*(indexx)>>4, 255U), i*255/cols, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. + indexx = perlin8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. + SEGMENT.setPixelColorXY(j, i, ColorFromPalette(pal, min(i*indexx/11, 225U), i*255/rows, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. } // for i } // for j - - return FRAMETIME; } // mode_2Dfirenoise() static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y scale,,,,Palette;;!;2;pal=66"; @@ -5083,144 +5285,190 @@ static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y sca ////////////////////////////// // 2D Frizzles // ////////////////////////////// -uint16_t mode_2DFrizzles(void) { // By: Stepko https://editor.soulmatelights.com/gallery/640-color-frizzles , Modified by: Andrew Tuline - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DFrizzles(void) { // By: Stepko https://editor.soulmatelights.com/gallery/640-color-frizzles , Modified by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; - SEGMENT.fadeToBlackBy(16); + SEGMENT.fadeToBlackBy(16 + SEGMENT.check1 * 10); for (size_t i = 8; i > 0; i--) { SEGMENT.addPixelColorXY(beatsin8_t(SEGMENT.speed/8 + i, 0, cols - 1), beatsin8_t(SEGMENT.intensity/8 - i, 0, rows - 1), ColorFromPalette(SEGPALETTE, beatsin8_t(12, 0, 255), 255, LINEARBLEND)); } - SEGMENT.blur(SEGMENT.custom1>>3); - - return FRAMETIME; + SEGMENT.blur(SEGMENT.custom1 >> (3 + SEGMENT.check1), SEGMENT.check1); } // mode_2DFrizzles() -static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y frequency,Blur;;!;2"; +static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y frequency,Blur,,,Smear;;!;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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); - const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled - const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) - - if (!SEGENV.allocateData(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++) { - unsigned 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)); +typedef struct Cell { + uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2; +} Cell; + +void mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ + // and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up + const int cols = SEG_W, rows = SEG_H; + const unsigned maxIndex = cols * rows; + + if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) FX_FALLBACK_STATIC; // allocation failed + + Cell *cells = reinterpret_cast (SEGENV.data); + + uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity + bool mutate = SEGMENT.check3; + uint8_t blur = map(SEGMENT.custom1, 0, 255, 255, 4); + + uint32_t bgColor = SEGCOLOR(1); + uint32_t birthColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 255); + + bool setup = SEGENV.call == 0; + if (setup) { + // Calculate glider length LCM(rows,cols)*4 once + unsigned a = rows, b = cols; + while (b) { unsigned t = b; b = a % b; a = t; } + gliderLength = (cols * rows / a) << 2; + } + + if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix + bool paused = SEGENV.step > strip.now; + + // Setup New Game of Life + if ((!paused && generation == 0) || setup) { + SEGENV.step = strip.now + 1280; // show initial state for 1.28 seconds + generation = 1; + paused = true; + //Setup Grid + memset(cells, 0, maxIndex * sizeof(Cell)); + + for (unsigned i = 0; i < maxIndex; i++) { + bool isAlive = !hw_random8(3); // ~33% + cells[i].alive = isAlive; + cells[i].faded = !isAlive; + unsigned x = i % cols, y = i / cols; + cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1); + + SEGMENT.setPixelColor(i, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor); + } + } + + if (paused || (strip.now - SEGENV.step < 1000 / map(SEGMENT.speed,0,255,1,42))) { + // Redraw if paused or between updates to remove blur + for (unsigned i = maxIndex; i--; ) { + if (!cells[i].alive) { + uint32_t cellColor = SEGMENT.getPixelColor(i); + if (cellColor != bgColor) { + uint32_t newColor; + bool needsColor = false; + if (cells[i].faded) { newColor = bgColor; needsColor = true; } + else { + uint32_t blended = color_blend(cellColor, bgColor, 2); + if (blended == cellColor) { blended = bgColor; cells[i].faded = 1; } + newColor = blended; needsColor = true; + } + if (needsColor) SEGMENT.setPixelColor(i, newColor); + } + } } - - 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++) { + // Repeat detection + bool updateOscillator = generation % 16 == 0; + bool updateSpaceship = gliderLength && generation % gliderLength == 0; + bool repeatingOscillator = true, repeatingSpaceship = true, emptyGrid = true; - 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 + unsigned cIndex = maxIndex-1; + for (unsigned y = rows; y--; ) for (unsigned x = cols; x--; cIndex--) { + Cell& cell = cells[cIndex]; - // iterate through neighbors and count them and their different colors - int neighbors = 0; - for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix - if (i==0 && j==0) continue; // ignore itself - // wrap around segment - int xx = x+i, yy = y+j; - if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0; - if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0; + if (cell.alive) emptyGrid = false; + if (cell.oscillatorCheck != cell.alive) repeatingOscillator = false; + if (cell.spaceshipCheck != cell.alive) repeatingSpaceship = false; + if (updateOscillator) cell.oscillatorCheck = cell.alive; + if (updateSpaceship) cell.spaceshipCheck = cell.alive; - unsigned xy = XY(xx, yy); // previous cell xy to check - // count different neighbours and colors - if (prevLeds[xy] != backgroundColor) { + unsigned neighbors = 0, aliveParents = 0, parentIdx[3]; + // Count alive neighbors + for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) if (i || j) { + int nX = x + j, nY = y + i; + if (cell.edgeCell) { + nX = (nX + cols) % cols; + nY = (nY + rows) % rows; + } + unsigned nIndex = nX + nY * cols; + Cell& neighbor = cells[nIndex]; + if (neighbor.alive) { neighbors++; - 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 + if (!neighbor.toggleStatus && neighbors < 4) { // Alive and not dying + parentIdx[aliveParents++] = nIndex; + } + } + } + + uint32_t newColor; + bool needsColor = false; + + if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation + cell.toggleStatus = 1; + if (blur == 255) cell.faded = 1; + newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColor(cIndex), bgColor, blur); + needsColor = true; + } + else if (!cell.alive) { + byte mutationRoll = mutate ? hw_random8(128) : 1; // if 0: 3 neighbor births fail and 2 neighbor births mutate + if ((neighbors == 3 && mutationRoll) || (mutate && neighbors == 2 && !mutationRoll)) { // Reproduction or Mutation + cell.toggleStatus = 1; + cell.faded = 0; + + if (aliveParents) { + // Set color based on random neighbor + unsigned parentIndex = parentIdx[random8(aliveParents)]; + birthColor = SEGMENT.getPixelColor(parentIndex); + } + newColor = birthColor; + needsColor = true; } - } // 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++) { @@ -5228,8 +5476,6 @@ uint16_t mode_2DHiphotic() { // By: ldirko https://edit SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(sin8_t(cos8_t(x * SEGMENT.speed/16 + a / 3) + sin8_t(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"; @@ -5248,13 +5494,13 @@ typedef struct Julia { float xymag; } julia; -uint16_t mode_2DJulia(void) { // An animated Julia set by Andrew Tuline. - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DJulia(void) { // An animated Julia set by Andrew Tuline. + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; - if (!SEGENV.allocateData(sizeof(julia))) return mode_static(); + if (!SEGENV.allocateData(sizeof(julia))) FX_FALLBACK_STATIC; Julia* julias = reinterpret_cast(SEGENV.data); float reAl; @@ -5344,21 +5590,20 @@ uint16_t mode_2DJulia(void) { // An animated Julia set } y += dy; } -// SEGMENT.blur(64); - - return FRAMETIME; + if(SEGMENT.check1) + SEGMENT.blur(100, true); } // 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"; +static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per pixel,X center,Y center,Area size, Blur;!;!;2;ix=24,c1=128,c2=128,c3=16"; ////////////////////////////// // 2D Lissajous // ////////////////////////////// -uint16_t mode_2DLissajous(void) { // By: Andrew Tuline - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DLissajous(void) { // By: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(SEGMENT.intensity); uint_fast16_t phase = (strip.now * (1 + SEGENV.custom3)) /32; // allow user to control rotation speed @@ -5373,23 +5618,23 @@ uint16_t mode_2DLissajous(void) { // By: Andrew Tuline 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(strip.now/100+i, false, PALETTE_SOLID_WRAP, 0)); } - - return FRAMETIME; + SEGMENT.blur(SEGMENT.custom1 >> (1 + SEGMENT.check1 * 3), SEGMENT.check1); } // mode_2DLissajous() -static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate,,,Speed;!;!;2;c3=15"; +static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate,Blur,,Speed,Smear;!;!;2;c1=0"; /////////////////////// // 2D Matrix // /////////////////////// -uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi, and softhack007. - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi, and softhack007. + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; unsigned dataSize = (SEGMENT.length()+7) >> 3; //1 bit per LED for trails - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5434,8 +5679,8 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. } // spawn new falling code - if (random8() <= SEGMENT.intensity || emptyScreen) { - uint8_t spawnX = random8(cols); + if (hw_random8() <= SEGMENT.intensity || emptyScreen) { + uint8_t spawnX = hw_random8(cols); SEGMENT.setPixelColorXY(spawnX, 0, spawnColor); // update hint for next run unsigned index = XY(spawnX, 0) >> 3; @@ -5443,8 +5688,6 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. bitSet(SEGENV.data[index], bitNum); } } - - return FRAMETIME; } // mode_2Dmatrix() static const char _data_FX_MODE_2DMATRIX[] PROGMEM = "Matrix@!,Spawning rate,Trail,,,Custom color;Spawn,Trail;;2"; @@ -5452,20 +5695,20 @@ static const char _data_FX_MODE_2DMATRIX[] PROGMEM = "Matrix@!,Spawning rate,Tra ///////////////////////// // 2D Metaballs // ///////////////////////// -uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have one of the dimensions be 2 or less. Adapted by Andrew Tuline. - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have one of the dimensions be 2 or less. Adapted by Andrew Tuline. + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; float speed = 0.25f * (1+(SEGMENT.speed>>6)); // get some 2 random moving points - int x2 = map(inoise8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); - int y2 = map(inoise8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); + int x2 = map(perlin8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); + int y2 = map(perlin8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); - int x3 = map(inoise8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); - int y3 = map(inoise8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); + int x3 = map(perlin8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); + int y3 = map(perlin8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); // and one Lissajou function int x1 = beatsin8_t(23 * speed, 0, cols-1); @@ -5477,15 +5720,15 @@ uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have // and add them together with weightening unsigned dx = abs(x - x1); unsigned dy = abs(y - y1); - unsigned dist = 2 * sqrt16((dx * dx) + (dy * dy)); + unsigned dist = 2 * sqrt32_bw((dx * dx) + (dy * dy)); dx = abs(x - x2); dy = abs(y - y2); - dist += sqrt16((dx * dx) + (dy * dy)); + dist += sqrt32_bw((dx * dx) + (dy * dy)); dx = abs(x - x3); dy = abs(y - y3); - dist += sqrt16((dx * dx) + (dy * dy)); + dist += sqrt32_bw((dx * dx) + (dy * dy)); // inverse result int color = dist ? 1000 / dist : 255; @@ -5502,8 +5745,6 @@ uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have SEGMENT.setPixelColorXY(x3, y3, WHITE); } } - - return FRAMETIME; } // mode_2Dmetaballs() static const char _data_FX_MODE_2DMETABALLS[] PROGMEM = "Metaballs@!;;!;2"; @@ -5511,22 +5752,20 @@ static const char _data_FX_MODE_2DMETABALLS[] PROGMEM = "Metaballs@!;;!;2"; ////////////////////// // 2D Noise // ////////////////////// -uint16_t mode_2Dnoise(void) { // By Andrew Tuline - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dnoise(void) { // By Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const unsigned 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, strip.now / (16 - SEGMENT.speed/16)); + uint8_t pixelHue8 = perlin8(x * scale, y * scale, strip.now / (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"; @@ -5534,19 +5773,19 @@ 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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DPlasmaball(void) { // By: Stepko https://editor.soulmatelights.com/gallery/659-plasm-ball , Modified by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); uint_fast32_t t = (strip.now * 8) / (256 - SEGMENT.speed); // optimized to avoid float for (int i = 0; i < cols; i++) { - unsigned thisVal = inoise8(i * 30, t, t); + unsigned thisVal = perlin8(i * 30, t, t); unsigned thisMax = map(thisVal, 0, 255, 0, cols-1); for (int j = 0; j < rows; j++) { - unsigned thisVal_ = inoise8(t, j * 30, t); + unsigned thisVal_ = perlin8(t, j * 30, t); unsigned thisMax_ = map(thisVal_, 0, 255, 0, rows-1); int x = (i + thisMax_ - cols / 2); int y = (j + thisMax - cols / 2); @@ -5562,8 +5801,6 @@ uint16_t mode_2DPlasmaball(void) { // By: Stepko https://edito } } SEGMENT.blur(SEGMENT.custom2>>5); - - return FRAMETIME; } // mode_2DPlasmaball() static const char _data_FX_MODE_2DPLASMABALL[] PROGMEM = "Plasma Ball@Speed,,Fade,Blur;;!;2"; @@ -5571,16 +5808,12 @@ static const char _data_FX_MODE_2DPLASMABALL[] PROGMEM = "Plasma Ball@Speed,,Fad //////////////////////////////// // 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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); +void mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline & @dedehai (palette support) + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - CRGBPalette16 auroraPalette = {0x000000, 0x003300, 0x006600, 0x009900, 0x00cc00, 0x00ff00, 0x33ff00, 0x66ff00, 0x99ff00, 0xccff00, 0xffff00, 0xffcc00, 0xff9900, 0xff6600, 0xff3300, 0xff0000}; + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5589,47 +5822,30 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https float adjustHeight = (float)map(rows, 8, 32, 28, 12); // maybe use mapf() ??? unsigned 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; - } - } -*/ unsigned _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); int _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))); + uint8_t palindex = qsub8(perlin8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), fabsf((float)rows / 2.0f - (float)y) * adjustHeight); + uint8_t palbrightness = palindex; + if(SEGMENT.check1) palindex = 255 - palindex; //flip palette + SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(palindex, false, false, 255, palbrightness)); } } - - return FRAMETIME; } // mode_2DPolarLights() -static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale;;;2"; +static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale,,,,Flip Palette;;!;2;pal=71"; ///////////////////////// // 2D Pulser // ///////////////////////// -uint16_t mode_2DPulser(void) { // By: ldirko https://editor.soulmatelights.com/gallery/878-pulse-test , modifed by: Andrew Tuline - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DPulser(void) { // By: ldirko https://editor.soulmatelights.com/gallery/878-pulse-test , modifed by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(8 - (SEGMENT.intensity>>5)); uint32_t a = strip.now / (18 - SEGMENT.speed / 16); @@ -5638,8 +5854,6 @@ uint16_t mode_2DPulser(void) { // By: ldirko https://edi SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, map(y, 0, rows-1, 0, 255), 255, LINEARBLEND)); SEGMENT.blur(SEGMENT.intensity>>4); - - return FRAMETIME; } // mode_2DPulser() static const char _data_FX_MODE_2DPULSER[] PROGMEM = "Pulser@!,Blur;;!;2"; @@ -5647,17 +5861,17 @@ 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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DSindots(void) { // By: ldirko https://editor.soulmatelights.com/gallery/597-sin-dots , modified by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } - SEGMENT.fadeToBlackBy(SEGMENT.custom1>>3); + SEGMENT.fadeToBlackBy((SEGMENT.custom1>>3) + (SEGMENT.check1 * 24)); byte t1 = strip.now / (257 - SEGMENT.speed); // 20; byte t2 = sin8_t(t1) / 4 * 2; @@ -5666,27 +5880,25 @@ uint16_t mode_2DSindots(void) { // By: ldirko http int y = sin8_t(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; + SEGMENT.blur(SEGMENT.custom2 >> (3 + SEGMENT.check1), SEGMENT.check1); } // mode_2DSindots() -static const char _data_FX_MODE_2DSINDOTS[] PROGMEM = "Sindots@!,Dot distance,Fade rate,Blur;;!;2"; +static const char _data_FX_MODE_2DSINDOTS[] PROGMEM = "Sindots@!,Dot distance,Fade rate,Blur,,Smear;;!;2;"; ////////////////////////////// // 2D Squared Swirl // ////////////////////////////// // custom3 affects the blur amount. -uint16_t mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://gist.github.com/kriegsman/368b316c55221134b160 +void mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://gist.github.com/kriegsman/368b316c55221134b160 // Modifed by: Andrew Tuline - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const uint8_t kBorderWidth = 2; - SEGMENT.fadeToBlackBy(24); + SEGMENT.fadeToBlackBy(1 + SEGMENT.intensity / 5); SEGMENT.blur(SEGMENT.custom3>>1); // Use two out-of-sync sine waves @@ -5700,22 +5912,20 @@ uint16_t mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://g SEGMENT.addPixelColorXY(i, m, ColorFromPalette(SEGPALETTE, strip.now/29, 255, LINEARBLEND)); SEGMENT.addPixelColorXY(j, n, ColorFromPalette(SEGPALETTE, strip.now/41, 255, LINEARBLEND)); SEGMENT.addPixelColorXY(k, p, ColorFromPalette(SEGPALETTE, strip.now/73, 255, LINEARBLEND)); - - return FRAMETIME; } // mode_2Dsquaredswirl() -static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,,,,Blur;;!;2"; +static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,Fade,,,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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DSunradiation(void) { // By: ldirko https://editor.soulmatelights.com/gallery/599-sun-radiation , modified by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; - if (!SEGENV.allocateData(sizeof(byte)*(cols+2)*(rows+2))) return mode_static(); //allocation failed + if (!SEGENV.allocateData(sizeof(byte)*(cols+2)*(rows+2))) FX_FALLBACK_STATIC; //allocation failed byte *bump = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) { @@ -5727,7 +5937,8 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi 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; + //byte col = (inoise8_raw(i * someVal, j * someVal, t)) / 2; + byte col = ((int16_t)perlin8(i * someVal, j * someVal, t) - 0x7F) / 3; bump[index++] = col; } } @@ -5750,8 +5961,6 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi } yindex += (cols + 2); } - - return FRAMETIME; } // mode_2DSunradiation() static const char _data_FX_MODE_2DSUNRADIATION[] PROGMEM = "Sun Radiation@Variance,Brightness;;;2"; @@ -5759,11 +5968,11 @@ static const char _data_FX_MODE_2DSUNRADIATION[] PROGMEM = "Sun Radiation@Varian ///////////////////////// // 2D Tartan // ///////////////////////// -uint16_t mode_2Dtartan(void) { // By: Elliott Kember https://editor.soulmatelights.com/gallery/3-tartan , Modified by: Andrew Tuline - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dtartan(void) { // By: Elliott Kember https://editor.soulmatelights.com/gallery/3-tartan , Modified by: Andrew Tuline + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5789,8 +5998,6 @@ uint16_t mode_2Dtartan(void) { // By: Elliott Kember https://editor.so 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"; @@ -5798,20 +6005,20 @@ static const char _data_FX_MODE_2DTARTAN[] PROGMEM = "Tartan@X scale,Y scale,,,S ///////////////////////// // 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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void 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 || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; uint32_t tb = strip.now >> 12; // every ~4s if (tb > SEGENV.step) { int dir = ++SEGENV.aux0; - dir += (int)random8(3)-1; + dir += (int)hw_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); + SEGENV.step = tb + hw_random8(4); } SEGMENT.fadeToBlackBy(map(SEGMENT.speed, 0, 255, 248, 16)); @@ -5820,7 +6027,7 @@ uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [ht for (size_t i = 0; i < 8; i++) { int x = beatsin8_t(12 + i, 2, cols - 3); int y = beatsin8_t(15 + i, 2, rows - 3); - CRGB color = ColorFromPalette(SEGPALETTE, beatsin8_t(12 + i, 0, 255), 255); + uint32_t color = ColorFromPalette(SEGPALETTE, beatsin8_t(12 + i, 0, 255), 255); SEGMENT.addPixelColorXY(x, y, color); if (cols > 24 || rows > 24) { SEGMENT.addPixelColorXY(x+1, y, color); @@ -5829,23 +6036,21 @@ uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [ht SEGMENT.addPixelColorXY(x, y-1, color); } } - SEGMENT.blur(SEGMENT.intensity>>3); - - return FRAMETIME; + SEGMENT.blur(SEGMENT.intensity >> 3, SEGMENT.check1); } -static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur;;!;2"; +static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur,,,,Smear;;!;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) +//// Crazy bees by stepko (c)12.02.21 [https://editor.soulmatelights.com/gallery/651-crazy-bees], adapted by Blaz Kristan (AKA blazoncek), improved by @dedehai #define MAX_BEES 5 -uint16_t mode_2Dcrazybees(void) { - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dcrazybees(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; byte n = MIN(MAX_BEES, (rows * cols) / 256 + 1); @@ -5865,7 +6070,7 @@ uint16_t mode_2Dcrazybees(void) { }; } bee_t; - if (!SEGENV.allocateData(sizeof(bee_t)*MAX_BEES)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(sizeof(bee_t)*MAX_BEES)) FX_FALLBACK_STATIC; //allocation failed bee_t *bee = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) { @@ -5879,14 +6084,14 @@ uint16_t mode_2Dcrazybees(void) { if (strip.now > SEGENV.step) { SEGENV.step = strip.now + (FRAMETIME * 16 / ((SEGMENT.speed>>4)+1)); - - SEGMENT.fadeToBlackBy(32); - + SEGMENT.fadeToBlackBy(32 + ((SEGMENT.check1*SEGMENT.intensity) / 25)); + SEGMENT.blur(SEGMENT.intensity / (2 + SEGMENT.check1 * 9), SEGMENT.check1); 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)); + uint32_t flowerCcolor = SEGMENT.color_from_palette(bee[i].hue, false, true, 255); + SEGMENT.addPixelColorXY(bee[i].aimX + 1, bee[i].aimY, flowerCcolor); + SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY + 1, flowerCcolor); + SEGMENT.addPixelColorXY(bee[i].aimX - 1, bee[i].aimY, flowerCcolor); + SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, flowerCcolor); 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))); int error2 = bee[i].error * 2; @@ -5902,23 +6107,22 @@ uint16_t mode_2Dcrazybees(void) { bee[i].aimed(cols, rows); } } - SEGMENT.blur(SEGMENT.intensity>>4); } - return FRAMETIME; } -static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; +static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0"; #undef MAX_BEES +#ifdef WLED_PS_DONT_REPLACE_2D_FX ///////////////////////// // 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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dghostrider(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; typedef struct Lighter { int16_t gPosX; @@ -5933,7 +6137,7 @@ uint16_t mode_2Dghostrider(void) { int8_t Vspeed; } lighter_t; - if (!SEGENV.allocateData(sizeof(lighter_t))) return mode_static(); //allocation failed + if (!SEGENV.allocateData(sizeof(lighter_t))) FX_FALLBACK_STATIC; //allocation failed lighter_t *lighter = reinterpret_cast(SEGENV.data); const size_t maxLighters = min(cols + rows, LIGHTERS_AM); @@ -5941,9 +6145,8 @@ uint16_t mode_2Dghostrider(void) { if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { SEGENV.aux0 = cols; SEGENV.aux1 = rows; - //random16_set_seed(strip.now); - lighter->angleSpeed = random8(0,20) - 10; - lighter->gAngle = random16(); + lighter->angleSpeed = hw_random8(0,20) - 10; + lighter->gAngle = hw_random16(); lighter->Vspeed = 5; lighter->gPosX = (cols/2) * 10; lighter->gPosY = (rows/2) * 10; @@ -5971,7 +6174,7 @@ uint16_t mode_2Dghostrider(void) { 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); + lighter->time[i] += hw_random8(5, 20); if (lighter->time[i] >= 255 || (lighter->lightersPosX[i] <= 0) || (lighter->lightersPosX[i] >= (cols - 1) * 10) || @@ -5982,7 +6185,7 @@ uint16_t mode_2Dghostrider(void) { if (lighter->reg[i]) { lighter->lightersPosY[i] = lighter->gPosY; lighter->lightersPosX[i] = lighter->gPosX; - lighter->Angle[i] = lighter->gAngle + ((int)random8(20) - 10); + lighter->Angle[i] = lighter->gAngle + ((int)hw_random8(20) - 10); lighter->time[i] = 0; lighter->reg[i] = false; } else { @@ -5993,8 +6196,6 @@ uint16_t mode_2Dghostrider(void) { } SEGMENT.blur(SEGMENT.intensity>>3); } - - return FRAMETIME; } static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; #undef LIGHTERS_AM @@ -6004,11 +6205,11 @@ static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate, //////////////////////////// //// 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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dfloatingblobs(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; typedef struct Blob { float x[MAX_BLOBS], y[MAX_BLOBS]; @@ -6020,7 +6221,7 @@ uint16_t mode_2Dfloatingblobs(void) { size_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 + if (!SEGENV.allocateData(sizeof(blob_t))) FX_FALLBACK_STATIC; //allocation failed blob_t *blob = reinterpret_cast(SEGENV.data); if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { @@ -6028,12 +6229,12 @@ uint16_t mode_2Dfloatingblobs(void) { 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->r[i] = hw_random8(1, cols>8 ? (cols/4) : 2); + blob->sX[i] = (float) hw_random8(3, cols) / (float)(256 - SEGMENT.speed); // speed x + blob->sY[i] = (float) hw_random8(3, rows) / (float)(256 - SEGMENT.speed); // speed y + blob->x[i] = hw_random8(0, cols-1); + blob->y[i] = hw_random8(0, rows-1); + blob->color[i] = hw_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; @@ -6072,19 +6273,19 @@ uint16_t mode_2Dfloatingblobs(void) { 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->sX[i] = (float)hw_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] = (float)hw_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->sY[i] = (float)hw_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] = (float)hw_random8(3, rows) / (256 - SEGMENT.speed); blob->sY[i] = -blob->sY[i]; blob->y[i] = (float)rows - 1.01f; } @@ -6092,21 +6293,19 @@ uint16_t mode_2Dfloatingblobs(void) { SEGMENT.blur(SEGMENT.custom1>>2); if (SEGENV.step < strip.now) SEGENV.step = strip.now + 2000; // change colors every 2 seconds - - return FRAMETIME; } static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; #undef MAX_BLOBS - +#endif // WLED_PS_DONT_REPLACE_2D_FX //////////////////////////// // 2D Scrolling text // //////////////////////////// -uint16_t mode_2Dscrollingtext(void) { - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dscrollingtext(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; unsigned letterWidth, rotLW; unsigned letterHeight, rotLH; @@ -6119,7 +6318,8 @@ uint16_t mode_2Dscrollingtext(void) { case 5: letterWidth = 5; letterHeight = 12; break; } // letters are rotated - if (((SEGMENT.custom3+1)>>3) % 2) { + const int8_t rotate = map(SEGMENT.custom3, 0, 31, -2, 2); + if (rotate == 1 || rotate == -1) { rotLH = letterWidth; rotLW = letterHeight; } else { @@ -6128,9 +6328,7 @@ uint16_t mode_2Dscrollingtext(void) { } char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; - if (SEGMENT.name) for (size_t i=0,j=0; i31 && SEGMENT.name[i]<128) text[j++] = SEGMENT.name[i]; - const bool zero = strchr(text, '0') != nullptr; - + size_t result_pos = 0; char sec[5]; int AmPmHour = hour(localTime); bool isitAM = true; @@ -6142,16 +6340,62 @@ uint16_t mode_2Dscrollingtext(void) { sprintf_P(sec, PSTR(":%02d"), second(localTime)); } - if (!strlen(text)) { // fallback if empty segment name: display date and time + size_t len = 0; + if (SEGMENT.name) len = strlen(SEGMENT.name); // note: SEGMENT.name is limited to WLED_MAX_SEGNAME_LEN + if (len == 0) { // fallback if empty segment name: display date and time sprintf_P(text, PSTR("%s %d, %d %d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec); } else { - if (!strncmp_P(text,PSTR("#DATE"),5)) sprintf_P(text, zero?PSTR("%02d.%02d.%04d"):PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime)); - else if (!strncmp_P(text,PSTR("#DDMM"),5)) sprintf_P(text, zero?PSTR("%02d.%02d") :PSTR("%d.%d"), day(localTime), month(localTime)); - else if (!strncmp_P(text,PSTR("#MMDD"),5)) sprintf_P(text, zero?PSTR("%02d/%02d") :PSTR("%d/%d"), month(localTime), day(localTime)); - else if (!strncmp_P(text,PSTR("#TIME"),5)) sprintf_P(text, zero?PSTR("%02d:%02d%s") :PSTR("%2d:%02d%s"), AmPmHour, minute(localTime), sec); - else if (!strncmp_P(text,PSTR("#HHMM"),5)) sprintf_P(text, zero?PSTR("%02d:%02d") :PSTR("%d:%02d"), AmPmHour, minute(localTime)); - else if (!strncmp_P(text,PSTR("#HH"),3)) sprintf_P(text, zero?PSTR("%02d") :PSTR("%d"), AmPmHour); - else if (!strncmp_P(text,PSTR("#MM"),3)) sprintf_P(text, zero?PSTR("%02d") :PSTR("%d"), minute(localTime)); + size_t i = 0; + while (i < len) { + if (SEGMENT.name[i] == '#') { + char token[7]; // copy up to 6 chars + null terminator + bool zero = false; // a 0 suffix means display leading zeros + size_t j = 0; + while (j < 6 && i + j < len) { + token[j] = std::toupper(SEGMENT.name[i + j]); + if(token[j] == '0') + zero = true; // 0 suffix found. Note: there is an edge case where a '0' could be part of a trailing text and not the token, handling it is not worth the effort + j++; + } + token[j] = '\0'; + int advance = 5; // number of chars to advance in 'text' after processing the token + + // Process token + char temp[32]; + if (!strncmp_P(token,PSTR("#DATE"),5)) sprintf_P(temp, zero?PSTR("%02d.%02d.%04d"):PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime)); + else if (!strncmp_P(token,PSTR("#DDMM"),5)) sprintf_P(temp, zero?PSTR("%02d.%02d") :PSTR("%d.%d"), day(localTime), month(localTime)); + else if (!strncmp_P(token,PSTR("#MMDD"),5)) sprintf_P(temp, zero?PSTR("%02d/%02d") :PSTR("%d/%d"), month(localTime), day(localTime)); + else if (!strncmp_P(token,PSTR("#TIME"),5)) sprintf_P(temp, zero?PSTR("%02d:%02d%s") :PSTR("%2d:%02d%s"), AmPmHour, minute(localTime), sec); + else if (!strncmp_P(token,PSTR("#HHMM"),5)) sprintf_P(temp, zero?PSTR("%02d:%02d") :PSTR("%d:%02d"), AmPmHour, minute(localTime)); + else if (!strncmp_P(token,PSTR("#YYYY"),5)) sprintf_P(temp, PSTR("%04d") , year(localTime)); + else if (!strncmp_P(token,PSTR("#MONL"),5)) sprintf (temp, ("%s") , monthStr(month(localTime))); + else if (!strncmp_P(token,PSTR("#DDDD"),5)) sprintf (temp, ("%s") , dayStr(weekday(localTime))); + else if (!strncmp_P(token,PSTR("#YY"),3)) { sprintf (temp, ("%02d") , year(localTime)%100); advance = 3; } + else if (!strncmp_P(token,PSTR("#HH"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), AmPmHour); advance = 3; } + else if (!strncmp_P(token,PSTR("#MM"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), minute(localTime)); advance = 3; } + else if (!strncmp_P(token,PSTR("#SS"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), second(localTime)); advance = 3; } + else if (!strncmp_P(token,PSTR("#MON"),4)) { sprintf (temp, ("%s") , monthShortStr(month(localTime))); advance = 4; } + else if (!strncmp_P(token,PSTR("#MO"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), month(localTime)); advance = 3; } + else if (!strncmp_P(token,PSTR("#DAY"),4)) { sprintf (temp, ("%s") , dayShortStr(weekday(localTime))); advance = 4; } + else if (!strncmp_P(token,PSTR("#DD"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), day(localTime)); advance = 3; } + else { temp[0] = '#'; temp[1] = '\0'; zero = false; advance = 1; } // Unknown token, just copy the # + + if(zero) advance++; // skip the '0' suffix + size_t temp_len = strlen(temp); + if (result_pos + temp_len < WLED_MAX_SEGNAME_LEN) { + strcpy(text + result_pos, temp); + result_pos += temp_len; + } + + i += advance; + } + else { + if (result_pos < WLED_MAX_SEGNAME_LEN) { + text[result_pos++] = SEGMENT.name[i++]; // no token, just copy char + } else + break; // buffer full + } + } } const int numberOfLetters = strlen(text); @@ -6180,34 +6424,37 @@ uint16_t mode_2Dscrollingtext(void) { SEGENV.step = strip.now + map(SEGMENT.speed, 0, 255, 250, 50); // shift letters every ~250ms to ~50ms } - if (!SEGMENT.check2) SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail + SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail + uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); + uint32_t col2 = BLACK; + // if gradient is selected and palette is default (0) drawCharacter() uses gradient from SEGCOLOR(0) to SEGCOLOR(2) + // otherwise col2 == BLACK means use currently selected palette for gradient + // if gradient is not selected set both colors the same + if (SEGMENT.check1) { // use gradient + if (SEGMENT.palette == 0) { // use colors for gradient + col1 = SEGCOLOR(0); + col2 = SEGCOLOR(2); + } + } else col2 = col1; // force characters to use single color (from palette) for (int i = 0; i < numberOfLetters; i++) { int xoffset = int(cols) - int(SEGENV.aux0) + rotLW*i; if (xoffset + rotLW < 0) continue; // don't draw characters off-screen - uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); - uint32_t col2 = BLACK; - if (SEGMENT.check1 && SEGMENT.palette == 0) { - col1 = SEGCOLOR(0); - col2 = SEGCOLOR(2); - } - SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, map(SEGMENT.custom3, 0, 31, -2, 2)); + SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, rotate); } - - return FRAMETIME; } -static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,Overlay,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; +static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; //////////////////////////// // 2D Drift Rose // //////////////////////////// -//// Drift Rose by stepko (c)2021 [https://editor.soulmatelights.com/gallery/1369-drift-rose-pattern], adapted by Blaz Kristan (AKA blazoncek) -uint16_t mode_2Ddriftrose(void) { - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +//// Drift Rose by stepko (c)2021 [https://editor.soulmatelights.com/gallery/1369-drift-rose-pattern], adapted by Blaz Kristan (AKA blazoncek) improved by @dedehai +void mode_2Ddriftrose(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const float CX = (cols-cols%2)/2.f - .5f; const float CY = (rows-rows%2)/2.f - .5f; @@ -6218,26 +6465,25 @@ uint16_t mode_2Ddriftrose(void) { float angle = radians(i * 10); uint32_t x = (CX + (sin_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f; uint32_t y = (CY + (cos_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f; - SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); + if(SEGMENT.palette == 0) SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); + else SEGMENT.wu_pixel(x, y, ColorFromPalette(SEGPALETTE, i * 10)); } - SEGMENT.blur(SEGMENT.intensity>>4); - - return FRAMETIME; + SEGMENT.blur(SEGMENT.intensity >> 4, SEGMENT.check1); } -static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;;2"; +static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur,,,,Smear;;!;2;pal=11"; ///////////////////////////// // 2D PLASMA ROTOZOOMER // ///////////////////////////// // Plasma Rotozoomer by ldirko (c)2020 [https://editor.soulmatelights.com/gallery/457-plasma-rotozoomer], adapted for WLED by Blaz Kristan (AKA blazoncek) -uint16_t mode_2Dplasmarotozoom() { - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Dplasmarotozoom() { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; unsigned dataSize = SEGMENT.length() + sizeof(float); - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed float *a = reinterpret_cast(SEGENV.data); byte *plasma = reinterpret_cast(SEGENV.data+sizeof(float)); @@ -6267,8 +6513,6 @@ uint16_t mode_2Dplasmarotozoom() { } *a -= 0.03f + float(SEGENV.speed-128)*0.0002f; // rotation speed if(*a < -6283.18530718f) *a += 6283.18530718f; // 1000*2*PI, protect sin/cos from very large input float values (will give wrong results) - - return FRAMETIME; } static const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = "Rotozoomer@!,Scale,,,,Alt;;!;2;pal=54"; @@ -6280,56 +6524,16 @@ static const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = "Rotozoomer@!,Scale /////////////////////////////////////////////////////////////////////////////// -/* 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 (UsermodManager::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. +void mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuline. // This currently has no controls. - #define maxsteps 16 // Case statement wouldn't allow a variable. + #define MAXSTEPS 16 // Case statement wouldn't allow a variable. unsigned maxRipples = 16; unsigned dataSize = sizeof(Ripple) * maxRipples; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); um_data_t *um_data = getAudioData(); @@ -6343,7 +6547,6 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli // printUmData(); if (SEGENV.call == 0) { - SEGENV.aux0 = 255; SEGMENT.custom1 = *binNum; SEGMENT.custom2 = *maxVol * 2; } @@ -6362,35 +6565,33 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli break; case 255: // Initialize ripple variables. - ripples[i].pos = random16(SEGLEN); + ripples[i].pos = hw_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(); + ripples[i].color = hw_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)); + SEGMENT.setPixelColor(ripples[i].pos, SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0)); ripples[i].state++; break; - case maxsteps: // At the end of the ripples. 254 is an inactive mode. + 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)); + 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), uint8_t(2*255/ripples[i].state))); + 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), uint8_t(2*255/ripples[i].state))); 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 @@ -6400,11 +6601,11 @@ static const char _data_FX_MODE_RIPPLEPEAK[] PROGMEM = "Ripple Peak@Fade rate,Ma // * 2D Swirl // ///////////////////////// // By: Mark Kriegsman https://gist.github.com/kriegsman/5adca44e14ad025e6d3b , modified by Andrew Tuline -uint16_t mode_2DSwirl(void) { - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DSwirl(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -6429,8 +6630,6 @@ uint16_t mode_2DSwirl(void) { SEGMENT.addPixelColorXY(nj,ni, ColorFromPalette(SEGPALETTE, (strip.now / 29 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 29, 200, 255); SEGMENT.addPixelColorXY( i,nj, ColorFromPalette(SEGPALETTE, (strip.now / 37 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 37, 200, 255); SEGMENT.addPixelColorXY(ni, j, ColorFromPalette(SEGPALETTE, (strip.now / 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? @@ -6439,11 +6638,11 @@ static const char _data_FX_MODE_2DSWIRL[] PROGMEM = "Swirl@!,Sensitivity,Blur;,B // * 2D Waverly // ///////////////////////// // By: Stepko, https://editor.soulmatelights.com/gallery/652-wave , modified by Andrew Tuline -uint16_t mode_2DWaverly(void) { - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DWaverly(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; @@ -6452,10 +6651,10 @@ uint16_t mode_2DWaverly(void) { long t = strip.now / 2; for (int i = 0; i < cols; i++) { - unsigned thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; + unsigned thisVal = (1 + SEGMENT.intensity/64) * perlin8(i * 45 , t , t)/2; // use audio if available if (um_data) { - thisVal /= 32; // reduce intensity of inoise8() + thisVal /= 32; // reduce intensity of perlin8() thisVal *= volumeSmth; } int thisMax = map(thisVal, 0, 512, 0, rows); @@ -6466,8 +6665,6 @@ uint16_t mode_2DWaverly(void) { } } if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100); - - return FRAMETIME; } // mode_2DWaverly() static const char _data_FX_MODE_2DWAVERLY[] PROGMEM = "Waverly@Amplification,Sensitivity,,,,,Blur;;!;2v;ix=64,si=0"; // Beatsin @@ -6482,155 +6679,138 @@ typedef struct Gravity { /////////////////////// // * GRAVCENTER // /////////////////////// -uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); +// Gravcenter effects By Andrew Tuline. +// Gravcenter base function for Gravcenter (0), Gravcentric (1), Gravimeter (2), Gravfreq (3) (merged by @dedehai) + +void mode_gravcenter_base(unsigned mode) { + if (SEGLEN == 1) FX_FALLBACK_STATIC; const unsigned dataSize = sizeof(gravity); - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; - //SEGMENT.fade_out(240); - SEGMENT.fade_out(251); // 30% + if(mode == 1) SEGMENT.fade_out(253); // //Gravcentric + else if(mode == 2) SEGMENT.fade_out(249); // Gravimeter + else if(mode == 3) SEGMENT.fade_out(250); // Gravfreq + else SEGMENT.fade_out(251); // Gravcenter + float mySampleAvg; + int tempsamp; float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 32, 0, (float)SEGLEN/2.0f); // map to pixels available in current segment - 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, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + uint8_t gravity = 8 - SEGMENT.speed/32; + int offset = 1; + if(mode == 2) offset = 0; // Gravimeter + if (tempsamp >= gravcen->topLED) gravcen->topLED = tempsamp-offset; + else if (gravcen->gravityCounter % gravity == 0) gravcen->topLED--; + + if(mode == 1) { //Gravcentric + for (int i=0; itopLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); + } + } + else if(mode == 2) { //Gravimeter + for (int i=0; itopLED > 0) { + SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + } + } + else if(mode == 3) { //Gravfreq + for (int i=0; iu_data[4]; // used in mode 3: Gravfreq + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; + uint8_t index = (log10f(FFT_MajorPeak) - (MAX_FREQ_LOG10 - 1.78f)) * 255; + SEGMENT.setPixelColor(i+SEGLEN/2, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(SEGLEN/2-i-1, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); + } + if (gravcen->topLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); + } } + else { //Gravcenter + for (int i=0; itopLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + } + } gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; +} - return FRAMETIME; -} // mode_gravcenter() +void mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. + mode_gravcenter_base(0); +} static const char _data_FX_MODE_GRAVCENTER[] PROGMEM = "Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin - /////////////////////// // * GRAVCENTRIC // /////////////////////// -uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - - unsigned dataSize = sizeof(gravity); - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - Gravity* gravcen = reinterpret_cast(SEGENV.data); - - um_data_t *um_data = getAudioData(); - float volumeSmth = *(float*) um_data->u_data[0]; - - // printUmData(); - - //SEGMENT.fade_out(240); - //SEGMENT.fade_out(240); // twice? really? - SEGMENT.fade_out(253); // 50% - - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0f); // map to pixels availeable in current segment - int tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. - uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) - gravcen->topLED = tempsamp-1; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED >= 0) { - SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); - } - gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; - - return FRAMETIME; -} // mode_gravcentric() +void mode_gravcentric(void) { // Gravcentric. By Andrew Tuline. + mode_gravcenter_base(1); +} static const char _data_FX_MODE_GRAVCENTRIC[] PROGMEM = "Gravcentric@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=3,si=0"; // Corner, Beatsin /////////////////////// // * GRAVIMETER // /////////////////////// -uint16_t mode_gravimeter(void) { // Gravmeter. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - - unsigned dataSize = sizeof(gravity); - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - Gravity* gravcen = reinterpret_cast(SEGENV.data); - - um_data_t *um_data = getAudioData(); - float volumeSmth = *(float*) um_data->u_data[0]; - - //SEGMENT.fade_out(240); - SEGMENT.fade_out(249); // 25% - - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0; - segmentSampleAvg *= 0.25; // divide by 4, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment - int tempsamp = constrain(mySampleAvg,0,SEGLEN-1); // Keep the sample from overflowing. - uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) - gravcen->topLED = tempsamp; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; +void mode_gravimeter(void) { // Gravmeter. By Andrew Tuline. + mode_gravcenter_base(2); +} +static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin - if (gravcen->topLED > 0) { - SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(strip.now, 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 +/////////////////////// +// ** Gravfreq // +/////////////////////// +void mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. + mode_gravcenter_base(3); +} +static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin ////////////////////// // * JUGGLES // ////////////////////// -uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. +void mode_juggles(void) { // Juggles. By Andrew Tuline. um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(224); // 6.25% - unsigned my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); + uint8_t my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); for (size_t i=0; i(SEGENV.data); um_data_t *um_data = getAudioData(); @@ -6806,7 +6983,7 @@ uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. plasmoip->thisphase += beatsin8_t(6,-4,4); // You can change direction and speed individually. plasmoip->thatphase += beatsin8_t(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. + for (unsigned 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_t(((i*(97 +(5*SEGMENT.speed/32)))+plasmoip->thatphase) & 0xFF)/2; // Let's munge the brightness a bit and animate it all with the phases. @@ -6816,88 +6993,66 @@ uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. 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;!,!;!;01v;sx=128,ix=128,m12=0,si=0"; // Pixels, Beatsin -/////////////////////// -// * PUDDLEPEAK // -/////////////////////// -// Andrew's crappy peak detector. If I were 40+ years younger, I'd learn signal processing. -uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - - unsigned size = 0; - uint8_t fadeVal = map(SEGMENT.speed,0,255, 224, 254); - unsigned pos = random16(SEGLEN); // Set a random starting position. - - um_data_t *um_data = getAudioData(); - 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 (unsigned i=0; iu_data[1]; + 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 (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; + if(peakdetect) { // puddles peak + *binNum = SEGMENT.custom1; // Select a bin. + *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. + 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; + } } - + else { // puddles + 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 (unsigned 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; @@ -6911,25 +7066,17 @@ uint16_t mode_pixels(void) { // Pixels. By Andrew Tuline. SEGMENT.fade_out(64+(SEGMENT.speed>>1)); for (int i=0; i SPEED_FORMULA_L) { - unsigned segLoc = random16(SEGLEN); - SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/max(1, SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), 2*fftResult[SEGENV.aux0%16])); + unsigned segLoc = hw_random16(SEGLEN); + SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/max(1, (int)SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), uint8_t(2*fftResult[SEGENV.aux0%16]))); ++(SEGENV.aux0) %= 16; // make sure it doesn't cross 16 SEGENV.step = 1; - SEGMENT.blur(SEGMENT.intensity); + SEGMENT.blur(SEGMENT.intensity); // note: blur > 210 results in a alternating pattern, this could be fixed by mapping but some may like it (very old bug) } - - 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 @@ -6961,7 +7106,7 @@ static const char _data_FX_MODE_BLURZ[] PROGMEM = "Blurz@Fade rate,Blur;!,Color ///////////////////////// // ** DJLight // ///////////////////////// -uint16_t mode_DJLight(void) { // Written by ??? Adapted by Will Tatam. +void mode_DJLight(void) { // Written by ??? Adapted by Will Tatam. // No need to prevent from executing on single led strips, only mid will be set (mid = 0) const int mid = SEGLEN / 2; @@ -6983,8 +7128,6 @@ uint16_t mode_DJLight(void) { // Written by ??? Adapted by Wil 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;;;01f;m12=2,si=0"; // Circle, Beatsin @@ -6992,8 +7135,8 @@ static const char _data_FX_MODE_DJLIGHT[] PROGMEM = "DJ Light@Speed;;;01f;m12=2, //////////////////// // ** Freqmap // //////////////////// -uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. Would be better if a higher framerate. - if (SEGLEN == 1) return mode_static(); +void mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. Would be better if a higher framerate. + if (SEGLEN <= 1) FX_FALLBACK_STATIC; // Start frequency = 60 Hz and log10(60) = 1.78 // End frequency = MAX_FREQUENCY in Hz and lo10(MAX_FREQUENCY) = MAX_FREQ_LOG10 @@ -7009,15 +7152,13 @@ uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. 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; + if (locn >= (int)SEGLEN) locn = SEGLEN-1; unsigned 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 - unsigned bright = (int)my_magnitude; + uint8_t bright = (uint8_t)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 @@ -7025,7 +7166,7 @@ static const char _data_FX_MODE_FREQMAP[] PROGMEM = "Freqmap@Fade rate,Starting /////////////////////// // ** Freqmatrix // /////////////////////// -uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung. +void mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung. // No need to prevent from executing on single led strips, we simply change pixel 0 each time and avoid the shift um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; @@ -7068,8 +7209,6 @@ uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Plesch // if SEGLEN equals 1 this loop won't execute for (int i = SEGLEN - 1; i > 0; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left } - - return FRAMETIME; } // mode_freqmatrix() static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound effect,Low bin,High bin,Sensitivity;;;01f;m12=3,si=0"; // Corner, Beatsin @@ -7081,7 +7220,7 @@ static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound e // 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. +void mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 16.0f; @@ -7099,11 +7238,9 @@ uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. uint8_t pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow for (int i=0; i < SEGMENT.intensity/32+1; i++) { - unsigned locn = random16(0,SEGLEN); - SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); + unsigned locn = hw_random16(0,SEGLEN); + SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (uint8_t)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 @@ -7123,7 +7260,7 @@ static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Sta // // 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. +void mode_freqwave(void) { // Freqwave. By Andreas Pleschung. // As before, this effect can also work on single pixels, we just lose the shifting effect um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; @@ -7162,67 +7299,17 @@ uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschun // shift the pixels one pixel outwards // if SEGLEN equals 1 these loops won't execute - 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 + for (unsigned i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left + for (unsigned 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;;;01f;m12=2,si=0"; // Circle, Beatsin -/////////////////////// -// ** Gravfreq // -/////////////////////// -uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - unsigned dataSize = sizeof(gravity); - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - Gravity* gravcen = reinterpret_cast(SEGENV.data); - - um_data_t *um_data = getAudioData(); - float FFT_MajorPeak = *(float*)um_data->u_data[4]; - float volumeSmth = *(float*)um_data->u_data[0]; - if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) - - SEGMENT.fade_out(250); - - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0,32, 0, (float)SEGLEN/2.0f); // map to pixels availeable in current segment - int tempsamp = constrain(mySampleAvg,0,SEGLEN/2); // Keep the sample from overflowing. - uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) - gravcen->topLED = tempsamp-1; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED >= 0) { - SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); - } - gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; - - return FRAMETIME; -} // mode_gravfreq() -static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin - - ////////////////////// // ** Noisemove // ////////////////////// -uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuline +void mode_noisemove(void) { // Noisemove. By: Andrew Tuline um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; @@ -7231,21 +7318,19 @@ uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuli 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; @@ -7271,8 +7356,6 @@ uint16_t mode_rocktaves(void) { // Rocktaves. Same note from eac unsigned i = map(beatsin8_t(8+octCount*4, 0, 255, 0, octCount*8), 0, 255, 0, SEGLEN-1); i = constrain(i, 0U, SEGLEN-1U); 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@;!,!;!;01f;m12=1,si=0"; // Bar, Beatsin @@ -7281,10 +7364,13 @@ static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;01f;m12= // ** Waterfall // /////////////////////// // Combines peak detection with FFT_MajorPeak and FFT_Magnitude. -uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tuline +void mode_waterfall(void) { // Waterfall. By: Andrew Tuline // effect can work on single pixels, we just lose the shifting effect - - um_data_t *um_data = getAudioData(); + unsigned dataSize = sizeof(uint32_t) * SEGLEN; + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed + uint32_t* pixels = reinterpret_cast(SEGENV.data); + + um_data_t *um_data = getAudioData(); 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]; @@ -7294,7 +7380,7 @@ uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tulin if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) if (SEGENV.call == 0) { - SEGMENT.fill(BLACK); + for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = BLACK; // may not be needed as resetIfRequired() clears buffer SEGENV.aux0 = 255; SEGMENT.custom1 = *binNum; SEGMENT.custom2 = *maxVol * 2; @@ -7311,16 +7397,19 @@ uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tulin 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 + unsigned k = SEGLEN-1; if (samplePeak) { - SEGMENT.setPixelColor(SEGLEN-1, CHSV(92,92,92)); + pixels[k] = (uint32_t)CRGB(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)); + pixels[k] = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude); } + SEGMENT.setPixelColor(k, pixels[k]); // loop will not execute if SEGLEN equals 1 - for (int i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left + for (unsigned i = 0; i < k; i++) { + pixels[i] = pixels[i+1]; // shift left + SEGMENT.setPixelColor(i, pixels[i]); + } } - - return FRAMETIME; } // mode_waterfall() static const char _data_FX_MODE_WATERFALL[] PROGMEM = "Waterfall@!,Adjust color,Select bin,Volume (min);!,!;!;01f;c2=0,m12=2,si=0"; // Circles, Beatsin @@ -7329,14 +7418,15 @@ static const char _data_FX_MODE_WATERFALL[] PROGMEM = "Waterfall@!,Adjust color, ///////////////////////// // ** 2D GEQ // ///////////////////////// -uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int NUM_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int CENTER_BIN = map(SEGMENT.custom3, 0, 31, 0, 15); + const int cols = SEG_W; + const int rows = SEG_H; - if (!SEGENV.allocateData(cols*sizeof(uint16_t))) return mode_static(); //allocation failed + if (!SEGENV.allocateData(cols*sizeof(uint16_t))) FX_FALLBACK_STATIC; //allocation failed uint16_t *previousBarHeight = reinterpret_cast(SEGENV.data); //array of previous bar heights per frequency band um_data_t *um_data = getAudioData(); @@ -7354,8 +7444,14 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. 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, 0, NUM_BANDS); - 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. + int band = map(x, 0, cols, 0, NUM_BANDS); + if (NUM_BANDS < 16) { + int startBin = constrain(CENTER_BIN - NUM_BANDS/2, 0, 15 - NUM_BANDS + 1); + if(NUM_BANDS <= 1) + band = CENTER_BIN; // map() does not work for single band + else + band = map(band, 0, NUM_BANDS - 1, startBin, startBin + NUM_BANDS - 1); + } band = constrain(band, 0, 15); unsigned colorIndex = band * 17; int barHeight = map(fftResult[band], 0, 255, 0, rows); // do not subtract -1 from rows here @@ -7374,20 +7470,18 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. 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 +static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ@Fade speed,Ripple decay,# of bands,,Bin,Color bars;!,,Peaks;!;2f;c1=255,c2=64,pal=11,si=0,c3=0"; ///////////////////////// // ** 2D Funky plank // ///////////////////////// -uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Will Tatam. - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DFunkyPlank(void) { // Written by ??? Adapted by Will Tatam. + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; int NUMB_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); int barWidth = (cols / NUMB_BANDS); @@ -7427,8 +7521,6 @@ uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Wil } } } - - return FRAMETIME; } // mode_2DFunkyPlank static const char _data_FX_MODE_2DFUNKYPLANK[] PROGMEM = "Funky Plank@Scroll speed,,# of bands;;;2f;si=0"; // Beatsin @@ -7471,11 +7563,11 @@ static uint8_t akemi[] PROGMEM = { 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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2DAkemi(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; @@ -7523,7 +7615,7 @@ uint16_t mode_2DAkemi(void) { unsigned band = map(x, 0, max(xMax,4), 0, 15); // map 0..cols/8 to 16 GEQ bands band = constrain(band, 0, 15); int barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); - CRGB color = CRGB(SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0)); + uint32_t 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); @@ -7531,37 +7623,38 @@ uint16_t mode_2DAkemi(void) { } } } - - 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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +// adapted for WLED by @blazoncek, improvements by @dedehai +void mode_2Ddistortionwaves() { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; uint8_t speed = SEGMENT.speed/32; uint8_t scale = SEGMENT.intensity/32; - - uint8_t w = 2; + if(SEGMENT.check2) scale += 192 / (cols+rows); // zoom out some more. note: not changing scale slider for backwards compatibility unsigned a = strip.now/32; unsigned a2 = a/2; unsigned a3 = a/3; + unsigned colsScaled = cols * scale; + unsigned rowsScaled = rows * scale; + + unsigned cx = beatsin16_t(10-speed,0,colsScaled); + unsigned cy = beatsin16_t(12-speed,0,rowsScaled); + unsigned cx1 = beatsin16_t(13-speed,0,colsScaled); + unsigned cy1 = beatsin16_t(15-speed,0,rowsScaled); + unsigned cx2 = beatsin16_t(17-speed,0,colsScaled); + unsigned cy2 = beatsin16_t(14-speed,0,rowsScaled); + + byte rdistort, gdistort, bdistort; - unsigned cx = beatsin8_t(10-speed,0,cols-1)*scale; - unsigned cy = beatsin8_t(12-speed,0,rows-1)*scale; - unsigned cx1 = beatsin8_t(13-speed,0,cols-1)*scale; - unsigned cy1 = beatsin8_t(15-speed,0,rows-1)*scale; - unsigned cx2 = beatsin8_t(17-speed,0,cols-1)*scale; - unsigned cy2 = beatsin8_t(14-speed,0,rows-1)*scale; - unsigned xoffs = 0; for (int x = 0; x < cols; x++) { xoffs += scale; @@ -7570,65 +7663,132 @@ uint16_t mode_2Ddistortionwaves() { for (int y = 0; y < rows; y++) { yoffs += scale; - byte rdistort = cos8_t((cos8_t(((x<<3)+a )&255)+cos8_t(((y<<3)-a2)&255)+a3 )&255)>>1; - byte gdistort = cos8_t((cos8_t(((x<<3)-a2)&255)+cos8_t(((y<<3)+a3)&255)+a+32 )&255)>>1; - byte bdistort = cos8_t((cos8_t(((x<<3)+a3)&255)+cos8_t(((y<<3)-a) &255)+a2+64)&255)>>1; + if(SEGMENT.check3) { + // alternate mode from original code + rdistort = cos8_t (((x+y)*8+a2)&255)>>1; + gdistort = cos8_t (((x+y)*8+a3+32)&255)>>1; + bdistort = cos8_t (((x+y)*8+a+64)&255)>>1; + } else { + rdistort = cos8_t((cos8_t(((x<<3)+a )&255)+cos8_t(((y<<3)-a2)&255)+a3 )&255)>>1; + gdistort = cos8_t((cos8_t(((x<<3)-a2)&255)+cos8_t(((y<<3)+a3)&255)+a+32 )&255)>>1; + bdistort = cos8_t((cos8_t(((x<<3)+a3)&255)+cos8_t(((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 )); + byte valueR = rdistort + ((a- ( ((xoffs - cx) * (xoffs - cx) + (yoffs - cy) * (yoffs - cy))>>7 ))<<1); + byte valueG = gdistort + ((a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 ))<<1); + byte valueB = bdistort + ((a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 ))<<1); - valueR = gamma8(cos8_t(valueR)); - valueG = gamma8(cos8_t(valueG)); - valueB = gamma8(cos8_t(valueB)); + valueR = cos8_t(valueR); + valueG = cos8_t(valueG); + valueB = cos8_t(valueB); - SEGMENT.setPixelColorXY(x, y, RGBW32(valueR, valueG, valueB, 0)); + if(SEGMENT.palette == 0) { + // use RGB values (original color mode) + SEGMENT.setPixelColorXY(x, y, RGBW32(valueR, valueG, valueB, 0)); + } else { + // use palette + uint8_t brightness = (valueR + valueG + valueB) / 3; + if(SEGMENT.check1) { // map brightness to palette index + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP)); + } else { + // color mapping: calculate hue from pixel color, map it to palette index + CHSV hsvclr = rgb2hsv_approximate(CRGB(valueR>>2, valueG>>2, valueB>>2)); // scale colors down to not saturate for better hue extraction + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hsvclr.h, brightness)); + } + } } } - return FRAMETIME; + // palette mode and not filling: smear-blur to cover up palette wrapping artefacts + if(!SEGMENT.check1 && SEGMENT.palette) + SEGMENT.blur(200, true); } -static const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = "Distortion Waves@!,Scale;;;2"; +static const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = "Distortion Waves@!,Scale,,,,Fill,Zoom,Alt;;!;2;pal=0"; //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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +// adapted for WLED by @blazoncek, optimization by @dedehai +static void soapPixels(bool isRow, uint8_t *noise3d, CRGB *pixels) { + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return x + y * cols; }; + const auto abs = [](int x) { return x<0 ? -x : x; }; + const int tRC = isRow ? rows : cols; // transpose if isRow + const int tCR = isRow ? cols : rows; // transpose if isRow + const int amplitude = max(1, (tCR - 8) >> 3) * (1 + (SEGMENT.custom1 >> 5)); + const int shift = 0; //(128 - SEGMENT.custom2)*2; + + CRGB ledsbuff[tCR]; + + for (int i = 0; i < tRC; i++) { + int amount = ((int)noise3d[isRow ? i*cols : i] - 128) * amplitude + shift; // use first row/column: XY(0,i)/XY(i,0) + int delta = abs(amount) >> 8; + int fraction = abs(amount) & 255; + for (int j = 0; j < tCR; j++) { + int zD, zF; + if (amount < 0) { + zD = j - delta; + zF = zD - 1; + } else { + zD = j + delta; + zF = zD + 1; + } + int yA = abs(zD)%tCR; + int yB = abs(zF)%tCR; + int xA = i; + int xB = i; + if (isRow) { + std::swap(xA,yA); + std::swap(xB,yB); + } + const int indxA = XY(xA,yA); + const int indxB = XY(xB,yB); + CRGB PixelA; + CRGB PixelB; + if ((zD >= 0) && (zD < tCR)) PixelA = pixels[indxA]; + else PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[indxA]*3); + if ((zF >= 0) && (zF < tCR)) PixelB = pixels[indxB]; + else PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[indxB]*3); + ledsbuff[j] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); + } + for (int j = 0; j < tCR; j++) { + CRGB c = ledsbuff[j]; + if (isRow) std::swap(j,i); + SEGMENT.setPixelColorXY(i, j, pixels[XY(i,j)] = c); + if (isRow) std::swap(j,i); + } + } +} - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); +void mode_2Dsoap() { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - 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 + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return x + y * cols; }; - 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 size_t segSize = SEGMENT.width() * SEGMENT.height(); // prevent reallocation if mirrored or grouped + const size_t dataSize = segSize * (sizeof(uint8_t) + sizeof(CRGB)); // pixels and noise + if (!SEGENV.allocateData(dataSize + sizeof(uint32_t)*3)) FX_FALLBACK_STATIC; //allocation failed + + uint8_t *noise3d = reinterpret_cast(SEGENV.data); + CRGB *pixels = reinterpret_cast(SEGENV.data + segSize * sizeof(uint8_t)); + uint32_t *noisecoord = reinterpret_cast(SEGENV.data + dataSize); // x, y, z coordinates 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; - } + if (SEGENV.call == 0) for (int i = 0; i < 3; i++) noisecoord[i] = hw_random(); // init + else for (int i = 0; i < 3; i++) noisecoord[i] += 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; + uint8_t data = perlin16(noisecoord[0] + ioffset, noisecoord[1] + joffset, noisecoord[2]) >> 8; noise3d[XY(i,j)] = scale8(noise3d[XY(i,j)], smoothness) + scale8(data, 255 - smoothness); } } @@ -7643,75 +7803,22 @@ uint16_t mode_2Dsoap() { } } - int zD; - int zF; - int amplitude; - int shiftX = 0; //(SEGMENT.custom1 - 128) / 4; - int shiftY = 0; //(SEGMENT.custom2 - 128) / 4; - CRGB ledsbuff[MAX(cols,rows)]; - - amplitude = (cols >= 16) ? (cols-8)/8 : 1; - for (int y = 0; y < rows; y++) { - int amount = ((int)noise3d[XY(0,y)] - 128) * 2 * amplitude + 256*shiftX; - int delta = abs(amount) >> 8; - int fraction = abs(amount) & 255; - for (int x = 0; x < cols; x++) { - if (amount < 0) { - zD = x - delta; - zF = zD - 1; - } else { - zD = x + delta; - zF = zD + 1; - } - CRGB PixelA = CRGB::Black; - if ((zD >= 0) && (zD < cols)) PixelA = SEGMENT.getPixelColorXY(zD, y); - else PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[XY(abs(zD),y)]*3); - CRGB PixelB = CRGB::Black; - if ((zF >= 0) && (zF < cols)) PixelB = SEGMENT.getPixelColorXY(zF, y); - else PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[XY(abs(zF),y)]*3); - ledsbuff[x] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); - } - for (int x = 0; x < cols; x++) SEGMENT.setPixelColorXY(x, y, ledsbuff[x]); - } - - amplitude = (rows >= 16) ? (rows-8)/8 : 1; - for (int x = 0; x < cols; x++) { - int amount = ((int)noise3d[XY(x,0)] - 128) * 2 * amplitude + 256*shiftY; - int delta = abs(amount) >> 8; - int fraction = abs(amount) & 255; - for (int y = 0; y < rows; y++) { - if (amount < 0) { - zD = y - delta; - zF = zD - 1; - } else { - zD = y + delta; - zF = zD + 1; - } - CRGB PixelA = CRGB::Black; - if ((zD >= 0) && (zD < rows)) PixelA = SEGMENT.getPixelColorXY(x, zD); - else PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[XY(x,abs(zD))]*3); - CRGB PixelB = CRGB::Black; - if ((zF >= 0) && (zF < rows)) PixelB = SEGMENT.getPixelColorXY(x, zF); - else PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[XY(x,abs(zF))]*3); - ledsbuff[y] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); - } - for (int y = 0; y < rows; y++) SEGMENT.setPixelColorXY(x, y, ledsbuff[y]); - } - - return FRAMETIME; + soapPixels(true, noise3d, pixels); // rows + soapPixels(false, noise3d, pixels); // cols } -static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness;;!;2"; +static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness,Density;;!;2;pal=11"; //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 || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +void mode_2Doctopus() { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; const uint8_t mapp = 180 / MAX(cols,rows); typedef struct { @@ -7720,7 +7827,7 @@ uint16_t mode_2Doctopus() { } 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 + if (!SEGENV.allocateData(dataSize + 2)) FX_FALLBACK_STATIC; //allocation failed map_t *rMap = reinterpret_cast(SEGENV.data); uint8_t *offsX = reinterpret_cast(SEGENV.data + dataSize); @@ -7753,247 +7860,3072 @@ uint16_t mode_2Doctopus() { //CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1))); unsigned intensity = sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); intensity = map((intensity*intensity) & 0xFFFF, 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); + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity)); } } - return FRAMETIME; } static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offset Y,Legs,fasttan;;!;2;"; //Waving Cell //@Stepko (https://editor.soulmatelights.com/gallery/1704-wavingcells) -// adapted for WLED by @blazoncek -uint16_t mode_2Dwavingcell() { - if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up +// adapted for WLED by @blazoncek, improvements by @dedehai +void mode_2Dwavingcell() { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up + + const int cols = SEG_W; + const int rows = SEG_H; + + uint32_t t = (strip.now*(SEGMENT.speed + 1))>>3; + uint32_t aX = SEGMENT.custom1/16 + 9; + uint32_t aY = SEGMENT.custom2/16 + 1; + uint32_t aZ = SEGMENT.custom3 + 1; + for (int x = 0; x < cols; x++) { + for (int y = 0; y < rows; y++) { + uint32_t wave = sin8_t((x * aX) + sin8_t((((y<<8) + t) * aY)>>8)) + cos8_t(y * aZ); // bit shifts to increase temporal resolution + uint8_t colorIndex = wave + (t>>(8-(SEGMENT.check2*3))); + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, colorIndex)); + } + } + SEGMENT.blur(SEGMENT.intensity); +} +static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amplitude 1,Amplitude 2,Amplitude 3,,Flow;;!;2;ix=0"; - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); +#ifndef WLED_DISABLE_PARTICLESYSTEM2D - uint32_t t = strip.now/(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 setMotionBlur(180); + #else + PartSys->setMotionBlur(130); + #endif + for (i = 0; i < min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); i++) { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].maxLife = 900; + PartSys->sources[i].minLife = 800; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + uint32_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 + #ifdef ESP8266 + for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed + int partindex = (int)PartSys->usedParticles - (int)i; + if (partindex >= 0) { + PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center + PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center + PartSys->particles[partindex].sat = 230; + PartSys->particles[partindex].ttl = 256; //keep alive + } + } + #endif - return FRAMETIME; -} -static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; + if (SEGMENT.check1) + PartSys->setSmearBlur(90); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur + // update colors of the sprays + for (i = 0; i < spraycount; i++) { + uint32_t coloroffset = 0xFF / spraycount; + PartSys->sources[i].source.hue = coloroffset * i; + } -#endif // WLED_DISABLE_2D + // set rotation direction and speed + // can use direction flag to determine current direction + bool direction = SEGMENT.check2; //no automatic direction change, set it to flag + int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step + if (SEGMENT.custom2 > 0) { // automatic direction change enabled + uint32_t changeinterval = 1040 - ((uint32_t)SEGMENT.custom2 << 2); + direction = SEGENV.aux1 & 0x01; //set direction according to flag -////////////////////////////////////////////////////////////////////////////////////////// -// mode data -static const char _data_RESERVED[] PROGMEM = "RSVD"; + if (SEGMENT.check3) // random interval + changeinterval = 20 + changeinterval + hw_random16(changeinterval); -// add (or replace reserved) effect mode and data into vector -// use id==255 to find unallocated gaps (with "Reserved" data string) -// if vector size() is smaller than id (single) data is appended at the end (regardless of id) -// return the actual id used for the effect or 255 if the add failed. -uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { - if (id == 255) { // find empty slot - for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } + if (SEGMENT.call % changeinterval == 0) { //flip direction on next frame + SEGENV.aux1 |= 0x02; // set the update flag (for random interval update) + if (direction) + SEGENV.aux1 &= ~0x01; // clear the direction flag + else + SEGENV.aux1 |= 0x01; // set the direction flag + } } - if (id < _mode.size()) { - if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect - _mode[id] = mode_fn; - _modeData[id] = mode_name; - return id; - } else if(_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added - _mode.push_back(mode_fn); - _modeData.push_back(mode_name); - if (_modeCount < _mode.size()) _modeCount++; - return _mode.size() - 1; - } else { - return 255; // The vector is full so return 255 + + int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 3); + int32_t speeddiff = targetspeed - currentspeed; + int32_t speedincrement = speeddiff / 50; + + if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed + if (speeddiff < 0) + speedincrement = -1; + else if (speeddiff > 0) + speedincrement = 1; } -} -void WS2812FX::setupEffectData() { - // Solid must be first! (assuming vector is empty upon call to setup) - _mode.push_back(&mode_static); - _modeData.push_back(_data_FX_MODE_STATIC); - // fill reserved word in case there will be any gaps in the array - for (size_t i=1; i<_modeCount; i++) { - _mode.push_back(&mode_static); - _modeData.push_back(_data_RESERVED); + currentspeed += speedincrement; + SEGENV.aux0 += currentspeed; + SEGENV.step = (uint32_t)currentspeed; //save it back + + uint16_t angleoffset = 0xFFFF / spraycount; // angle offset for an even distribution + uint32_t skip = PS_P_HALFRADIUS / (SEGMENT.intensity + 1) + 1; // intensity is emit speed, emit less on low speeds + if (SEGMENT.call % skip == 0) { + j = hw_random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + for (i = 0; i < spraycount; i++) { // emit one particle per spray (if available) + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation + #ifdef ESP8266 + if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles + #endif + PartSys->angleEmit(PartSys->sources[j], SEGENV.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); + j = (j + 1) % spraycount; + } } - // now replace all pre-allocated effects - // --- 1D non-audio effects --- - addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); - addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); - addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); - addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); - addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); - addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); - addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); - addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW); - addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE); - addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN); - addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN); - addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE); - addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE); - addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW); - addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS); - addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW); - addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); - addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); - addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); - addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); - addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); - addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); - addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); - addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW); - addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE); - addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW); - addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID); - addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR); - addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM); - addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW); - addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH); - addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM); - addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE); - addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL); - addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT); - addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM); - addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR); - addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); - addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); - addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); - addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); - addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); - addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); - addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); - addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); - addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); - addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + PartSys->update(); //update all particles and render to frame +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=27,c1=200,c2=0,c3=0"; - addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); - addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); - addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); - addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); +/* + Particle Fireworks + Rockets shoot up and explode in a random color, sometimes in a defined pattern + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 8 - addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); - addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); - addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); - addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); - addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); - addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); - addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); - addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); - addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); - addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); - addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); - addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); - addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); - addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); - addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); - addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); - addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); - addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2); - addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3); - addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4); - addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); - addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); - addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); - addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); - addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); - addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); - addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); - addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT); - addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES); - addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN); - addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); - addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); - addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); - addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); - addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); - addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); - addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); - addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); - addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); - addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); - addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); - addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); - addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); - addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); - addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); - addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); - addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); - addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); - addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); - addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); - addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); - addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); - addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); - addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL); - addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); - addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); - addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); - addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); - addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); +void mode_particlefireworks(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t numRockets; - addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); - addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); - addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) + FX_FALLBACK_STATIC; // allocation failed - // --- 1D audio effects --- - addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); - addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); - addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); - addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); - addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); - addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID); - addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES); - addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE); - addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); - addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); - addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setWallHardness(120); // ground bounce is fixed + numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (uint32_t j = 0; j < numRockets; j++) { + PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon + PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); - addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! - addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); - addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); - addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numRockets = map(SEGMENT.speed, 0 , 255, 4, min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES)); - addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); - addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceY(SEGMENT.check2); + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top + PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 245)); // anable motion blur - addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); + // update the rockets, set the speed state + for (uint32_t j = 0; j < numRockets; j++) { + PartSys->applyGravity(PartSys->sources[j].source); + PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->sources[j].sourceFlags); + if (PartSys->sources[j].source.ttl == 0) { + if (PartSys->sources[j].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code below) + PartSys->sources[j].source.vy = 0; + } + else if (PartSys->sources[j].source.vy < 0) { // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom + PartSys->sources[j].source.x = (PartSys->maxX >> 2) + hw_random(PartSys->maxX >> 1); // centered half + PartSys->sources[j].source.vy = (SEGMENT.custom3) + hw_random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height + PartSys->sources[j].source.vx = hw_random16(7) - 3; // not perfectly straight up + PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].source.ttl = hw_random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time + PartSys->sources[j].maxLife = 40; // exhaust particle life + PartSys->sources[j].minLife = 10; + PartSys->sources[j].vx = 0; // emitting speed + PartSys->sources[j].vy = -5; // emitting speed + PartSys->sources[j].var = 4; // speed variation around vx,vy (+/- var) + } + } + } + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time + uint32_t emitparticles, frequency, baseangle, hueincrement; // number of particles to emit for each rocket's state + // variables for circular explosions + [[maybe_unused]] int32_t speed, currentspeed, speedvariation, percircle; + int32_t counter = 0; + [[maybe_unused]] uint16_t angle; + [[maybe_unused]] unsigned angleincrement; + bool circularexplosion = false; + + // emit particles for each rocket + for (uint32_t j = 0; j < numRockets; j++) { + // determine rocket state by its speed: + if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust + emitparticles = 1; + } + else if (PartSys->sources[j].source.vy < 0) { // falling down, standby time + emitparticles = 0; + } + else { // speed is zero, explode! + PartSys->sources[j].source.hue = hw_random16(); // random color + PartSys->sources[j].source.sat = hw_random16(55) + 200; + PartSys->sources[j].maxLife = 200; + PartSys->sources[j].minLife = 100; + PartSys->sources[j].source.ttl = hw_random16((2000 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch + PartSys->sources[j].var = ((SEGMENT.intensity >> 4) + 5); // speed variation around vx,vy (+/- var) + PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + #ifdef ESP8266 + emitparticles = hw_random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + #else + emitparticles = hw_random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion + #endif + + if (random16() & 1) { // 50% chance for circular explosion + circularexplosion = true; + speed = 2 + hw_random16(3) + ((SEGMENT.intensity >> 6)); + currentspeed = speed; + angleincrement = 2730 + hw_random16(5461); // minimum 15° + random(30°) + angle = hw_random16(); // random start angle + baseangle = angle; // save base angle for modulation + percircle = 0xFFFF / angleincrement + 1; // number of particles to make complete circles + hueincrement = hw_random16() & 127; // &127 is equivalent to %128 + int circles = 1 + hw_random16(3) + ((SEGMENT.intensity >> 6)); + frequency = hw_random16() & 127; // modulation frequency (= "waves per circle"), x.4 fixed point + emitparticles = percircle * circles; + PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random + } + } + uint32_t i; + for (i = 0; i < emitparticles; i++) { + if (circularexplosion) { + int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values + currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle + PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); // note: compiler warnings can be ignored, variables are set just above + counter++; + if (counter > percircle) { // full circle completed, increase speed + counter = 0; + speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave + PartSys->sources[j].source.hue += hueincrement; // new color for next circle + PartSys->sources[j].source.sat = 100 + hw_random16(156); + } + angle += angleincrement; // set angle for next particle + } + else { // random explosion or exhaust + PartSys->sprayEmit(PartSys->sources[j]); + if ((j % 3) == 0) { + PartSys->sources[j].source.hue = hw_random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) + } + } + } + if (i == 0) // no particles emitted, this rocket is falling + PartSys->sources[j].source.y = 1000; // reset position so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) + circularexplosion = false; // reset for next rocket + } + if (SEGMENT.check3) { // fast speed, move particles twice + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, nullptr); + } + } + PartSys->update(); // update and render +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,ix=50,c1=40,c2=0,c3=12"; + +/* + Particle Volcano + Particles are sprayed from below, spray moves back and forth if option is set + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 1 +void mode_particlevolcano(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D volcanosettings; + volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled + uint8_t numSprays; // note: so far only one tested but more is possible + uint32_t i = 0; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + FX_FALLBACK_STATIC; // allocation failed or not 2D + + PartSys->setBounceY(true); + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(230); // anable motion blur + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue = hw_random16(); + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 250; + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].sourceFlags.perpetual = true; // source never dies + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of volcanoes + + // change source emitting color from time to time, emit one particle per spray + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles (and update the sources) + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. + PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') + PartSys->sources[i].source.hue++; // = hw_random16(); //change hue of spray source (note: random does not look good) + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? (SEGMENT.custom1 >> 2) : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS + PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed (upwards) + PartSys->sources[i].vx = 0; + PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) + PartSys->sprayEmit(PartSys->sources[i]); + PartSys->setWallHardness(255); // full hardness for source bounce + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &volcanosettings); //move the source + } + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + PartSys->update(); // update and render +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,AgeColor,Walls,Collide;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1"; + +/* + Particle Fire + realistic fire effect using particles. heat based and using perlin-noise for wind + by DedeHai (Damian Schneider) +*/ +void mode_particlefire(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; // index variable + uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, SEGMENT.vWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) + FX_FALLBACK_STATIC; // allocation failed or not 2D + SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.check1 * 170); // anable/disable motion blur + PartSys->setSmearBlur(!SEGMENT.check1 * 60); // enable smear blur if motion blur is not enabled + + uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) + if (SEGMENT.speed < 100) { //slow, limit FPS + uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); + uint32_t period = strip.now - *lastcall; + if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) { // limit to 90FPS - 20FPS + SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) + return; //do not update this frame + } + *lastcall = strip.now; + } + + uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) + numFlames = min((uint32_t)PartSys->numSources, (4 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + uint32_t percycle = (numFlames * 2) / 3; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) + + // update the flame sprays: + for (i = 0; i < numFlames; i++) { + if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) { // every second frame + PartSys->sources[i].source.ttl--; + } else { // flame source is dead: initialize new flame: set properties of source + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + hw_random(spread); // change flame position: distribute randomly on chosen width + PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame + PartSys->sources[i].source.ttl = 20 + hw_random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].maxLife = hw_random16(SEGMENT.vHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; + PartSys->sources[i].vx = hw_random16(5) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards) + PartSys->sources[i].var = 2 + hw_random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) + } + } + + if (SEGMENT.call % 3 == 0) { // update noise position and add wind + SEGENV.aux0++; // position in the perlin noise matrix for wind generation + if (SEGMENT.call % 10 == 0) + SEGENV.aux1++; // move in noise y direction so noise does not repeat as often + // add wind force to all particles + int8_t windspeed = ((int16_t)(perlin8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; + PartSys->applyForce(windspeed, 0); + } + SEGENV.step++; + + if (SEGMENT.check3) { //add turbulance (parameters and algorithm found by experimentation) + if (SEGMENT.call % map(firespeed, 0, 255, 4, 15) == 0) { + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].y < PartSys->maxY / 4) { // do not apply turbulance everywhere -> bottom quarter seems a good balance + int32_t curl = ((int32_t)perlin8(PartSys->particles[i].x, PartSys->particles[i].y, SEGENV.step << 4) - 127); + PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; + } + } + } + } + + // emit faster sparks at first flame position, amount and speed mostly dependends on intensity + if(hw_random8() < 10 + (SEGMENT.intensity >> 2)) { + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl == 0) { // find a dead particle + PartSys->particles[i].ttl = hw_random16(SEGMENT.vHeight()) + 30; + PartSys->particles[i].x = PartSys->sources[0].source.x; + PartSys->particles[i].y = PartSys->sources[0].source.y; + PartSys->particles[i].vx = PartSys->sources[0].source.vx; + PartSys->particles[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards) + break; // emit only one particle + } + } + } + + uint8_t j = hw_random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) + for (i = 0; i < percycle; i++) { + j = (j + 1) % numFlames; + PartSys->flameEmit(PartSys->sources[j]); + } + + PartSys->updateFire(SEGMENT.intensity); // update and render the fire +} +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Flame Height,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; + +/* + PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce + sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation + this is quite versatile, can be made to look like rain or snow or confetti etc. + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particlepit(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 0, 0, true, false)) // init + FX_FALLBACK_STATIC; // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setGravity(); // enable with default gravity + PartSys->setUsedParticles(170); // use 75% of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy) + if (SEGMENT.custom2 > 0) + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + uint32_t i; + if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) { // every nth frame emit particles, stop emitting if set to zero + for (i = 0; i < PartSys->usedParticles; i++) { // emit particles + if (PartSys->particles[i].ttl == 0) { // find a dead particle + // emit particle at random position over the top of the matrix (random16 is not random enough) + PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + hw_random16(500); // if speed is higher, make them die sooner + PartSys->particles[i].x = hw_random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].y = (PartSys->maxY << 1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (int16_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- + PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed + PartSys->particles[i].hue = hw_random16(); // set random color + PartSys->particleFlags[i].collide = true; // enable collision for particle + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; + // set particle size + if (SEGMENT.custom1 == 255) { + PartSys->perParticleSize = true; + PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size + } else { + PartSys->perParticleSize = false; + PartSys->setParticleSize(SEGMENT.custom1); // set global size + PartSys->advPartProps[i].size = SEGMENT.custom1; // also set individual size for consistency + } + break; // emit only one particle per round + } + } + } + + uint32_t frictioncoefficient = 1 + SEGMENT.check1; //need more friction if wrapX is set, see below note + if (SEGMENT.speed < 50) // for low speeds, apply more friction + frictioncoefficient = 50 - SEGMENT.speed; + + if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) // note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled + PartSys->applyFriction(frictioncoefficient); + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=70,c2=180,c3=31,o3=1"; + +/* + Particle Waterfall + Uses palette for particle color, spray source at top emitting particles, many config options + by DedeHai (Damian Schneider) +*/ +void mode_particlewaterfall(void) { + ParticleSystem2D *PartSys = nullptr; + uint8_t numSprays; + uint32_t i = 0; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed + FX_FALLBACK_STATIC; // allocation failed or not 2D + + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(190); // anable motion blur + PartSys->setSmearBlur(30); // enable 2D blurring (smearing) + for (i = 0; i < PartSys->numSources; i++) { + PartSys->sources[i].source.hue = i*90; + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide + #ifdef ESP8266 + PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) + PartSys->sources[i].minLife = 100; + #else + PartSys->sources[i].maxLife = 400; // lifetime in frames + PartSys->sources[i].minLife = 150; + #endif + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); // cylinder + PartSys->setBounceX(SEGMENT.check2); // walls + PartSys->setBounceY(SEGMENT.check3); // ground + PartSys->setWallHardness(SEGMENT.custom2); + numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else { + PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(120); // set hardness (for ground bounce) to fixed value if not using collisions + } + + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue += 1 + hw_random16(SEGMENT.custom1>>1); // change hue of spray source + } + + if (SEGMENT.call % (12 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, emit particles, do not emit if intensity is zero + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + //PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix + PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32 + PartSys->sprayEmit(PartSys->sources[i]); + } + } + + if (SEGMENT.call % 20 == 0) + PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collide,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,o3=1"; + +/* + Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particlebox(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init + FX_FALLBACK_STATIC; // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + SEGENV.aux0 = hw_random16(); // position in perlin noise + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness + int maxParticleSize = min(((SEGMENT.vWidth() * SEGMENT.vHeight()) >> 2), 255U); // max particle size based on matrix size + unsigned currentParticleSize = map(SEGMENT.custom3, 0, 31, 0, maxParticleSize); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153) / (1 + (currentParticleSize >> 4))); // 1% - 60%, reduce if using larger size + if (SEGMENT.custom3 < 31) + PartSys->setParticleSize(currentParticleSize); // set global size if not max (resets perParticleSize) + else + PartSys->perParticleSize = true; // per particle size, uses advPartProps.size (randomized below) + + // add in new particles if amount has changed + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl < 260) { // initialize dead particles + PartSys->particles[i].ttl = 260; // full brigthness + PartSys->particles[i].x = hw_random16(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random8(); // make it colorful + PartSys->particleFlags[i].perpetual = true; // never die + PartSys->particleFlags[i].collide = true; // all particles colllide + PartSys->advPartProps[i].size = hw_random8(maxParticleSize); // random size, used only if size is set to max (SEGMENT.custom3=31) + break; // only spawn one particle per frame for less chaotic transitions + } + } + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) { // how often the force is applied depends on speed setting + int32_t xgravity; + int32_t ygravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + + if (SEGMENT.check2) { // washing machine + int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); + SEGENV.aux0 += speed; + if (speed == 0) SEGENV.aux0 = 190; //down (= 270°) + } + else + SEGENV.aux0 -= increment; + + if (SEGMENT.check1) { // random, use perlin noise + xgravity = ((int16_t)perlin8(SEGENV.aux0) - 127); + ygravity = ((int16_t)perlin8(SEGENV.aux0 + 10000) - 127); + // scale the gravity force + xgravity = (xgravity * SEGMENT.custom1) / 128; + ygravity = (ygravity * SEGMENT.custom1) / 128; + } + else { // go in a circle + xgravity = ((int32_t)(SEGMENT.custom1) * cos16_t(SEGENV.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1) * sin16_t(SEGENV.aux0 << 8)) / 0xFFFF; + } + if (SEGMENT.check3) { // sloshing, y force is always downwards + if (ygravity > 0) + ygravity = -ygravity; + } + + PartSys->applyForce(xgravity, ygravity); + } + + if ((SEGMENT.call & 0x0F) == 0) // every 16th frame + PartSys->applyFriction(1); + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Tilt,Hardness,Size,Random,Washing Machine,Sloshing;;!;2;pal=53,ix=50,c3=1,o1=1"; + +/* + Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above + calculates slope gradient at the particle positions and applies 'downhill' force, resulting in a fuzzy perlin noise display + by DedeHai (Damian Schneider) +*/ +void mode_particleperlin(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties + FX_FALLBACK_STATIC; // allocation failed or not 2D + + PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles + PartSys->setMotionBlur(230); // anable motion blur + PartSys->setBounceY(true); + SEGENV.aux0 = rand(); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(!SEGMENT.check1); + PartSys->setWallHardness(SEGMENT.custom1); // wall hardness + PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // min is 10%, max is 50% + PartSys->setSmearBlur(SEGMENT.check2 * 15); // enable 2D blurring (smearing) + + // apply 'gravity' from a 2D perlin noise map + SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position + // update position in noise + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl == 0) { // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) + PartSys->particles[i].ttl = hw_random16(500) + 200; + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random(PartSys->maxY); + PartSys->particleFlags[i].collide = true; // particle colllides + } + uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); + uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider + uint16_t ynoise = PartSys->particles[i].y / scale; + int16_t baseheight = perlin8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position + PartSys->particles[i].hue = baseheight; // color particles to perlin noise value + if (SEGMENT.call % 8 == 0) { // do not apply the force every frame, is too chaotic + int8_t xslope = (baseheight + (int16_t)perlin8(xnoise - 10, ynoise, SEGENV.aux0)); + int8_t yslope = (baseheight + (int16_t)perlin8(xnoise, ynoise - 10, SEGENV.aux0)); + PartSys->applyForce(i, xslope, yslope); + } + } + + if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) + PartSys->applyFriction(2); + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; + +/* + Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 8 +void mode_particleimpact(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t numMeteors; + PSsettings2D meteorsettings; + meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + FX_FALLBACK_STATIC; // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setGravity(); // enable default gravity + PartSys->setBounceY(true); // always use ground bounce + PartSys->setWallRoughness(220); // high roughness + numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (uint32_t i = 0; i < numMeteors; i++) { + PartSys->sources[i].source.ttl = hw_random16(10 * i); // set initial delay for meteors + PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom3<<3); + uint8_t hardness = map(SEGMENT.custom2, 0, 255, PS_P_MINSURFACEHARDNESS - 2, 255); + PartSys->setWallHardness(hardness); + PartSys->enableParticleCollisions(SEGMENT.check3, hardness); // enable collisions and set particle collision hardness + numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + uint32_t emitparticles; // number of particles to emit for each rocket's state + + for (uint32_t i = 0; i < numMeteors; i++) { + // determine meteor state by its speed: + if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks + emitparticles = 1; + else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' + emitparticles = 0; + else { // speed is zero, explode! + PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again + emitparticles = map(SEGMENT.intensity, 0, 255, 10, hw_random16(PartSys->usedParticles>>2)); // defines the size of the explosion + } + for (int e = emitparticles; e > 0; e--) { + PartSys->sprayEmit(PartSys->sources[i]); + } + } + + // update the meteors, set the speed state + for (uint32_t i = 0; i < numMeteors; i++) { + if (PartSys->sources[i].source.ttl) { + PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice + if (PartSys->sources[i].source.vy < 0) { // move down + PartSys->applyGravity(PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &meteorsettings); + + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) { // reached the bottom pixel on its way down + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].sourceFlags.collide = true; + #ifdef ESP8266 + PartSys->sources[i].maxLife = 900; + PartSys->sources[i].minLife = 100; + #else + PartSys->sources[i].maxLife = 1250; + PartSys->sources[i].minLife = 250; + #endif + PartSys->sources[i].source.ttl = hw_random16((768 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) + } + } + } + else if (PartSys->sources[i].source.vy > 0) { // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + // reinitialize meteor + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top + PartSys->sources[i].source.x = hw_random(PartSys->maxX); + PartSys->sources[i].source.vy = -hw_random16(30) - 30; // meteor downward speed + PartSys->sources[i].source.vx = hw_random16(50) - 25; // TODO: make this dependent on position so they do not move out of frame + PartSys->sources[i].source.hue = hw_random16(); // random color + PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom + PartSys->sources[i].sourceFlags.collide = false; // trail particles will not collide + PartSys->sources[i].maxLife = 300; // spark particle life + PartSys->sources[i].minLife = 100; + PartSys->sources[i].vy = -9; // emitting speed (down) + PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) + } + } + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 5) PartSys->particles[i].ttl -= 5; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + } + + PartSys->update(); // update and render +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,!,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o3=1"; + +/* + Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles + uses inverse square law like in planetary motion + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleattractor(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D sourcesettings; + sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSparticleFlags attractorFlags; + attractorFlags.asByte = 0; // no flags set + PSparticle *attractor; // particle pointer to the attractor + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, sizeof(PSparticle), true)) // init using 1 source and advanced particle settings + FX_FALLBACK_STATIC; // allocation failed or not 2D + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide + PartSys->sources[0].sourceFlags.perpetual = true; //source does not age + #ifdef ESP8266 + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].minLife = 30; + #else + PartSys->sources[0].maxLife = 350; // lifetime in frames + PartSys->sources[0].minLife = 50; + #endif + PartSys->sources[0].var = 4; // emiting variation + PartSys->setWallHardness(255); //bounce forever + PartSys->setWallRoughness(200); //randomize wall bounce + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190)); + attractor = reinterpret_cast(PartSys->PSdataEnd); + // set attractor properties + attractor->ttl = 100; // never dies + if (SEGMENT.check2) { + if ((SEGMENT.call % 3) == 0) // move slowly + PartSys->particleMoveUpdate(*attractor, attractorFlags, &sourcesettings); // move the attractor + } + else { + attractor->x = PartSys->maxX >> 1; // set to center + attractor->y = PartSys->maxY >> 1; + } + if (SEGMENT.call == 0) { + attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y + attractor->vy = PartSys->sources[0].source.vx; + } + + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + if (SEGMENT.call % 5 == 0) + PartSys->sources[0].source.hue++; + + SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) + if (SEGMENT.call % 2 == 0) // alternate direction of emit + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, 12); + else + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well + // apply force + uint32_t strength = SEGMENT.speed; + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // AR active, do not use simulated data + uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255 + strength = (SEGMENT.speed * volumeSmth) >> 8; + } + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3); + } + + + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) + PartSys->applyFriction(2); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); // move the source + PartSys->update(); // update and render +} +//static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=1,c2=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=2,c2=0"; + +/* + Particle Spray, just a particle spray with many parameters + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particlespray(void) { + ParticleSystem2D *PartSys = nullptr; + const uint8_t hardness = 200; // collision hardness is fixed + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed + FX_FALLBACK_STATIC; // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setBounceY(true); + PartSys->setMotionBlur(200); // anable motion blur + PartSys->setSmearBlur(10); // anable motion blur + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].var = 3; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounceX(!SEGMENT.check2); + PartSys->setWrapX(SEGMENT.check2); + PartSys->setWallHardness(hardness); + PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + //position according to sliders + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); + uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8; + + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data + uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 + uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 + PartSys->sources[0].minLife = 30; + + if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) { // defines interval of particle emit + PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames + PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed) >> 12); + uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeRaw >> 3); + PartSys->sources[0].source.hue += volumeSmth/30; + PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); + } + } + else { //no AR data, fall back to normal mode + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].maxLife = 300 + SEGMENT.intensity; // lifetime in frames + PartSys->sources[0].minLife = 150 + SEGMENT.intensity; + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + } + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collide;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21"; + + +/* + Particle base Graphical Equalizer + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleGEQ(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) + FX_FALLBACK_STATIC; // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setUsedParticles(170); // use 2/3 of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + uint32_t i; + // set particle system properties + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + //PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(SEGMENT.custom2); + PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) + uint32_t threshold = 300 - SEGMENT.intensity; + uint32_t emitparticles = 0; + + for (uint32_t bin = 0; bin < 16; bin++) { + uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band + uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) + emitparticles = 0; + + if (fftResult[bin] > threshold) { + emitparticles = 1;// + (fftResult[bin]>>6); + } + else if (fftResult[bin] > 0) { // band has low volue + uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; + if (hw_random16() % restvolume == 0) + emitparticles = 1; + } + + while (i < PartSys->usedParticles && emitparticles > 0) { // emit particles if there are any left, low frequencies take priority + if (PartSys->particles[i].ttl == 0) { // find a dead particle + //set particle properties TODO: could also use the spray... + PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + hw_random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + hw_random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width + PartSys->particles[i].y = 0; // start at the bottom + PartSys->particles[i].vx = hw_random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 + PartSys->particles[i].vy = emitspeed; + PartSys->particles[i].hue = (bin<<4) + hw_random16(17) - 8; // color from palette according to bin + emitparticles--; + } + i++; + } + } + + PartSys->update(); // update and render +} + +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS GEQ 2D@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; + +/* + Particle rotating GEQ + Particles sprayed from center with rotating spray + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 16 +void mode_particlecenterGEQ(void) { + ParticleSystem2D *PartSys = nullptr; + uint8_t numSprays; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources + FX_FALLBACK_STATIC; // allocation failed or not 2D + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].source.hue = i * 16; // even color distribution + PartSys->sources[i].maxLife = 400; + PartSys->sources[i].minLife = 200; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t threshold = 300 - SEGMENT.intensity; + + if (SEGMENT.check2) + SEGENV.aux0 += SEGMENT.custom1 << 2; + else + SEGENV.aux0 -= SEGMENT.custom1 << 2; + + uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; + uint32_t j = hw_random16(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + for (i = 0; i < numSprays; i++) { + if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) + PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); + + PartSys->sources[j].var = SEGMENT.custom3 >> 2; + int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed + 20)) >> 10); // emit speed according to loudness of band + uint16_t emitangle = j * angleoffset + SEGENV.aux0; + + uint32_t emitparticles = 0; + if (fftResult[j] > threshold) + emitparticles = 1; + else if (fftResult[j] > 0) { // band has low value + uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; + if (hw_random16() % restvolume == 0) + emitparticles = 1; + } + if (emitparticles) + PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); + + j = (j + 1) % numSprays; + } + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS GEQ Nova@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; + +/* + Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original FX by stepko adapted by Blaz Kristan (AKA blazoncek) +*/ +#define MAXANGLESTEP 2200 //32767 means 180° +void mode_particleghostrider(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D ghostsettings; + ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed + FX_FALLBACK_STATIC; // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].maxLife = 260; // lifetime in frames + PartSys->sources[0].minLife = 250; + PartSys->sources[0].source.x = hw_random16(PartSys->maxX); + PartSys->sources[0].source.y = hw_random16(PartSys->maxY); + SEGENV.step = hw_random16(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + if (SEGMENT.intensity > 0) { // spiraling + if (SEGENV.aux1) { + SEGENV.step += SEGMENT.intensity>>3; + if ((int32_t)SEGENV.step > MAXANGLESTEP) + SEGENV.aux1 = 0; + } + else { + SEGENV.step -= SEGMENT.intensity>>3; + if ((int32_t)SEGENV.step < -MAXANGLESTEP) + SEGENV.aux1 = 1; + } + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + PartSys->sources[0].var = SEGMENT.custom3 >> 1; + + // color by age (PS 'color by age' always starts with hue = 255, don't want that here) + if (SEGMENT.check1) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); + } + } + + // enable/disable walls + ghostsettings.bounceX = SEGMENT.check2; + ghostsettings.bounceY = SEGMENT.check2; + + SEGENV.aux0 += (int32_t)SEGENV.step; // step is angle increment + uint16_t emitangle = SEGENV.aux0 + 32767; // +180° + int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); + PartSys->sources[0].source.vx = ((int32_t)cos16_t(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vy = ((int32_t)sin16_t(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &ghostsettings); + // set head (steal one of the particles) + PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; + PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; + PartSys->particles[PartSys->usedParticles-1].ttl = 255; + PartSys->particles[PartSys->usedParticles-1].sat = 0; //white + // emit two particles + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].source.hue++; + } + if (SEGMENT.custom2 > 190) //fast color change + PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2; + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,AgeColor,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; + +/* + PS Blobs: large particles bouncing around, changing size and form + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleblobs(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { + if (!initParticleSystem2D(PartSys, 0, 0, true, true)) //init, no additional bytes, advanced size & size control + FX_FALLBACK_STATIC; // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + PartSys->setWallRoughness(255); + PartSys->setCollisionHardness(255); + PartSys->perParticleSize = true; // enable per particle size control + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // minimum 10%, maximum 50% of available particles (note: PS ensures at least 1) + PartSys->enableParticleCollisions(SEGMENT.check2); + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) { // speed changed or dead + PartSys->particles[i].vx = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // +/- speed/4 + PartSys->particles[i].vy = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); + } + if (SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + hw_random16((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size + + //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set + if (PartSys->particles[i].ttl == 0) { // find dead particle, renitialize + PartSys->particles[i].ttl = 300 + hw_random16(((uint16_t)SEGMENT.custom2 << 3) + 100); + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random16(); // set random color + PartSys->particleFlags[i].collide = true; // enable collision for particle + PartSys->advPartProps[i].size = 0; // start out small + PartSys->advPartSize[i].asymmetry = hw_random16(220); + PartSys->advPartSize[i].asymdir = hw_random16(255); + // set advanced size control properties + PartSys->advPartSize[i].grow = true; + PartSys->advPartSize[i].growspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].shrinkspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + hw_random16(3); + } + //PartSys->advPartSize[i].asymmetry++; + PartSys->advPartSize[i].pulsate = SEGMENT.check3; + PartSys->advPartSize[i].wobble = SEGMENT.check1; + } + SEGENV.aux0 = SEGMENT.speed; //write state back + SEGENV.aux1 = SEGMENT.custom1; + + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data if available, do not use simulated data + uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGMENT.check3) //pulsate selected + PartSys->advPartProps[i].size = volumeSmth; + } + } + + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; + +/* + Particle Galaxy, particles spiral like in a galaxy + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particlegalaxy(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D sourcesettings; + sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings + FX_FALLBACK_STATIC; // allocation failed or not 2D + PartSys->sources[0].source.vx = -4; // will collide with wall and get random bounce direction + PartSys->sources[0].source.x = PartSys->maxX >> 1; // start in the center + PartSys->sources[0].source.y = PartSys->maxY >> 1; + PartSys->sources[0].sourceFlags.perpetual = true; //source does not age + PartSys->sources[0].maxLife = 4000; // lifetime in frames + PartSys->sources[0].minLife = 800; + PartSys->sources[0].source.hue = hw_random16(); // start with random color + PartSys->setWallHardness(255); //bounce forever + PartSys->setWallRoughness(200); //randomize wall bounce + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + uint8_t particlesize = SEGMENT.custom1; + PartSys->setParticleSize(particlesize); // set size globally + PartSys->setMotionBlur(250 * SEGMENT.check3); // adds trails to single/quad pixel particles, no effect if size > 1 + + if ((SEGMENT.call % ((33 - SEGMENT.custom3) >> 1)) == 0) // change hue of emitted particles + PartSys->sources[0].source.hue+=2; + + if (hw_random8() < (10 + (SEGMENT.intensity >> 1))) // 5%-55% chance to emit a particle in this frame + PartSys->sprayEmit(PartSys->sources[0]); + + if ((SEGMENT.call & 0x3) == 0) // every 4th frame, move the emitter + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); + + // move alive particles in a spiral motion (or almost straight in fast starfield mode) + int32_t centerx = PartSys->maxX >> 1; // center of matrix in subpixel coordinates + int32_t centery = PartSys->maxY >> 1; + if (SEGMENT.check2) { // starfield mode + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].source.x = centerx; // set emitter to center + PartSys->sources[0].source.y = centery; + } + else { + PartSys->setKillOutOfBounds(false); + PartSys->sources[0].var = 1; // emiting variation + } + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles + if (PartSys->particles[i].ttl == 0) continue; //skip dead particles + // (dx/dy): vector pointing from particle to center + int32_t dx = centerx - PartSys->particles[i].x; + int32_t dy = centery - PartSys->particles[i].y; + //speed towards center: + int32_t distance = sqrt32_bw(dx * dx + dy * dy); // absolute distance to center + if (distance < 20) distance = 20; // avoid division by zero, keep a minimum + int32_t speedfactor; + if (SEGMENT.check2) { // starfield mode + speedfactor = 1 + (1 + (SEGMENT.speed >> 1)) * distance; // speed increases towards edge + //apply velocity + PartSys->particles[i].x += (-speedfactor * dx) / 400000 - (dy >> 6); + PartSys->particles[i].y += (-speedfactor * dy) / 400000 + (dx >> 6); + } + else { + speedfactor = 2 + (((50 + SEGMENT.speed) << 6) / distance); // speed increases towards center + // rotate clockwise + int32_t tempVx = (-speedfactor * dy); // speed is orthogonal to center vector + int32_t tempVy = (speedfactor * dx); + //add speed towards center to make particles spiral in + int vxc = (dx << 9) / (distance - 19); // subtract value from distance to make the pull-in force a bit stronger (helps on faster speeds) + int vyc = (dy << 9) / (distance - 19); + //apply velocity + PartSys->particles[i].x += (tempVx + vxc) / 1024; // note: cannot use bit shift as that causes asymmetric rounding + PartSys->particles[i].y += (tempVy + vyc) / 1024; + + if (distance < 128) { // close to center + if (PartSys->particles[i].ttl > 3) + PartSys->particles[i].ttl -= 4; //age fast + PartSys->particles[i].sat = distance << 1; // turn white towards center + } + } + if(SEGMENT.custom3 == 31) // color by age but mapped to 1024 as particles have a long life, since age is random, this gives more or less random colors + PartSys->particles[i].hue = PartSys->particles[i].ttl >> 2; + else if(SEGMENT.custom3 == 0) // color by distance + PartSys->particles[i].hue = map(distance, 20, (PartSys->maxX + PartSys->maxY) >> 2, 0, 180); // color by distance to center + } + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=1,c3=4"; + +#endif //WLED_DISABLE_PARTICLESYSTEM2D +#endif // WLED_DISABLE_2D + +/////////////////////////// +// 1D Particle System FX // +/////////////////////////// + +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +/* + Particle version of Drip and Rain + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleDrip(void) { + ParticleSystem1D *PartSys = nullptr; + //uint8_t numSprays; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4)) // init + FX_FALLBACK_STATIC; // allocation failed or single pixel + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].source.hue = hw_random16(); + SEGENV.aux1 = 0xFFFF; // invalidate + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(true); + PartSys->setWallHardness(50); + + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.custom3 >> 1); // set gravity (8 is default strength) + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + + if (SEGMENT.check2) { //collisions enabled + PartSys->enableParticleCollisions(true); //enable, full hardness + } + else + PartSys->enableParticleCollisions(false); + + PartSys->sources[0].sourceFlags.collide = false; //drops do not collide + + if (SEGMENT.check1) { //rain mode, emit at random position, short life (3-8 seconds at 50fps) + if (SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops + PartSys->setBounce(false); + PartSys->sources[0].var = 5; + PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) + // lifetime in frames + PartSys->sources[0].minLife = 30; + PartSys->sources[0].maxLife = 200; + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random emit position + } + else { //drip + PartSys->sources[0].var = 0; + PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) + PartSys->sources[0].minLife = 3000; + PartSys->sources[0].maxLife = 3000; + PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; + } + + if (SEGENV.aux1 != SEGMENT.intensity) //slider changed + SEGENV.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 + + SEGENV.aux1 = SEGMENT.intensity; // save state + + // every nth frame emit a particle + if (SEGMENT.call % SEGENV.aux0 == 0) { + int32_t interval = 300 / ((SEGMENT.intensity) + 1); + SEGENV.aux0 = interval + hw_random(interval + 5); + // if (SEGMENT.check1) // rain mode + // PartSys->sources[0].source.hue = 0; + // else + PartSys->sources[0].source.hue = hw_random8(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. + PartSys->sprayEmit(PartSys->sources[0]); + } + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles + if (PartSys->particles[i].ttl && PartSys->particleFlags[i].collide == false) { // use collision flag to identify splash particles + if (SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) { //splash enabled and reached bottom + PartSys->particles[i].ttl = 0; //kill origin particle + PartSys->sources[0].maxLife = 80; + PartSys->sources[0].minLife = 20; + PartSys->sources[0].var = 10 + (SEGMENT.custom1 >> 3); + PartSys->sources[0].v = 0; + PartSys->sources[0].source.hue = PartSys->particles[i].hue; + PartSys->sources[0].source.x = PS_P_RADIUS_1D; + PartSys->sources[0].sourceFlags.collide = true; //splashes do collide if enabled + for (int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) { + PartSys->sprayEmit(PartSys->sources[0]); + } + } + } + + if (SEGMENT.check1) { //rain mode, fade hue to max + if (PartSys->particles[i].hue < 245) + PartSys->particles[i].hue += 8; + } + //increase speed on high settings by calling the move function twice note: this can lead to missed collisions + if (SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); + } + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21"; + + +/* + Particle Version of "Bouncing Balls by Aircoookie" + Also does rolling balls and juggle (and popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particlePinball(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].source.x = -1000; // shoot up from below + //PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + SEGENV.aux0 = 1; + SEGENV.aux1 = 5000; // set settings out of range to ensure uptate on first call + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + //uint32_t hardness = 240 + (SEGMENT.custom1>>4); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 8)); // set gravity (8 is default strength) + PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->enableParticleCollisions(SEGMENT.check1, 255); // enable collisions and set particle collision to high hardness + PartSys->setColorByPosition(SEGMENT.check3); + uint32_t maxParticles = max(20, SEGMENT.intensity / (1 + (SEGMENT.check2 * (SEGMENT.custom1 >> 5)))); // max particles depends on intensity and rolling balls mode + size + if (SEGMENT.custom1 < 255) + PartSys->setParticleSize(SEGMENT.custom1); // set size globally + else { + PartSys->perParticleSize = true; // use random individual particle size (see below) + maxParticles *= 2; // use more particles if individual s ize is used as there is more space + } + PartSys->setUsedParticles(maxParticles); // reduce if using larger size and rolling balls mode + + bool updateballs = false; + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles) { // user settings change or more particles are available + SEGENV.step = SEGMENT.call; // reset delay + updateballs = true; + PartSys->sources[0].maxLife = SEGMENT.custom3 ? 1000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1; + } + + if (SEGMENT.check2) { // rolling balls + PartSys->setGravity(0); + PartSys->setWallHardness(255); + int speedsum = 0; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].ttl = 500; // keep particles alive + if (updateballs) { // speed changed or particle is dead, set particle properties + PartSys->particleFlags[i].collide = true; + if (PartSys->particles[i].x == 0) { // still at initial position + PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles + PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction + } + PartSys->particles[i].hue = hw_random8(); //set ball colors to random + PartSys->advPartProps[i].sat = 255; + PartSys->advPartProps[i].size = hw_random8(); // set ball size for individual size mode + } + speedsum += abs(PartSys->particles[i].vx); + } + int32_t avgSpeed = speedsum / PartSys->usedParticles; + int32_t setSpeed = 2 + (SEGMENT.speed >> 2); + if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going + for (int i = 0; i < setSpeed - avgSpeed; i++) { + int idx = hw_random16(PartSys->usedParticles); + if (abs(PartSys->particles[idx].vx) < PS_P_MAXSPEED) + PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction + } + } + else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down + PartSys->applyFriction(1); + } + else { // bouncing balls + PartSys->setWallHardness(220); + PartSys->sources[0].var = SEGMENT.speed >> 3; + int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); + PartSys->sources[0].v = newspeed; + //check for balls that are 'laying on the ground' and remove them + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl < 50) PartSys->particles[i].ttl = 0; // no dark particles + else if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1)) + PartSys->particles[i].ttl -= 50; // age fast + + if (updateballs) { + if (SEGMENT.custom3 == 0) // gravity off, update speed + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction + } + } + + // every nth frame emit a ball + if (SEGMENT.call > SEGENV.step) { + int interval = 260 - ((int)SEGMENT.intensity); + SEGENV.step += interval + hw_random16(interval); + PartSys->sources[0].source.hue = hw_random16(); //set ball color + PartSys->sources[0].sat = 255; + PartSys->sources[0].size = hw_random8(); //set ball size + PartSys->sprayEmit(PartSys->sources[0]); + } + } + SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles; + //for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + // PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed note: this leads to bad collisions, also need to run collision detection before + //} + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,ix=220,c2=0,c3=8,o1=1"; + +/* + Particle Replacement for original Dancing Shadows: + "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" + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleDancingShadows(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) // init, one source + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + if (SEGMENT.check1) + PartSys->setSmearBlur(120); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setColorByPosition(SEGMENT.check2); // color fixed by position + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); // set percentage of particles to use + + uint32_t deadparticles = 0; + //kill out of bounds and moving away plus change color + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame + if ((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it + } + PartSys->particleFlags[i].perpetual = true; //particles do not age + if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) + PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); + //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot + if (SEGENV.aux0 != SEGMENT.speed) { //speed changed + //update all particle speed by setting them to current value + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; + } + if (PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles + } + SEGENV.aux0 = SEGMENT.speed; + + //generate a spotlight: generates particles just outside of view + if (deadparticles > 5 && (SEGMENT.call & 0x03) == 0) { + //random color, random type + uint32_t type = hw_random16(SPOT_TYPES_COUNT); + int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 1)) + (SEGMENT.speed >> 4); + int32_t width = hw_random16(1, 10); + uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) + int32_t position; + //choose random start position, left and right from the segment + if (hw_random() & 0x01) { + position = PartSys->maxXpixel; + speed = -speed; + } + else + position = -width; + + PartSys->sources[0].v = speed; //emitted particle speed + PartSys->sources[0].source.hue = hw_random8(); //random spotlight color + for (int32_t i = 0; i < width; i++) { + if (width > 1) { + switch (type) { + case SPOT_TYPE_SOLID: + //nothing to do + break; + + case SPOT_TYPE_GRADIENT: + ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; //make gradient more pronounced + break; + + case SPOT_TYPE_2X_GRADIENT: + ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; + break; + + case SPOT_TYPE_2X_DOT: + if (i > 0) position++; //skip one pixel + i++; + break; + + case SPOT_TYPE_3X_DOT: + if (i > 0) position += 2; //skip two pixels + i+=2; + break; + + case SPOT_TYPE_4X_DOT: + if (i > 0) position += 3; //skip three pixels + i+=3; + break; + } + } + //emit particle + //set the particle source position: + PartSys->sources[0].source.x = position * PS_P_RADIUS_1D; + uint32_t partidx = PartSys->sprayEmit(PartSys->sources[0]); + PartSys->particles[partidx].ttl = ttl; + position++; //do the next pixel + } + } + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Position Color,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; + +/* + Particle Fireworks 1D replacement + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleFireworks1D(void) { + ParticleSystem1D *PartSys = nullptr; + uint8_t *forcecounter; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].sourceFlags.custom1 = 1; // set rocket state to standby + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + forcecounter = PartSys->PSdataEnd; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + int32_t gravity = (1 + (SEGMENT.speed >> 3)); // gravity value used for rocket speed calculation + PartSys->setGravity(SEGMENT.speed ? gravity : 0); // set gravity + + if (PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby + PartSys->sources[0].source.ttl--; + if (PartSys->sources[0].source.ttl == 0) { // time is up, relaunch + + if (hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true + SEGENV.aux0 = 1; + else + SEGENV.aux0 = 0; + + PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state + PartSys->sources[0].source.hue = hw_random16(); // different color for each launch + PartSys->sources[0].var = 10 * SEGMENT.check2; // emit variation, 0 if trail mode is off + PartSys->sources[0].v = -10 * SEGMENT.check2; // emit speed, 0 if trail mode is off + PartSys->sources[0].minLife = 180; + PartSys->sources[0].maxLife = SEGMENT.check2 ? 700 : 240; // exhaust particle life + PartSys->sources[0].source.x = SEGENV.aux0 * PartSys->maxX; // start from bottom or top + uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame + PartSys->sources[0].source.vx = min(speed, (uint32_t)127); + PartSys->sources[0].source.ttl = 4000; + PartSys->sources[0].sat = 30; // low saturation exhaust + PartSys->sources[0].size = SEGMENT.check3; // single or double pixel rendering + PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity + + if (SEGENV.aux0) { // inverted rockets launch from end + PartSys->sources[0].sourceFlags.reversegrav = true; + //PartSys->sources[0].source.x = PartSys->maxX; // start from top + PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction + PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed + } + } + } + else { // rocket is launched + int32_t rocketgravity = -gravity; + int32_t currentspeed = PartSys->sources[0].source.vx; + if (SEGENV.aux0) { // negative speed rocket + rocketgravity = -rocketgravity; + currentspeed = -currentspeed; + } + PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase rocket speed by calling the move function twice, also ages twice + uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; + + if (currentspeed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee + PartSys->sources[0].source.ttl = 50 - gravity;// min((uint32_t)50, 15 + (rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3))); // alive for a few more frames + + if (PartSys->sources[0].source.ttl < 2) { // explode + PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state + PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (20 + (SEGMENT.intensity << 1))) / (PartSys->maxX << 2)); // set explosion particle speed + PartSys->sources[0].minLife = 1200; + PartSys->sources[0].maxLife = 2600; + PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch + PartSys->sources[0].sat = SEGMENT.custom3 < 16 ? 10 + (SEGMENT.custom3 << 4) : 255; //color saturation + PartSys->sources[0].size = SEGMENT.check3 ? hw_random16(SEGMENT.intensity) : 0; // random particle size in explosion + uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); + explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); + PartSys->setColorByAge(false); // disable + PartSys->setColorByPosition(false); // disable + for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if(SEGMENT.custom3 > 23) { + if(SEGMENT.custom3 == 31) { // highest slider value + PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled + PartSys->setColorByPosition(!SEGMENT.check1); // color by position otherwise + } + else { // if custom3 is set to high value (but not highest), set particle color by initial speed + PartSys->particles[idx].hue = map(abs(PartSys->particles[idx].vx), 0, PartSys->sources[0].var, 0, 16 + hw_random16(200)); // set hue according to speed, use random amount of palette width + PartSys->particles[idx].hue += PartSys->sources[0].source.hue; // add hue offset of the rocket (random starting color) + } + } + else { + if (SEGMENT.check1) // colorful mode + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + } + } + } + } + if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby + PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle + + if ((SEGMENT.call & 0x03) == 0) // every fourth frame + PartSys->applyFriction(1); // apply friction to all particles + + PartSys->update(); // update and render + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } +} +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Color,Colorful,Trail,Smooth;,!;!;1;c2=30,o1=1"; + +/* + Particle based Sparkle effect + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleSparkler(void) { + ParticleSystem1D *PartSys = nullptr; + uint32_t numSparklers; + PSsettings1D sparklersettings; + sparklersettings.asByte = 0; // PS settings for sparkler (set below) + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 16, 128 ,0, true)) // init, no additional data needed + FX_FALLBACK_STATIC; // allocation failed or is single pixel + } else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + sparklersettings.wrap = !SEGMENT.check2; + sparklersettings.bounce = SEGMENT.check2; // note: bounce always takes priority over wrap + + numSparklers = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur/overlay + //PartSys->setSmearBlur(SEGMENT.custom2); // anable smearing blur + + for (uint32_t i = 0; i < numSparklers; i++) { + PartSys->sources[i].source.hue = hw_random16(); + PartSys->sources[i].var = 0; // sparks stationary + PartSys->sources[i].minLife = 150 + SEGMENT.intensity; + PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1); + int32_t speed = SEGMENT.speed >> 1; + if (SEGMENT.check1) // sparks move (slide option) + PartSys->sources[i].var = SEGMENT.intensity >> 3; + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? speed : -speed; // update speed, do not change direction + PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) + PartSys->sources[i].sat = SEGMENT.custom1; // color saturation + PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; + if (SEGMENT.speed == 255) // random position at highest speed setting + PartSys->sources[i].source.x = hw_random16(PartSys->maxX); + else + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler + } + + numSparklers = min(1 + (SEGMENT.custom3 >> 1), (int)numSparklers); // set used sparklers, 1 to 16 + + if (SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute + for (uint32_t i = 1; i < numSparklers; i++) { + PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly + } + } + SEGENV.aux0 = SEGMENT.custom3; + + for (uint32_t i = 0; i < numSparklers; i++) { + if (hw_random() % (((271 - SEGMENT.intensity) >> 4)) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + } + + PartSys->update(); // update and render + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } +} +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,c1=0,c2=0,c3=6"; + +/* + Particle based Hourglass, particles falling at defined intervals + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleHourglass(void) { + ParticleSystem1D *PartSys = nullptr; + constexpr int positionOffset = PS_P_RADIUS_1D / 2;; // resting position offset + bool* direction; + uint32_t* settingTracker; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 0, 255, 8, false)) // init + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->setBounce(true); + PartSys->setWallHardness(100); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + settingTracker = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + direction = reinterpret_cast(PartSys->PSdataEnd + 4); //assign data pointer + PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8)); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); + PartSys->enableParticleCollisions(true, 64); // hardness value (found by experimentation on different settings) + + uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 + + if (SEGMENT.intensity != *settingTracker) { // initialize + *settingTracker = SEGMENT.intensity; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].reversegrav = true; // resting particles dont fall + *direction = 0; // down + SEGENV.aux1 = 1; // initialize below + } + SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle + } + + // re-order particles in case heavy collisions flipped particles (highest number index particle is on the "bottom") + for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) { + if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) { + std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x); + } + } + // calculate target position depending on direction + auto calcTargetPos = [&](size_t i) { + return PartSys->particleFlags[i].reversegrav ? + PartSys->maxX - i * PS_P_RADIUS_1D - positionOffset + : (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; + }; + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling + if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) { + int32_t targetposition = calcTargetPos(i); + bool belowtarget = PartSys->particleFlags[i].reversegrav ? (PartSys->particles[i].x > targetposition) : (PartSys->particles[i].x < targetposition); + bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < PS_P_RADIUS_1D; + if (belowtarget || closeToTarget) { // overshot target or close to target and slow speed + PartSys->particles[i].x = targetposition; // set exact position + PartSys->particleFlags[i].fixed = true; // pin particle + } + } + if (colormode == 7) + PartSys->setColorByPosition(true); // color fixed by position + else { + PartSys->setColorByPosition(false); + uint8_t basehue = ((SEGMENT.custom1 & 0x1F) << 3); // use 5 LSBs to select color + switch(colormode) { + case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34) + case 1: PartSys->particles[i].hue = basehue; break; // fixed selectable color + case 2: // 2 colors inverleaved (same code as 3) + case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % 3)*74; break; // 3 interleved colors + case 4: PartSys->particles[i].hue = basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors + case 5: PartSys->particles[i].hue = basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors + case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient + default: break; // use color by position + } + } + if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen + PartSys->particles[i].hue += 120; + } + + if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].collide = true; + PartSys->particleFlags[i].perpetual = true; + PartSys->particles[i].ttl = 260; + PartSys->particles[i].x = calcTargetPos(i); + PartSys->particleFlags[i].fixed = true; + } + } + + if (SEGENV.aux1 == 0) { // countdown passed, run + if (strip.now >= SEGENV.step) { // drop a particle + // set next drop time + if (SEGMENT.check3 && *direction) // fast reset + SEGENV.step = strip.now + 100; // drop one particle every 100ms + else // normal interval + SEGENV.step = strip.now + max(100, SEGMENT.speed * 100); // map speed slider from 0.1s to 25.5s + if (SEGENV.aux0 < PartSys->usedParticles) { + PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise + PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin + } + else { // overflow + *direction = !(*direction); // flip direction + SEGENV.aux1 = (SEGMENT.check2) * SEGMENT.vLength() + 100; // set restart countdown, make it short if auto start is unchecked + } + if (*direction == 0) // down, start dropping the highest number particle + SEGENV.aux0--; // next particle + else + SEGENV.aux0++; + } + } + else if (SEGMENT.check2) // auto start/reset + SEGENV.aux1--; // countdown + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=5,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; + +/* + Particle based Spray effect (like a volcano, possible replacement for popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particle1Dspray(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->setWallHardness(150); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + int32_t gravity = -((int32_t)SEGMENT.custom3 - 16); // gravity setting, 0-15 is positive (down), 17 - 31 is negative (up) + PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) + + PartSys->sources[0].source.hue = SEGMENT.aux0; // hw_random16(); + PartSys->sources[0].var = 20; + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = 400; + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position + PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed + PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false; + + if (hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + SEGMENT.aux0++; // increment hue + } + + //update color settings + PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' + PartSys->setColorByPosition(SEGMENT.check3); + for (uint i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].reversegrav = PartSys->sources[0].sourceFlags.reversegrav; // update gravity direction + } + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0"; + +/* + Particle based balance: particles move back and forth (1D pendent to 2D particle box) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleBalance(void) { + ParticleSystem1D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128)) // init, no additional data needed, use half of max particles + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // enable motion blur + PartSys->setBounce(!SEGMENT.check2); + PartSys->setWrap(SEGMENT.check2); + uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled + PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 + PartSys->setWallHardness(200); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); + if (PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize + for (i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].x = i * PS_P_RADIUS_1D; + PartSys->particles[i].ttl = 300; + PartSys->particleFlags[i].perpetual = true; + PartSys->particleFlags[i].collide = true; + } + } + SEGENV.aux1 = PartSys->usedParticles; + + // re-order particles in case collisions flipped particles + for (i = 0; i < PartSys->usedParticles - 1; i++) { + if (PartSys->particles[i].x > PartSys->particles[i+1].x) { + if (SEGMENT.check2) { // check for wrap around + if (PartSys->particles[i].x - PartSys->particles[i+1].x > 3 * PS_P_RADIUS_1D) + continue; + } + std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x); + } + } + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting + int32_t xgravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + SEGENV.aux0 += increment; + if (SEGMENT.check3) // random, use perlin noise + xgravity = ((int16_t)perlin8(SEGENV.aux0) - 128); + else // sinusoidal + xgravity = (int16_t)cos8_t(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) + // scale the force + xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; // xgravity: -127 to +127 + PartSys->applyForce(xgravity); + } + + uint32_t randomindex = hw_random16(PartSys->usedParticles); + PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping + + //if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out + if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 4) // apply friction every 16th frame to smooth things out (except for low tilt) + PartSys->applyFriction(1); // apply friction to all particles + + //update colors + PartSys->setColorByPosition(SEGMENT.check1); + if (!SEGMENT.check1) { + for (i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue = (1024 * i) / PartSys->usedParticles; // color by particle index + } + } + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,c2=0,c3=4,o1=1"; + +/* +Particle based Chase effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +void mode_particleChase(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 191, 2, true)) // init + FX_FALLBACK_STATIC; // allocation failed or is single pixel + SEGENV.aux0 = 0xFFFF; // invalidate + *PartSys->PSdataEnd = 1; // huedir + *(PartSys->PSdataEnd + 1) = 1; // sizedir + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByPosition(SEGMENT.check3); + PartSys->setMotionBlur(7 + ((SEGMENT.custom3) << 3)); // anable motion blur + uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 0, PartSys->usedParticles / (1 + (SEGMENT.custom1 >> 5))); // depends on intensity and particle size (custom1), minimum 1 + numParticles = min(numParticles, PartSys->usedParticles); // limit to available particles + int32_t huestep = 1 + ((((uint32_t)SEGMENT.custom2 << 19) / numParticles) >> 16); // hue increment + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; + if (SEGENV.aux0 != settingssum) { // settings changed changed, update + if (SEGMENT.check1) + SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles); + else { + SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 6)) / numParticles; // spacing between particles + SEGENV.step = (SEGENV.step / PS_P_RADIUS_1D) * PS_P_RADIUS_1D; // round down to nearest multiple of particle subpixel unit to align to pixel grid (makes them move in union) + } + for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { + PartSys->advPartProps[i].sat = 255; + PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) + PartSys->particles[i].vx = SEGMENT.speed >> 2; + PartSys->advPartProps[i].size = SEGMENT.custom1; + if (SEGMENT.custom2 < 255) + PartSys->particles[i].hue = i * huestep; // gradient distribution + else + PartSys->particles[i].hue = hw_random16(); + } + SEGENV.aux0 = settingssum; + } + + if(SEGMENT.check1) { + huestep = 1 + (max((int)huestep, 3) * ((int(sin16_t(strip.now * 3) + 32767))) >> 15); // changes gradient spread (scale hue step) + } + + // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) + for (int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame + if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around + uint32_t nextindex = (i + 1) % PartSys->usedParticles; + PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; + if(SEGMENT.check1) // playful mode, vary size + PartSys->advPartProps[i].size = max(1 + (SEGMENT.custom1 >> 1), ((int(sin16_t(strip.now << 1) + 32767)) >> 8)); // cycle size + if (SEGMENT.custom2 < 255) + PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; + else + PartSys->particles[i].hue = hw_random16(); + } + PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time + } + + if (SEGMENT.check1) { // playful mode, changes hue, size, speed, density dynamically + int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + int8_t* stepdir = reinterpret_cast(PartSys->PSdataEnd + 1); + if(*stepdir == 0) *stepdir = 1; // initialize directions + if(*huedir == 0) *huedir = 1; + if (SEGENV.step >= (PartSys->advPartProps[0].size + PS_P_RADIUS_1D * 4) + PartSys->maxX / numParticles) + *stepdir = -1; // increase density (decrease space between particles) + else if (SEGENV.step <= (PartSys->advPartProps[0].size >> 1) + ((PartSys->maxX / numParticles))) + *stepdir = 1; // decrease density + if (SEGENV.aux1 > 512) + *huedir = -1; + else if (SEGENV.aux1 < 50) + *huedir = 1; + if (SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 2))) == 0) + SEGENV.aux1 += *huedir; + int8_t globalhuestep = 0; // global hue increment + if (SEGMENT.call % (1 + (int(sin16_t(strip.now) + 32767) >> 12)) == 0) + globalhuestep = 2; // global hue change to add some color variation + if ((SEGMENT.call & 0x1F) == 0) + SEGENV.step += *stepdir; // change density + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue -= globalhuestep; // shift global hue (both directions) + PartSys->particles[i].vx = 1 + (SEGMENT.speed >> 2) + ((int32_t(sin16_t(strip.now >> 1) + 32767) * (SEGMENT.speed >> 2)) >> 16); + } + } + + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,Playful,,Position Color;,!;!;1;pal=11,sx=50,c2=5,c3=0"; + +/* + Particle Fireworks Starburst replacement (smoother rendering, more settings) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleStarburst(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->enableParticleCollisions(true, 200); + PartSys->sources[0].source.ttl = 1; // set initial stanby time + PartSys->sources[0].sat = 0; // emitted particles start out white + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity + + if (PartSys->sources[0].source.ttl-- == 0) { // stanby time elapsed TODO: make it a timer? + uint32_t explosionsize = 4 + hw_random16(SEGMENT.intensity >> 2); + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].var = 10 + (explosionsize << 1); + PartSys->sources[0].minLife = 250; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random explosion position + PartSys->sources[0].source.ttl = 10 + hw_random16(255 - SEGMENT.speed); + PartSys->sources[0].size = SEGMENT.custom1; // Fragment size + PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering + PartSys->sources[0].sourceFlags.collide = SEGMENT.check3; + for (uint32_t e = 0; e < explosionsize; e++) { // emit particles + if (SEGMENT.check2) + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + } + } + //shrink all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->advPartProps[i].size) + PartSys->advPartProps[i].size--; + if (PartSys->advPartProps[i].sat < 251) + PartSys->advPartProps[i].sat += 1 + (SEGMENT.custom3 >> 2); //note: it should be >> 3, the >> 2 creates overflows resulting in blinking if custom3 > 27, which is a bonus feature + } + + if (SEGMENT.call % 5 == 0) { + PartSys->applyFriction(1); //slow down particles + } + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; + +/* + Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particle1DGEQ(void) { + ParticleSystem1D *PartSys = nullptr; + uint32_t numSources; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 16, 255, 0, true)) // init, no additional data needed + FX_FALLBACK_STATIC; // allocation failed or is single pixel + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSources = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + uint32_t spacing = PartSys->maxX / numSources; + for (i = 0; i < numSources; i++) { + PartSys->sources[i].source.hue = i * 16; // hw_random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].var = SEGMENT.speed >> 2; + PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); + PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; + PartSys->sources[i].sat = 255; + PartSys->sources[i].size = SEGMENT.custom1; + PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly + } + + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + else PartSys->particles[i].ttl = 0; + } + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t bin = hw_random16(numSources); //current bin , start with random one to distribute available particles fairly + uint32_t threshold = 300 - SEGMENT.intensity; + + for (i = 0; i < numSources; i++) { + bin++; + bin = bin % numSources; + uint32_t emitparticle = 0; + // uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) + if (fftResult[bin] > threshold) { + emitparticle = 1; + } + else if (fftResult[bin] > 0) { // band has low volue + uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2; + if (hw_random() % restvolume == 0) { + emitparticle = 1; + } + } + + if (emitparticle) + PartSys->sprayEmit(PartSys->sources[bin]); + } + //TODO: add color control? + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS GEQ 1D@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; + +/* + Particle based Fire effect + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +void mode_particleFire1D(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 5)) // init + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // enable motion blur + PartSys->setColorByAge(true); + uint32_t emitparticles = 1; + uint32_t j = hw_random16(); + for (uint i = 0; i < 3; i++) { // 3 base flames + if (PartSys->sources[i].source.ttl > 50) + PartSys->sources[i].source.ttl -= 10; // TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same + else + PartSys->sources[i].source.ttl = 100 + hw_random16(200); + } + for (uint i = 0; i < PartSys->numSources; i++) { + j = (j + 1) % PartSys->numSources; + PartSys->sources[j].source.x = 0; + PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4); + // base flames + if (j > 2) { + PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); // TODO: in 2D, min life is maxlife/2 and that looks very nice + PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3); + PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j << 1))); + if (emitparticles) { + emitparticles--; + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle + } + } + else { + PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; + PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; + PartSys->sources[j].v = SEGMENT.speed >> 2; + if (SEGENV.call & 0x01) // every second frame + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle + } + } + + for (uint i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].x += PartSys->particles[i].ttl >> 7; // 'hot' particles are faster, apply some extra velocity + if (PartSys->particles[i].ttl > 3 + ((255 - SEGMENT.custom1) >> 1)) + PartSys->particles[i].ttl -= map(SEGMENT.custom1, 0, 255, 1, 3); // age faster + } + + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; + +/* + Particle based AR effect, swoop particles along the strip with selected frequency loudness + by DedeHai (Damian Schneider) +*/ +void mode_particle1DsonicStream(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].source.x = 0; // at start + //PartSys->sources[1].source.x = PartSys->maxX; // at end + PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(20 + (SEGMENT.custom2 >> 1)); // anable motion blur + PartSys->setSmearBlur(200); // smooth out the edges + PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); + + // FFT processing + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t loudness; + uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); + + loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; + if (baseBin > 12) + loudness = loudness << 2; // double loudness for high frequencies (better detecion) + + uint32_t threshold = 140 - (SEGMENT.intensity >> 1); + if (SEGMENT.check2) { // enable low pass filter for dynamic threshold + SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold + threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold + } + + // color + uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 + PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white + PartSys->setColorByPosition(SEGMENT.custom1 == 255); + + // particle manipulation + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual + if (PartSys->particles[i].ttl > 2) { + PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + } + else PartSys->particles[i].ttl = 0; + } + if (SEGMENT.check1) { // modulate colors by mid frequencies + int mids = sqrt32_bw((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) + PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + } + } + + if (loudness > threshold) { + SEGMENT.aux0 += hueincrement; // change color + PartSys->sources[0].minLife = 100 + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); + PartSys->sources[0].maxLife = PartSys->sources[0].minLife; + PartSys->sources[0].source.hue = SEGMENT.aux0; + PartSys->sources[0].size = SEGMENT.speed; + if (PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead + int partindex = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if (partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle + } + } + else loudness = 0; // required for push mode + + PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) + + if (SEGMENT.check3) { // push mode + PartSys->sources[0].sourceFlags.perpetual = true; // emitted particles dont age + PartSys->applyFriction(1); //slow down particles + int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10; + if (movestep) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl) { + PartSys->particles[i].x += movestep; // push particles + PartSys->particles[i].vx = 10 + (SEGMENT.speed >> 4) ; // give particles some speed for smooth movement (friction will slow them down) + } + } + } + } + else { + PartSys->sources[0].sourceFlags.perpetual = false; // emitted particles age + // move all particles (again) to allow faster speeds + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].vx == 0) + PartSys->particles[i].vx = PartSys->sources[0].v; // move static particles (after disabling push mode) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, &PartSys->advPartProps[i]); + } + } +} +static const char _data_FX_MODE_PS_SONICSTREAM[] PROGMEM = "PS Sonic Stream@!,!,Color,Blur,Bin,Mod,Filter,Push;,!;!;1f;c3=0,o2=1"; + + +/* + Particle based AR effect, creates exploding particles on beats + by DedeHai (Damian Schneider) +*/ +void mode_particle1DsonicBoom(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + FX_FALLBACK_STATIC; // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(180 * SEGMENT.check3); + PartSys->setSmearBlur(64 * SEGMENT.check3); + PartSys->sources[0].var = map(SEGMENT.speed, 0, 255, 10, 127); + + // FFT processing + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t loudness; + uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); + loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; + + if (baseBin > 12) + loudness = loudness << 2; // double loudness for high frequencies (better detecion) + uint32_t threshold = 150 - (SEGMENT.intensity >> 1); + if (SEGMENT.check2) { // enable low pass filter for dynamic threshold + SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold + threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold + } + + // particle manipulation + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.check1) { // modulate colors by mid frequencies + int mids = sqrt32_bw((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) + PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + } + if (PartSys->particles[i].ttl > 16) { + PartSys->particles[i].ttl -= 16; //ttl is linked to brightness, this allows to use higher brightness but still a (very) short lifespan + } + } + + if (loudness > threshold) { + if (SEGMENT.aux1 == 0) { // edge detected, code only runs once per "beat" + // update position + if (SEGMENT.custom2 < 128) // fixed position + PartSys->sources[0].source.x = map(SEGMENT.custom2, 0, 127, 0, PartSys->maxX); + else if (SEGMENT.custom2 < 255) { // advances on each "beat" + int32_t step = PartSys->maxX / (((270 - SEGMENT.custom2) >> 3)); // step: 2 - 33 steps for full segment width + PartSys->sources[0].source.x = (PartSys->sources[0].source.x + step) % PartSys->maxX; + if (PartSys->sources[0].source.x < step) // align to be symmetrical by making the first position half a step from start + PartSys->sources[0].source.x = step >> 1; + } + else // position set to max, use random postion per beat + PartSys->sources[0].source.x = hw_random(PartSys->maxX); + + // update color + //PartSys->setColorByPosition(SEGMENT.custom1 == 255); // color slider at max: particle color by position + PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white + if (SEGMENT.custom1 == 255) // emit color by position + SEGMENT.aux0 = map(PartSys->sources[0].source.x , 0, PartSys->maxX, 0, 255); + else if (SEGMENT.custom1 > 0) + SEGMENT.aux0 += (SEGMENT.custom1 >> 1); // change emit color per "beat" + } + SEGMENT.aux1 = 1; // track edge detection + + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = PartSys->sources[0].minLife + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); + PartSys->sources[0].source.hue = SEGMENT.aux0; + PartSys->sources[0].size = 1; //SEGMENT.speed>>3; + uint32_t explosionsize = 4 + (PartSys->maxXpixel >> 2); + explosionsize = hw_random16((explosionsize * loudness) >> 10); + for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + } + } + else + SEGMENT.aux1 = 0; // reset edge detection + + PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) +} +static const char _data_FX_MODE_PS_SONICBOOM[] PROGMEM = "PS Sonic Boom@!,!,Color,Position,Bin,Mod,Filter,Blur;,!;!;1f;c2=63,c3=0,o2=1"; + +/* +Particles bound by springs +by DedeHai (Damian Schneider) +*/ +void mode_particleSpringy(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init with advanced properties (used for spring forces) + FX_FALLBACK_STATIC; // allocation failed or is single pixel + SEGENV.aux0 = SEGENV.aux1 = 0xFFFF; // invalidate settings + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + FX_FALLBACK_STATIC; // something went wrong, no data! + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(220 * SEGMENT.check1); // anable motion blur + PartSys->setSmearBlur(50); // smear a little + PartSys->setUsedParticles(map(SEGMENT.custom1, 0, 255, 30 >> SEGMENT.check2, 255 >> (SEGMENT.check2*2))); // depends on density and particle size + //PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft. + int32_t springlength = PartSys->maxX / (PartSys->usedParticles); // spring length (spacing between particles) + int32_t springK = map(SEGMENT.speed, 0, 255, 5, 35); // spring constant (stiffness) + + uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2; + PartSys->setParticleSize(SEGMENT.check2 ? 120 : 1); // large or small particles + + if (SEGENV.aux0 != settingssum) { // number of particles changed, update distribution + for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { + PartSys->advPartProps[i].sat = 255; // full saturation + //PartSys->particleFlags[i].collide = true; // enable collision for particles -> results in chaos, removed for now + PartSys->particles[i].x = (i+1) * ((PartSys->maxX) / (PartSys->usedParticles)); // distribute + //PartSys->particles[i].vx = 0; //reset speed + //PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big -> use global size + } + SEGENV.aux0 = settingssum; + } + int dxlimit = (2 + ((255 - SEGMENT.speed) >> 5)) * springlength; // limit for spring length to avoid overstretching + + int springforce[PartSys->usedParticles]; // spring forces + memset(springforce, 0, PartSys->usedParticles * sizeof(int32_t)); // reset spring forces + + // calculate spring forces and limit particle positions + if (PartSys->particles[0].x < -springlength) + PartSys->particles[0].x = -springlength; // limit the spring length + else if (PartSys->particles[0].x > dxlimit) + PartSys->particles[0].x = dxlimit; // limit the spring length + springforce[0] += ((springlength >> 1) - (PartSys->particles[0].x)) * springK; // first particle anchors to x=0 + + for (uint32_t i = 1; i < PartSys->usedParticles; i++) { + // reorder particles if they are out of order to prevent chaos + if (PartSys->particles[i].x < PartSys->particles[i-1].x) + std::swap(PartSys->particles[i].x, PartSys->particles[i-1].x); // swap particle positions to maintain order + int dx = PartSys->particles[i].x - PartSys->particles[i-1].x; // distance, always positive + if (dx > dxlimit) { // limit the spring length + PartSys->particles[i].x = PartSys->particles[i-1].x + dxlimit; + dx = dxlimit; + } + int dxleft = (springlength - dx); // offset from spring resting position + springforce[i] += dxleft * springK; + springforce[i-1] -= dxleft * springK; + if (i == (PartSys->usedParticles - 1)) { + if (PartSys->particles[i].x >= PartSys->maxX + springlength) + PartSys->particles[i].x = PartSys->maxX + springlength; + int dxright = (springlength >> 1) - (PartSys->maxX - PartSys->particles[i].x); // last particle anchors to x=maxX + springforce[i] -= dxright * springK; + } + } + // apply spring forces to particles + bool dampenoscillations = (SEGMENT.call % (9 - (SEGMENT.speed >> 5))) == 0; // dampen oscillation if particles are slow, more damping on stiffer springs + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + springforce[i] = springforce[i] / 64; // scale spring force (cannot use shifts because of negative values) + int maxforce = 120; // limit spring force + springforce[i] = springforce[i] > maxforce ? maxforce : springforce[i] < -maxforce ? -maxforce : springforce[i]; // limit spring force + PartSys->applyForce(PartSys->particles[i], springforce[i], PartSys->advPartProps[i].forcecounter); + //dampen slow particles to avoid persisting oscillations on higher stiffness + if (dampenoscillations) { + if (abs(PartSys->particles[i].vx) < 3 && abs(springforce[i]) < (springK >> 2)) + PartSys->particles[i].vx = (PartSys->particles[i].vx * 254) / 256; // take out some energy + } + PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual + } + + if (SEGMENT.call % ((65 - ((SEGMENT.intensity * (1 + (SEGMENT.speed>>3))) >> 7))) == 0) // more damping for higher stiffness + PartSys->applyFriction((SEGMENT.intensity >> 2)); + + // add a small resetting force so particles return to resting position even under high damping + for (uint32_t i = 1; i < PartSys->usedParticles - 1; i++) { + int restposition = (springlength >> 1) + i * springlength; // resting position + int dx = restposition - PartSys->particles[i].x; // distance, always positive + PartSys->applyForce(PartSys->particles[i], dx > 0 ? 1 : (dx < 0 ? -1 : 0), PartSys->advPartProps[i].forcecounter); + } + + // Modes + if (SEGMENT.check3) { // use AR, custom 3 becomes frequency band to use, applies velocity to center particle according to loudness + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t baseBin = map(SEGMENT.custom3, 0, 31, 0, 14); + uint32_t loudness = fftResult[baseBin] + fftResult[baseBin+1]; + uint32_t threshold = 80; //150 - (SEGMENT.intensity >> 1); + if (loudness > threshold) { + int offset = (PartSys->maxX >> 1) - PartSys->particles[PartSys->usedParticles>>1].x; // offset from center + if (abs(offset) < PartSys->maxX >> 5) // push particle around in center sector + PartSys->particles[PartSys->usedParticles>>1].vx = ((PartSys->particles[PartSys->usedParticles>>1].vx > 0 ? 1 : -1)) * (loudness >> 3); + } + } + else{ + if (SEGMENT.custom3 <= 10) { // periodic pulse: 0-5 apply at start, 6-10 apply at center + if (strip.now > SEGMENT.step) { + int speed = (SEGMENT.custom3 > 5) ? (SEGMENT.custom3 - 6) : SEGMENT.custom3; + SEGMENT.step = strip.now + 7500 - ((SEGMENT.speed << 3) + (speed << 10)); + int amplitude = 40 + (SEGMENT.custom1 >> 2); + int index = (SEGMENT.custom3 > 5) ? (PartSys->usedParticles / 2) : 0; // center or start particle + PartSys->particles[index].vx += amplitude; + } + } + else if (SEGMENT.custom3 <= 30) { // sinusoidal wave: 11-20 apply at start, 21-30 apply at center + int index = (SEGMENT.custom3 > 20) ? (PartSys->usedParticles / 2) : 0; // center or start particle + int restposition = 0; + if (index > 0) restposition = PartSys->maxX >> 1; // center + //int amplitude = 5 + (SEGMENT.speed >> 3) + (SEGMENT.custom1 >> 2); // amplitude depends on density + int amplitude = 5 + (SEGMENT.custom1 >> 2); // amplitude depends on density + int speed = SEGMENT.custom3 - 10 - (index ? 10 : 0); // map 11-20 and 21-30 to 1-10 + int phase = strip.now * ((1 + (SEGMENT.speed >> 4)) * speed); + if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles + PartSys->particles[index].x = restposition + ((sin16_t(phase) * amplitude) >> 12); // apply position + } + else { + if (hw_random16() < 656) { // ~1% chance to add a pulse + int amplitude = 60; + if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles + PartSys->particles[PartSys->usedParticles >> 1].vx += hw_random16(amplitude << 1) - amplitude; // apply acceleration + } + } + } + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.custom2 == 255) { // map speed to hue + int speedclr = ((int8_t(abs(PartSys->particles[i].vx))) >> 2) << 4; // scale for greater color variation, dump small values to avoid flickering + //int speed = PartSys->particles[i].vx << 2; // +/- 512 + if (speedclr > 240) speedclr = 240; // limit color to non-wrapping part of palette + PartSys->particles[i].hue = speedclr; + } + else if (SEGMENT.custom2 > 0) + PartSys->particles[i].hue = i * (SEGMENT.custom2 >> 2); // gradient distribution + else { + // map hue to particle density + int deviation; + if (i == 0) // First particle: measure density based on distance to anchor point + deviation = springlength/2 - PartSys->particles[i].x; + else if (i == PartSys->usedParticles - 1) // Last particle: measure density based on distance to right boundary + deviation = springlength/2 - (PartSys->maxX - PartSys->particles[i].x); + else { + // Middle particles: average of compression/expansion from both sides + int leftDx = PartSys->particles[i].x - PartSys->particles[i-1].x; + int rightDx = PartSys->particles[i+1].x - PartSys->particles[i].x; + int avgDistance = (leftDx + rightDx) >> 1; + if (avgDistance < 0) avgDistance = 0; // avoid negative distances (not sure why this happens) + deviation = (springlength - avgDistance); + } + deviation = constrain(deviation, -127, 112); // limit deviation to -127..112 (do not go intwo wrapping part of palette) + PartSys->particles[i].hue = 127 + deviation; // map density to hue + } + } + PartSys->update(); // update and render +} +static const char _data_FX_MODE_PS_SPRINGY[] PROGMEM = "PS Springy@Stiffness,Damping,Density,Hue,Mode,Smear,XL,AR;,!;!;1f;pal=54,c2=0,c3=23"; + +#endif // WLED_DISABLE_PARTICLESYSTEM1D + +////////////////////////////////////////////////////////////////////////////////////////// +// mode data +static const char _data_RESERVED[] PROGMEM = "RSVD"; + +// add (or replace reserved) effect mode and data into vector +// use id==255 to find unallocated gaps (with "Reserved" data string) +// if vector size() is smaller than id (single) data is appended at the end (regardless of id) +// return the actual id used for the effect or 255 if the add failed. +uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { + if (id == 255) { // find empty slot + for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } + } + if (id < _mode.size()) { + if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect + _mode[id] = mode_fn; + _modeData[id] = mode_name; + return id; + } else if (_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added + _mode.push_back(mode_fn); + _modeData.push_back(mode_name); + if (_modeCount < _mode.size()) _modeCount++; + return _mode.size() - 1; + } else { + return 255; // The vector is full so return 255 + } +} + +void WS2812FX::setupEffectData() { + // Solid must be first! (assuming vector is empty upon call to setup) + _mode.push_back(&mode_static); + _modeData.push_back(_data_FX_MODE_STATIC); + // fill reserved word in case there will be any gaps in the array + for (size_t i=1; i<_modeCount; i++) { + _mode.push_back(&mode_static); + _modeData.push_back(_data_RESERVED); + } + // now replace all pre-allocated effects + addEffect(FX_MODE_COPY, &mode_copy_segment, _data_FX_MODE_COPY); + // --- 1D non-audio effects --- + addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); + addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); + addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); + addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); + addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); + addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); + addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); + addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW); + addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE); + addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN); + addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN); + addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE); + addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE); + addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW); + addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS); + addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW); + addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); + addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); + addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); + addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); + addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); + addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); + addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW); + addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE); + addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW); + addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID); + addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR); + addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM); + addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW); + addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH); + addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM); + addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE); + addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL); + addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT); + addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM); + addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR); + addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); + addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); + addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); + addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); + addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); + addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); + addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); + addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); + addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); + addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); + addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); + addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); + addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); + addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); + addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); + #ifdef WLED_ENABLE_GIF + addEffect(FX_MODE_IMAGE, &mode_image, _data_FX_MODE_IMAGE); + #endif + addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); + addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); + addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); + addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); + addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); + addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); + addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); + addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); + addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); + addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); + addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); + addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); + addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); + addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2); + addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3); + addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4); + addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); + addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); + addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); + //addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); // merged with mode_meteor + addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); + addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); + addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); + addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT); + addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES); + addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN); + addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); + addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); + addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); + addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); + #if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX) + addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); + addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); + #endif + addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); + addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); + addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); + addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); + #ifdef WLED_PS_DONT_REPLACE_1D_FX + addEffect(FX_MODE_ROLLINGBALLS, &mode_rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + #endif + addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); + addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); + addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); + addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); + addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); + addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); + addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); + addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); + addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); + addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); + addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); + addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); + addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); + addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); + addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); + addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); + addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL); + addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); + addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); + addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); + addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); + addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); + addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); + addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); + addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + addEffect(FX_MODE_PACMAN, &mode_pacman, _data_FX_MODE_PACMAN); + + // --- 1D audio effects --- + addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); + addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); + addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); + addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); + addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); + addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID); + addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES); + addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE); + addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); + addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); + addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); + addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); + addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); + addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); + addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); + addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); + addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); + addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); + addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); - addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); - addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); - addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); + addEffect(FX_MODE_SHIMMER, &mode_shimmer, _data_FX_MODE_SHIMMER); // --- 2D effects --- #ifndef WLED_DISABLE_2D addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); + + #ifdef WLED_PS_DONT_REPLACE_2D_FX addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + #endif + addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); - addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio - addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE); - addEffect(FX_MODE_2DFIRENOISE, &mode_2Dfirenoise, _data_FX_MODE_2DFIRENOISE); addEffect(FX_MODE_2DSQUAREDSWIRL, &mode_2Dsquaredswirl, _data_FX_MODE_2DSQUAREDSWIRL); @@ -8002,15 +10934,12 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DMATRIX, &mode_2Dmatrix, _data_FX_MODE_2DMATRIX); addEffect(FX_MODE_2DMETABALLS, &mode_2Dmetaballs, _data_FX_MODE_2DMETABALLS); addEffect(FX_MODE_2DFUNKYPLANK, &mode_2DFunkyPlank, _data_FX_MODE_2DFUNKYPLANK); // audio - addEffect(FX_MODE_2DPULSER, &mode_2DPulser, _data_FX_MODE_2DPULSER); - addEffect(FX_MODE_2DDRIFT, &mode_2DDrift, _data_FX_MODE_2DDRIFT); addEffect(FX_MODE_2DWAVERLY, &mode_2DWaverly, _data_FX_MODE_2DWAVERLY); // audio addEffect(FX_MODE_2DSUNRADIATION, &mode_2DSunradiation, _data_FX_MODE_2DSUNRADIATION); addEffect(FX_MODE_2DCOLOREDBURSTS, &mode_2DColoredBursts, _data_FX_MODE_2DCOLOREDBURSTS); addEffect(FX_MODE_2DJULIA, &mode_2DJulia, _data_FX_MODE_2DJULIA); - addEffect(FX_MODE_2DGAMEOFLIFE, &mode_2Dgameoflife, _data_FX_MODE_2DGAMEOFLIFE); addEffect(FX_MODE_2DTARTAN, &mode_2Dtartan, _data_FX_MODE_2DTARTAN); addEffect(FX_MODE_2DPOLARLIGHTS, &mode_2DPolarLights, _data_FX_MODE_2DPOLARLIGHTS); @@ -8018,7 +10947,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DLISSAJOUS, &mode_2DLissajous, _data_FX_MODE_2DLISSAJOUS); addEffect(FX_MODE_2DFRIZZLES, &mode_2DFrizzles, _data_FX_MODE_2DFRIZZLES); addEffect(FX_MODE_2DPLASMABALL, &mode_2DPlasmaball, _data_FX_MODE_2DPLASMABALL); - addEffect(FX_MODE_2DHIPHOTIC, &mode_2DHiphotic, _data_FX_MODE_2DHIPHOTIC); addEffect(FX_MODE_2DSINDOTS, &mode_2DSindots, _data_FX_MODE_2DSINDOTS); addEffect(FX_MODE_2DDNASPIRAL, &mode_2DDNASpiral, _data_FX_MODE_2DDNASPIRAL); @@ -8026,8 +10954,44 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DSOAP, &mode_2Dsoap, _data_FX_MODE_2DSOAP); addEffect(FX_MODE_2DOCTOPUS, &mode_2Doctopus, _data_FX_MODE_2DOCTOPUS); addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); - addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D + addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); + addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); + addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); + addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); + addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); + addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT); + addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); // 872 bytes + addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); + addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); + addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); + addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ); + addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); + addEffect(FX_MODE_PARTICLEGALAXY, &mode_particlegalaxy, _data_FX_MODE_PARTICLEGALAXY); +#endif // WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); +addEffect(FX_MODE_PSPINBALL, &mode_particlePinball, _data_FX_MODE_PSPINBALL); //potential replacement for: bouncing balls, rollingballs, popcorn +addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); +addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); +addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER); +addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS); +addEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY); +addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); +addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); +addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); +addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); +addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); +addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1DsonicStream, _data_FX_MODE_PS_SONICSTREAM); +addEffect(FX_MODE_PS1DSONICBOOM, &mode_particle1DsonicBoom, _data_FX_MODE_PS_SONICBOOM); +addEffect(FX_MODE_PS1DSPRINGY, &mode_particleSpringy, _data_FX_MODE_PS_SPRINGY); +#endif // WLED_DISABLE_PARTICLESYSTEM1D + } diff --git a/wled00/FX.h b/wled00/FX.h index 5451615464..9c5291665c 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -1,3 +1,4 @@ +#pragma once /* WS2812FX.h - Library for WS2812 LED effects. Harm Aldick - 2016 @@ -8,14 +9,34 @@ Adapted from code originally licensed under the MIT license Modified for WLED + + Segment class/struct (c) 2022 Blaz Kristan (@blazoncek) */ #ifndef WS2812FX_h #define WS2812FX_h #include +#include "wled.h" -#include "const.h" +#ifdef WLED_DEBUG + // enable additional debug output + #if defined(WLED_DEBUG_HOST) + #include "net_debug.h" + #define DEBUGOUT NetDebug + #else + #define DEBUGOUT Serial + #endif + #define DEBUGFX_PRINT(x) DEBUGOUT.print(x) + #define DEBUGFX_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUGFX_PRINTF(x...) DEBUGOUT.printf(x) + #define DEBUGFX_PRINTF_P(x...) DEBUGOUT.printf_P(x) +#else + #define DEBUGFX_PRINT(x) + #define DEBUGFX_PRINTLN(x) + #define DEBUGFX_PRINTF(x...) + #define DEBUGFX_PRINTF_P(x...) +#endif #define FASTLED_INTERNAL //remove annoying pragma messages #define USE_GET_MILLISECOND_TIMER @@ -42,10 +63,21 @@ #define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) #endif +extern bool realtimeRespectLedMaps; // used in getMappedPixelIndex() +extern byte realtimeMode; // used in getMappedPixelIndex() + /* Not used in all effects yet */ #define WLED_FPS 42 #define FRAMETIME_FIXED (1000/WLED_FPS) #define FRAMETIME strip.getFrameTime() +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S2) + #define MIN_FRAME_DELAY 2 // minimum wait between repaints, to keep other functions like WiFi alive +#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + #define MIN_FRAME_DELAY 3 // S2/C3 are slower than normal esp32, and only have one core +#else + #define MIN_FRAME_DELAY 8 // 8266 legacy MIN_SHOW_DELAY +#endif +#define FPS_UNLIMITED 0 // FPS calculation (can be defined as compile flag for debugging) #ifndef FPS_CALC_AVG @@ -56,37 +88,37 @@ #endif #define FPS_CALC_SHIFT 7 // bit shift for fixed point math -/* each segment uses 82 bytes of SRAM memory, so if you're application fails because of - insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ +// heap memory limit for effects data, pixel buffers try to reserve it if PSRAM is available #ifdef ESP8266 - #define MAX_NUM_SEGMENTS 16 + #define MAX_NUM_SEGMENTS 16 /* How much data bytes all segments combined may allocate */ - #define MAX_SEGMENT_DATA 5120 + #define MAX_SEGMENT_DATA (6*1024) // 6k by default +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + #define MAX_NUM_SEGMENTS 32 + #define MAX_SEGMENT_DATA (20*1024) // 20k by default (S2 is short on free RAM), limit does not apply if PSRAM is available #else - #ifndef MAX_NUM_SEGMENTS - #define MAX_NUM_SEGMENTS 32 - #endif - #if defined(ARDUINO_ARCH_ESP32S2) - #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*768 // 24k by default (S2 is short on free RAM) + #ifdef BOARD_HAS_PSRAM + #define MAX_NUM_SEGMENTS 64 #else - #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*1280 // 40k by default + #define MAX_NUM_SEGMENTS 32 #endif + #define MAX_SEGMENT_DATA (64*1024) // 64k by default, limit does not apply if PSRAM is available #endif /* How much data bytes each segment should max allocate to leave enough space for other segments, assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ -#define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / strip.getMaxSegments()) +#define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / MAX_NUM_SEGMENTS) #define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) #define NUM_COLORS 3 /* number of colors per segment */ -#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 SEGMENT (*strip._currentSegment) +#define SEGENV (*strip._currentSegment) +#define SEGCOLOR(x) Segment::getCurrentColor(x) #define SEGPALETTE Segment::getCurrentPalette() -#define SEGLEN strip._virtualSegmentLength /* saves us a few kbytes of code */ +#define SEGLEN Segment::vLength() +#define SEG_W Segment::vWidth() +#define SEG_H Segment::vHeight() #define SPEED_FORMULA_L (5U + (50U*(255U - SEGMENT.speed))/SEGLEN) // some common colors @@ -132,10 +164,10 @@ #define FX_MODE_RAINBOW 8 #define FX_MODE_RAINBOW_CYCLE 9 #define FX_MODE_SCAN 10 -#define FX_MODE_DUAL_SCAN 11 +#define FX_MODE_DUAL_SCAN 11 // candidate for removal (use Scan) #define FX_MODE_FADE 12 #define FX_MODE_THEATER_CHASE 13 -#define FX_MODE_THEATER_CHASE_RAINBOW 14 +#define FX_MODE_THEATER_CHASE_RAINBOW 14 // candidate for removal (use Theater) #define FX_MODE_RUNNING_LIGHTS 15 #define FX_MODE_SAW 16 #define FX_MODE_TWINKLE 17 @@ -158,7 +190,7 @@ #define FX_MODE_COLORFUL 34 #define FX_MODE_TRAFFIC_LIGHT 35 #define FX_MODE_COLOR_SWEEP_RANDOM 36 -#define FX_MODE_RUNNING_COLOR 37 +#define FX_MODE_RUNNING_COLOR 37 // candidate for removal (use Theater) #define FX_MODE_AURORA 38 #define FX_MODE_RUNNING_RANDOM 39 #define FX_MODE_LARSON_SCANNER 40 @@ -173,8 +205,8 @@ #define FX_MODE_FAIRY 49 //was Police All prior to 0.13.0-b6 (use "Two Dots" with Red/Blue and full intensity) #define FX_MODE_TWO_DOTS 50 #define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity) -#define FX_MODE_RUNNING_DUAL 52 -// #define FX_MODE_HALLOWEEN 53 // removed in 0.14! +#define FX_MODE_RUNNING_DUAL 52 // candidate for removal (use Running) +#define FX_MODE_IMAGE 53 #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 #define FX_MODE_TRICOLOR_FADE 56 @@ -198,7 +230,8 @@ #define FX_MODE_COLORTWINKLE 74 #define FX_MODE_LAKE 75 #define FX_MODE_METEOR 76 -#define FX_MODE_METEOR_SMOOTH 77 +//#define FX_MODE_METEOR_SMOOTH 77 // replaced by Meteor +#define FX_MODE_COPY 77 #define FX_MODE_RAILWAY 78 #define FX_MODE_RIPPLE 79 #define FX_MODE_TWINKLEFOX 80 @@ -214,16 +247,16 @@ #define FX_MODE_EXPLODING_FIREWORKS 90 #define FX_MODE_BOUNCINGBALLS 91 #define FX_MODE_SINELON 92 -#define FX_MODE_SINELON_DUAL 93 -#define FX_MODE_SINELON_RAINBOW 94 +#define FX_MODE_SINELON_DUAL 93 // candidate for removal (use sinelon) +#define FX_MODE_SINELON_RAINBOW 94 // candidate for removal (use sinelon) #define FX_MODE_POPCORN 95 #define FX_MODE_DRIP 96 #define FX_MODE_PLASMA 97 #define FX_MODE_PERCENT 98 -#define FX_MODE_RIPPLE_RAINBOW 99 +#define FX_MODE_RIPPLE_RAINBOW 99 // candidate for removal (use ripple) #define FX_MODE_HEARTBEAT 100 #define FX_MODE_PACIFICA 101 -#define FX_MODE_CANDLE_MULTI 102 +#define FX_MODE_CANDLE_MULTI 102 // candidate for removal (use candle with multi select) #define FX_MODE_SOLID_GLITTER 103 // candidate for removal (use glitter) #define FX_MODE_SUNRISE 104 #define FX_MODE_PHASED 105 @@ -277,6 +310,7 @@ #define FX_MODE_2DFIRENOISE 149 #define FX_MODE_2DSQUAREDSWIRL 150 // #define FX_MODE_2DFIRE2012 151 +#define FX_MODE_PACMAN 151 // gap fill (non-SR). Do NOT renumber; SR-ID range must remain stable. #define FX_MODE_2DDNA 152 #define FX_MODE_2DMATRIX 153 #define FX_MODE_2DMETABALLS 154 @@ -287,6 +321,7 @@ #define FX_MODE_DJLIGHT 159 #define FX_MODE_2DFUNKYPLANK 160 //#define FX_MODE_2DCENTERBARS 161 +#define FX_MODE_SHIMMER 161 // gap fill, non SR 1D effect #define FX_MODE_2DPULSER 162 #define FX_MODE_BLURZ 163 #define FX_MODE_2DDRIFT 164 @@ -313,7 +348,68 @@ #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 -#define MODE_COUNT 187 +#define FX_MODE_PARTICLEVOLCANO 187 +#define FX_MODE_PARTICLEFIRE 188 +#define FX_MODE_PARTICLEFIREWORKS 189 +#define FX_MODE_PARTICLEVORTEX 190 +#define FX_MODE_PARTICLEPERLIN 191 +#define FX_MODE_PARTICLEPIT 192 +#define FX_MODE_PARTICLEBOX 193 +#define FX_MODE_PARTICLEATTRACTOR 194 +#define FX_MODE_PARTICLEIMPACT 195 +#define FX_MODE_PARTICLEWATERFALL 196 +#define FX_MODE_PARTICLESPRAY 197 +#define FX_MODE_PARTICLESGEQ 198 +#define FX_MODE_PARTICLECENTERGEQ 199 +#define FX_MODE_PARTICLEGHOSTRIDER 200 +#define FX_MODE_PARTICLEBLOBS 201 +#define FX_MODE_PSDRIP 202 +#define FX_MODE_PSPINBALL 203 +#define FX_MODE_PSDANCINGSHADOWS 204 +#define FX_MODE_PSFIREWORKS1D 205 +#define FX_MODE_PSSPARKLER 206 +#define FX_MODE_PSHOURGLASS 207 +#define FX_MODE_PS1DSPRAY 208 +#define FX_MODE_PSBALANCE 209 +#define FX_MODE_PSCHASE 210 +#define FX_MODE_PSSTARBURST 211 +#define FX_MODE_PS1DGEQ 212 +#define FX_MODE_PSFIRE1D 213 +#define FX_MODE_PS1DSONICSTREAM 214 +#define FX_MODE_PS1DSONICBOOM 215 +#define FX_MODE_PS1DSPRINGY 216 +#define FX_MODE_PARTICLEGALAXY 217 +#define MODE_COUNT 218 + + +#define BLEND_STYLE_FADE 0x00 // universal +#define BLEND_STYLE_FAIRY_DUST 0x01 // universal +#define BLEND_STYLE_SWIPE_RIGHT 0x02 // 1D or 2D +#define BLEND_STYLE_SWIPE_LEFT 0x03 // 1D or 2D +#define BLEND_STYLE_OUTSIDE_IN 0x04 // 1D or 2D +#define BLEND_STYLE_INSIDE_OUT 0x05 // 1D or 2D +#define BLEND_STYLE_SWIPE_UP 0x06 // 2D +#define BLEND_STYLE_SWIPE_DOWN 0x07 // 2D +#define BLEND_STYLE_OPEN_H 0x08 // 2D +#define BLEND_STYLE_OPEN_V 0x09 // 2D +#define BLEND_STYLE_SWIPE_TL 0x0A // 2D +#define BLEND_STYLE_SWIPE_TR 0x0B // 2D +#define BLEND_STYLE_SWIPE_BR 0x0C // 2D +#define BLEND_STYLE_SWIPE_BL 0x0D // 2D +#define BLEND_STYLE_CIRCULAR_OUT 0x0E // 2D +#define BLEND_STYLE_CIRCULAR_IN 0x0F // 2D +// as there are many push variants to optimise if statements they are groupped together +#define BLEND_STYLE_PUSH_RIGHT 0x10 // 1D or 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_LEFT 0x11 // 1D or 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_UP 0x12 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_DOWN 0x13 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_TL 0x14 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_TR 0x15 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_BR 0x16 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_BL 0x17 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_MASK 0x10 +#define BLEND_STYLE_COUNT 18 + typedef enum mapping1D2D { M12_Pixels = 0, @@ -323,170 +419,197 @@ typedef enum mapping1D2D { M12_sPinwheel = 4 } mapping1D2D_t; -// segment, 80 bytes -typedef struct Segment { +class WS2812FX; + +// segment, 76 bytes +class Segment { public: - uint16_t start; // start index / start X coordinate 2D (left) - uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0 - uint16_t offset; - uint8_t speed; - uint8_t intensity; - uint8_t palette; - uint8_t mode; + uint32_t colors[NUM_COLORS]; + uint16_t start; // start index / start X coordinate 2D (left) + uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0 + uint16_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows + uint16_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows + uint16_t offset; // offset for 1D effects (effect will wrap around) union { - uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected + mutable uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected struct { - bool selected : 1; // 0 : selected - bool reverse : 1; // 1 : reversed - bool on : 1; // 2 : is On - bool mirror : 1; // 3 : mirrored - bool freeze : 1; // 4 : paused/frozen - bool reset : 1; // 5 : indicates that Segment runtime requires reset - bool reverse_y : 1; // 6 : reversed Y (2D) - bool mirror_y : 1; // 7 : mirrored Y (2D) - bool transpose : 1; // 8 : transposed (2D, swapped X & Y) - uint8_t map1D2D : 3; // 9-11 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) - uint8_t soundSim : 2; // 12-13 : 0-3 sound simulation types ("soft" & "hard" or "on"/"off") - uint8_t set : 2; // 14-15 : 0-3 UI segment sets/groups + mutable bool selected : 1; // 0 : selected + bool reverse : 1; // 1 : reversed + mutable bool on : 1; // 2 : is On + bool mirror : 1; // 3 : mirrored + mutable bool freeze : 1; // 4 : paused/frozen + mutable bool reset : 1; // 5 : indicates that Segment runtime requires reset + bool reverse_y : 1; // 6 : reversed Y (2D) + bool mirror_y : 1; // 7 : mirrored Y (2D) + bool transpose : 1; // 8 : transposed (2D, swapped X & Y) + uint8_t map1D2D : 3; // 9-11 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) + uint8_t soundSim : 2; // 12-13 : 0-3 sound simulation types ("soft" & "hard" or "on"/"off") + mutable 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 + uint8_t opacity, cct; // 0==1900K, 255==10091K + // effect data + uint8_t mode; + uint8_t palette; + uint8_t speed; + uint8_t intensity; + 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 blendMode : 4; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn }; - 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; + uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn + char *name; // segment name // runtime data - unsigned long next_time; // millis() of next update - uint32_t step; // custom "step" var - uint32_t call; // call counter - uint16_t aux0; // custom var - uint16_t aux1; // custom var + mutable uint32_t step; // custom "step" var + mutable uint32_t call; // call counter + mutable uint16_t aux0; // custom var + mutable uint16_t aux1; // custom var byte *data; // effect data pointer - static uint16_t maxWidth, maxHeight; // these define matrix width & height (max. segment dimensions) - typedef struct TemporarySegmentData { - uint16_t _optionsT; - uint32_t _colorT[NUM_COLORS]; - uint8_t _speedT; - uint8_t _intensityT; - uint8_t _custom1T, _custom2T; // custom FX parameters/sliders - struct { - uint8_t _custom3T : 5; // reduced range slider (0-31) - bool _check1T : 1; // checkmark 1 - bool _check2T : 1; // checkmark 2 - bool _check3T : 1; // checkmark 3 - }; - uint16_t _aux0T; - uint16_t _aux1T; - uint32_t _stepT; - uint32_t _callT; - uint8_t *_dataT; - uint16_t _dataLenT; - TemporarySegmentData() - : _dataT(nullptr) // just in case... - , _dataLenT(0) - {} - } tmpsegd_t; + static uint16_t maxWidth, maxHeight; // these define matrix width & height (max. segment dimensions) private: + uint32_t *pixels; // pixel data + unsigned _dataLen; + uint8_t _default_palette; // palette number that gets assigned to pal0 union { - uint8_t _capabilities; + mutable uint8_t _capabilities; // determines segment capabilities in terms of what is available: RGB, W, CCT, manual W, etc. struct { bool _isRGB : 1; bool _hasW : 1; bool _isCCT : 1; bool _manualW : 1; - uint8_t _reserved : 4; }; }; - uint16_t _dataLen; - static uint16_t _usedSegmentData; - // perhaps this should be per segment, not static + // static variables are use to speed up effect calculations by stashing common pre-calculated values + static unsigned _usedSegmentData; // amount of data used by all segments + static unsigned _vLength; // 1D dimension used for current effect + static unsigned _vWidth, _vHeight; // 2D dimensions used for current effect + static uint32_t _currentColors[NUM_COLORS]; // colors used for current effect (faster access from effect functions) static CRGBPalette16 _currentPalette; // palette used for current effect (includes transition, used in color_from_palette()) static CRGBPalette16 _randomPalette; // actual random palette static CRGBPalette16 _newRandomPalette; // target random palette - static uint16_t _lastPaletteChange; // last random palette change time in millis()/1000 - static uint16_t _lastPaletteBlend; // blend palette according to set Transition Delay in millis()%0xFFFF - #ifndef WLED_DISABLE_MODE_BLEND + static uint16_t _lastPaletteChange; // last random palette change time (in seconds) + static uint16_t _nextPaletteBlend; // next due time for random palette morph (in millis()) static bool _modeBlend; // mode/effect blending semaphore - #endif + // clipping rectangle used for blending + static uint16_t _clipStart, _clipStop; + static uint8_t _clipStartY, _clipStopY; - // transition data, valid only if transitional==true, holds values during transition (72 bytes) + // transition data, holds values during transition (76 bytes/28 bytes) struct Transition { - #ifndef WLED_DISABLE_MODE_BLEND - tmpsegd_t _segT; // previous segment environment - uint8_t _modeT; // previous mode/effect - #else - uint32_t _colorT[NUM_COLORS]; + Segment *_oldSegment; // previous segment environment (may be nullptr if effect did not change) + unsigned long _start; // must accommodate millis() + uint32_t _colors[NUM_COLORS]; // current colors + #ifndef WLED_SAVE_RAM + CRGBPalette16 _palT; // temporary palette (slowly being morphed from old to new) #endif - uint8_t _briT; // temporary brightness - uint8_t _cctT; // temporary CCT - CRGBPalette16 _palT; // temporary palette - uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) - unsigned long _start; // must accommodate millis() - uint16_t _dur; + uint16_t _dur; // duration of transition in ms + uint16_t _progress; // transition progress (0-65535); pre-calculated from _start & _dur in updateTransitionProgress() + uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) + uint8_t _palette, _bri, _cct; // palette ID, brightness and CCT at the start of transition (brightness will be 0 if segment was off) Transition(uint16_t dur=750) - : _palT(CRGBPalette16(CRGB::Black)) - , _prevPaletteBlends(0) - , _start(millis()) - , _dur(dur) + : _oldSegment(nullptr) + , _start(millis()) + , _colors{0,0,0} + #ifndef WLED_SAVE_RAM + , _palT(CRGBPalette16(CRGB::Black)) + #endif + , _dur(dur) + , _progress(0) + , _prevPaletteBlends(0) + , _palette(0) + , _bri(0) + , _cct(0) {} + ~Transition() { + //DEBUGFX_PRINTF_P(PSTR("-- Destroying transition: %p\n"), this); + if (_oldSegment) delete _oldSegment; + } } *_t; - public: + protected: - Segment(uint16_t sStart=0, uint16_t sStop=30) : - start(sStart), - stop(sStop), - offset(0), - speed(DEFAULT_SPEED), - intensity(DEFAULT_INTENSITY), - palette(0), - mode(DEFAULT_MODE), - options(SELECTED | SEGMENT_ON), - grouping(1), - spacing(0), - opacity(255), - colors{DEFAULT_COLOR,BLACK,BLACK}, - cct(127), - custom1(DEFAULT_C1), - custom2(DEFAULT_C2), - custom3(DEFAULT_C3), - check1(false), - check2(false), - check3(false), - startY(0), - stopY(1), - name(nullptr), - next_time(0), - step(0), - call(0), - aux0(0), - aux1(0), - data(nullptr), - _capabilities(0), - _dataLen(0), - _t(nullptr) - { - #ifdef WLED_DEBUG - //Serial.printf("-- Creating segment: %p\n", this); - #endif + inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } + + inline uint32_t *getPixels() const { return pixels; } + inline void setPixelColorRaw(unsigned i, uint32_t c) const { pixels[i] = c; } + inline uint32_t getPixelColorRaw(unsigned i) const { return pixels[i]; }; + #ifndef WLED_DISABLE_2D + inline void setPixelColorXYRaw(unsigned x, unsigned y, uint32_t c) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; pixels[XY(x,y)] = c; } + inline uint32_t getPixelColorXYRaw(unsigned x, unsigned y) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; return pixels[XY(x,y)]; }; + #endif + void resetIfRequired(); // sets all SEGENV variables to 0 and clears data buffer + CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); + + // transition functions + void stopTransition(); // ends transition mode by destroying transition structure (does nothing if not in transition) + void updateTransitionProgress() const; // sets transition progress (0-65535) based on time passed since transition start + inline void handleTransition() { + updateTransitionProgress(); + if (isInTransition() && progress() == 0xFFFFU) stopTransition(); } + inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // relies on handleTransition()/updateTransitionProgress() to update progression variable + inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } - Segment(uint16_t sStartX, uint16_t sStopX, uint16_t sStartY, uint16_t sStopY) : Segment(sStartX, sStopX) { - startY = sStartY; - stopY = sStopY; + inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; } + inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; }; + inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-BLEND_STYLE_FADE transition + + static void handleRandomPalette(); + + public: + + Segment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1) + : colors{DEFAULT_COLOR,BLACK,BLACK} + , start(sStart) + , stop(sStop > sStart ? sStop : sStart+1) // minimum length is 1 + , startY(sStartY) + , stopY(sStopY > sStartY ? sStopY : sStartY+1) // minimum height is 1 + , offset(0) + , options(SELECTED | SEGMENT_ON) + , grouping(1) + , spacing(0) + , opacity(255) + , cct(127) + , mode(DEFAULT_MODE) + , palette(0) + , speed(DEFAULT_SPEED) + , intensity(DEFAULT_INTENSITY) + , custom1(DEFAULT_C1) + , custom2(DEFAULT_C2) + , custom3(DEFAULT_C3) + , check1(false) + , check2(false) + , check3(false) + , blendMode(0) + , name(nullptr) + , step(0) + , call(0) + , aux0(0) + , aux1(0) + , data(nullptr) + , _dataLen(0) + , _default_palette(6) + , _capabilities(0) + , _t(nullptr) + { + DEBUGFX_PRINTF_P(PSTR("-- Creating segment: %p [%d,%d:%d,%d]\n"), this, (int)start, (int)stop, (int)startY, (int)stopY); + // allocate render buffer (always entire segment), prefer PSRAM if DRAM is running low. Note: impact on FPS with PSRAM buffer is low (<2% with QSPI PSRAM) + pixels = static_cast(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR)); + if (!pixels) { + DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + extern byte errorFlag; + errorFlag = ERR_NORAM_PX; + stop = 0; // mark segment as inactive/invalid + } } Segment(const Segment &orig); // copy constructor @@ -494,60 +617,67 @@ typedef struct Segment { ~Segment() { #ifdef WLED_DEBUG - //Serial.printf("-- Destroying segment: %p", this); - //if (name) Serial.printf(" %s (%p)", name, name); - //if (data) Serial.printf(" %d->(%p)", (int)_dataLen, data); - //Serial.println(); + DEBUGFX_PRINTF_P(PSTR("-- Destroying segment: %p [%d,%d:%d,%d]"), this, (int)start, (int)stop, (int)startY, (int)stopY); + if (name) DEBUGFX_PRINTF_P(PSTR(" %s (%p)"), name, name); + if (data) DEBUGFX_PRINTF_P(PSTR(" %u->(%p)"), _dataLen, data); + DEBUGFX_PRINTF_P(PSTR(" T[%p]"), _t); + DEBUGFX_PRINTLN(); + #endif + clearName(); + #ifdef WLED_ENABLE_GIF + endImagePlayback(this); #endif - if (name) { delete[] name; name = nullptr; } - stopTransition(); deallocateData(); + p_free(pixels); } Segment& operator= (const Segment &orig); // copy assignment Segment& operator= (Segment &&orig) noexcept; // move assignment #ifdef WLED_DEBUG - size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0); } + size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0) + (pixels?length()*sizeof(uint32_t):0); } #endif - inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } - inline bool isSelected() const { return selected; } - inline bool isInTransition() const { return _t != nullptr; } - inline bool isActive() const { return stop > start; } - inline bool is2D() const { return (width()>1 && height()>1); } - inline bool hasRGB() const { return _isRGB; } - inline bool hasWhite() const { return _hasW; } - inline bool isCCT() const { return _isCCT; } - inline uint16_t width() const { return isActive() ? (stop - start) : 0; } // segment width in physical pixels (length if 1D) - inline uint16_t height() const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) - inline uint16_t length() const { return width() * height(); } // segment length (count) in physical pixels - inline uint16_t groupLength() const { return grouping + spacing; } + inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } + inline bool isSelected() const { return selected; } + inline bool isInTransition() const { return _t != nullptr; } + inline bool isActive() const { return stop > start && pixels; } + inline bool hasRGB() const { return _isRGB; } + inline bool hasWhite() const { return _hasW; } + inline bool isCCT() const { return _isCCT; } + inline uint16_t width() const { return stop > start ? (stop - start) : 0; }// segment width in physical pixels (length if 1D) + inline uint16_t height() const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) + inline uint16_t length() const { return width() * height(); } // segment length (count) in physical pixels + inline uint16_t groupLength() const { return grouping + spacing; } inline uint8_t getLightCapabilities() const { return _capabilities; } - - inline static uint16_t getUsedSegmentData() { return _usedSegmentData; } - inline static void addUsedSegmentData(int len) { _usedSegmentData += len; } - #ifndef WLED_DISABLE_MODE_BLEND - inline static void modeBlend(bool blend) { _modeBlend = blend; } - #endif - static void handleRandomPalette(); + inline void deactivate() { setGeometry(0,0); } + inline Segment &clearName() { p_free(name); name = nullptr; return *this; } + inline Segment &setName(const String &name) { return setName(name.c_str()); } + + inline static unsigned vLength() { return Segment::_vLength; } + inline static unsigned vWidth() { return Segment::_vWidth; } + inline static unsigned vHeight() { return Segment::_vHeight; } + inline static uint32_t getCurrentColor(unsigned i) { return Segment::_currentColors[i= 0 && i < length()) setPixelColorRaw(i,col); } #ifdef WLED_USE_AA_PIXELS - void setPixelColor(float i, uint32_t c, bool aa = true); - inline 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); } - inline void setPixelColor(float i, CRGB c, bool aa = true) { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } + void setPixelColor(float i, uint32_t c, bool aa = true) const; + inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) const { setPixelColor(i, RGBW32(r,g,b,w), aa); } + inline void setPixelColor(float i, CRGB c, bool aa = true) const { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } #endif + [[gnu::hot]] bool isPixelClipped(int i) const; [[gnu::hot]] uint32_t getPixelColor(int i) const; // 1D support functions (some implement 2D as well) - void blur(uint8_t, bool smear = false); - void fill(uint32_t c); - void fade_out(uint8_t r); - void fadeToBlackBy(uint8_t fadeBy); - inline void blendPixelColor(int n, uint32_t color, uint8_t blend) { setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } - inline void blendPixelColor(int n, CRGB c, uint8_t blend) { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } - inline void addPixelColor(int n, uint32_t color, bool fast = false) { setPixelColor(n, color_add(getPixelColor(n), color, fast)); } - inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(n, RGBW32(r,g,b,w), fast); } - inline void addPixelColor(int n, CRGB c, bool fast = false) { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), fast); } - inline void fadePixelColor(uint16_t n, uint8_t fade) { setPixelColor(n, color_fade(getPixelColor(n), fade, true)); } - [[gnu::hot]] uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255) const; + void blur(uint8_t, bool smear = false) const; + void clear() const { fill(BLACK); } // clear segment + void fill(uint32_t c) const; + void fade_out(uint8_t r) const; + void fadeToSecondaryBy(uint8_t fadeBy) const; + void fadeToBlackBy(uint8_t fadeBy) const; + inline void blendPixelColor(int n, uint32_t color, uint8_t blend) const { setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } + inline void blendPixelColor(int n, CRGB c, uint8_t blend) const { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColor(int n, uint32_t color, bool preserveCR = true) const { setPixelColor(n, color_add(getPixelColor(n), color, preserveCR)); } + inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) const + { addPixelColor(n, RGBW32(r,g,b,w), preserveCR); } + inline void addPixelColor(int n, CRGB c, bool preserveCR = true) const { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), preserveCR); } + inline void fadePixelColor(uint16_t n, uint8_t fade) const { setPixelColor(n, color_fade(getPixelColor(n), fade, true)); } + [[gnu::hot]] uint32_t color_from_palette(uint16_t, bool mapping, bool moving, uint8_t mcol, uint8_t pbri = 255) const; [[gnu::hot]] uint32_t color_wheel(uint8_t pos) const; - - // 2D Blur: shortcuts for bluring columns or rows only (50% faster than full 2D blur) - inline void blurCols(fract8 blur_amount, bool smear = false) { // blur all columns - const unsigned cols = virtualWidth(); - for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount, smear); - } - inline void blurRows(fract8 blur_amount, bool smear = false) { // blur all rows - const unsigned rows = virtualHeight(); - for ( unsigned i = 0; i < rows; i++) blurRow(i, blur_amount, smear); - } - // 2D matrix - [[gnu::hot]] unsigned virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) - [[gnu::hot]] unsigned virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) - inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) + unsigned virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) + unsigned virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) + inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) #ifndef WLED_DISABLE_2D return (is2D() && map1D2D == M12_pBar) ? virtualWidth() : 1; #else return 1; #endif } + inline unsigned rawLength() const { // returns length of used raw pixel buffer (eg. get/setPixelColorRaw()) + #ifndef WLED_DISABLE_2D + if (is2D()) return virtualWidth() * virtualHeight(); + #endif + return virtualLength(); + } + #ifndef WLED_DISABLE_2D - [[gnu::hot]] uint16_t XY(int x, int y); // support function to get relative index within segment - [[gnu::hot]] void setPixelColorXY(int x, int y, uint32_t c); // set relative pixel within segment with color - inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColorXY(int(x), int(y), c); } - inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } - inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } - inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColorXY(int(x), int(y), RGBW32(c.r,c.g,c.b,0)); } + inline bool is2D() const { return (width()>1 && height()>1); } + [[gnu::hot]] void setPixelColorXY(int x, int y, uint32_t c) const; // set relative pixel within segment with color + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColorXY(int(x), int(y), c); } + inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) const { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(int x, int y, CRGB c) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColorXY(int(x), int(y), RGBW32(c.r,c.g,c.b,0)); } #ifdef WLED_USE_AA_PIXELS - void setPixelColorXY(float x, float y, uint32_t c, bool aa = true); - inline 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); } - inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } + void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) const; + inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) const { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } + inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } #endif + [[gnu::hot]] bool isPixelXYClipped(int x, int y) const; [[gnu::hot]] uint32_t getPixelColorXY(int x, int y) const; // 2D support functions - inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } - inline 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); } - inline void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, fast)); } - inline 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); } - inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } - inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } - void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur - void blur2D(uint8_t blur_amount, bool smear = false); - void blurRow(uint32_t row, fract8 blur_amount, bool smear = false); - void blurCol(uint32_t col, fract8 blur_amount, bool smear = false); - 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 drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); - inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } - void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); - inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } - void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false); - inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline - void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0); - inline 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 - inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline - void wu_pixel(uint32_t x, uint32_t y, CRGB c); - inline void blur2d(fract8 blur_amount) { blur(blur_amount); } - inline void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) const { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColorXY(int x, int y, uint32_t color, bool preserveCR = true) const { setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, preserveCR)); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) + { addPixelColorXY(x, y, RGBW32(r,g,b,w), preserveCR); } + inline void addPixelColorXY(int x, int y, CRGB c, bool preserveCR = true) const { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), preserveCR); } + inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } + inline void blurCols(fract8 blur_amount, bool smear = false) const { blur2D(0, blur_amount, smear); } // blur all columns (50% faster than full 2D blur) + inline void blurRows(fract8 blur_amount, bool smear = false) const { blur2D(blur_amount, 0, smear); } // blur all rows (50% faster than full 2D blur) + //void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur + void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) const; + void moveX(int delta, bool wrap = false) const; + void moveY(int delta, bool wrap = false) const; + void move(unsigned dir, unsigned delta, bool wrap = false) const; + void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const; + void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const; + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) const; + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) const; + void wu_pixel(uint32_t x, uint32_t y, CRGB c) const; + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) const { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline + inline void fill_solid(CRGB c) const { fill(RGBW32(c.r,c.g,c.b,0)); } #else - inline uint16_t XY(uint16_t x, uint16_t y) { return x; } - inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } - inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColor(int(x), c); } - inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } - inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } - inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } + inline bool is2D() const { return false; } + inline void setPixelColorXY(int x, int y, uint32_t c) const { setPixelColor(x, c); } + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColor(int(x), c); } + inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) const { setPixelColor(x, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(int x, int y, CRGB c) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } #ifdef WLED_USE_AA_PIXELS - inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } + inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) const { setPixelColor(x, c, aa); } inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } - inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } + inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } #endif - inline uint32_t getPixelColorXY(int x, int y) { return getPixelColor(x); } - inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } - inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } - inline void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { addPixelColor(x, color, fast); } - inline 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); } - inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), fast); } - inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); } - inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {} - inline void blur2D(uint8_t blur_amount, bool smear = false) {} - inline void blurRow(uint32_t row, fract8 blur_amount, bool smear = false) {} - inline void blurCol(uint32_t col, fract8 blur_amount, bool smear = false) {} - inline void moveX(int8_t delta, bool wrap = false) {} - inline void moveY(int8_t delta, bool wrap = false) {} + inline bool isPixelXYClipped(int x, int y) const { return isPixelClipped(x); } + inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(x); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) const { blendPixelColor(x, c, blend); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColorXY(int x, int y, uint32_t color, bool saturate = false) const { addPixelColor(x, color, saturate); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool saturate = false) const { addPixelColor(x, RGBW32(r,g,b,w), saturate); } + inline void addPixelColorXY(int x, int y, CRGB c, bool saturate = false) const { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), saturate); } + inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { fadePixelColor(x, fade); } + //inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {} + inline void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) {} + inline void blurCols(fract8 blur_amount, bool smear = false) { blur(blur_amount, smear); } // blur all columns (50% faster than full 2D blur) + inline void blurRows(fract8 blur_amount, bool smear = false) {} + inline void moveX(int delta, bool wrap = false) {} + inline void moveY(int delta, bool wrap = false) {} inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} @@ -689,38 +811,32 @@ typedef struct Segment { inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {} inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {} - inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB color) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {} inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {} #endif -} segment; -//static int segSize = sizeof(Segment); + friend class WS2812FX; + friend class ParticleSystem2D; + friend class ParticleSystem1D; +}; -// main "strip" class -class WS2812FX { // 96 bytes - typedef uint16_t (*mode_ptr)(); // pointer to mode function +// main "strip" class (108 bytes) +class WS2812FX { + typedef void (*mode_ptr)(); // pointer to mode function typedef void (*show_callback)(); // pre show callback typedef struct ModeData { uint8_t _id; // mode (effect) id mode_ptr _fcn; // mode (effect) function const char *_data; // mode (effect) name and its UI control data - ModeData(uint8_t id, uint16_t (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {} + ModeData(uint8_t id, void (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {} } mode_data_t; - static WS2812FX* instance; - public: WS2812FX() : - paletteFade(0), paletteBlend(0), - cctBlending(0), now(millis()), timebase(0), isMatrix(false), -#ifndef WLED_DISABLE_2D - panels(1), -#endif #ifdef WLED_AUTOSEGMENTS autoSegments(true), #else @@ -728,30 +844,29 @@ class WS2812FX { // 96 bytes #endif correctWB(false), cctFromRgb(false), - // semi-private (just obscured) used in effect functions through macros - _colors_t{0,0,0}, - _virtualSegmentLength(0), // true private variables + _pixels(nullptr), + _pixelCCT(nullptr), _suspend(false), - _length(DEFAULT_LED_COUNT), _brightness(DEFAULT_BRIGHTNESS), + _length(DEFAULT_LED_COUNT), _transitionDur(750), - _targetFps(WLED_FPS), _frametime(FRAMETIME_FIXED), - _cumulativeFps(50 << FPS_CALC_SHIFT), + _cumulativeFps(WLED_FPS << FPS_CALC_SHIFT), + _targetFps(WLED_FPS), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), _triggered(false), + _segment_index(0), + _mainSegment(0), _modeCount(MODE_COUNT), _callback(nullptr), customMappingTable(nullptr), customMappingSize(0), _lastShow(0), - _segment_index(0), - _mainSegment(0) + _lastServiceShow(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 @@ -759,124 +874,112 @@ class WS2812FX { // 96 bytes } ~WS2812FX() { - if (customMappingTable) delete[] customMappingTable; + p_free(_pixels); + p_free(_pixelCCT); // just in case + d_free(customMappingTable); _mode.clear(); _modeData.clear(); _segments.clear(); #ifndef WLED_DISABLE_2D panel.clear(); #endif - customPalettes.clear(); } - static WS2812FX* getInstance() { return instance; } - void #ifdef WLED_DEBUG printSize(), // prints memory usage for strip components #endif finalizeInit(), // initialises strip components service(), // executes effect functions when due and calls strip.show() - setMode(uint8_t segid, uint8_t m), // sets effect/mode for given segment (high level API) - setColor(uint8_t slot, uint32_t c), // sets color (in slot) for given segment (high level API) setCCT(uint16_t k), // sets global CCT (either in relative 0-255 value or in K) setBrightness(uint8_t b, bool direct = false), // sets strip brightness setRange(uint16_t i, uint16_t i2, uint32_t col), // used for clock overlay purgeSegments(), // removes inactive segments from RAM (may incure penalty and memory fragmentation but reduces vector footprint) - 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), + setMainSegmentId(unsigned n = 0), resetSegments(), // marks all segments for reset makeAutoSegments(bool forceReset = false), // will create segments based on configured outputs fixInvalidSegments(), // fixes incorrect segment configuration - setPixelColor(unsigned n, uint32_t c), // paints absolute strip pixel with index n and color c + blendSegment(const Segment &topSegment) const, // blends topSegment into pixels show(), // initiates LED output - setTargetFps(uint8_t fps), - setupEffectData(); // add default effects to the list; defined in FX.cpp - - inline void resetTimebase() { timebase = 0UL - millis(); } - inline void restartRuntime() { for (Segment &seg : _segments) { seg.markForReset().resetIfRequired(); } } - inline void setTransitionMode(bool t) { for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } - inline 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)); } - inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } - inline void setPixelColor(unsigned n, CRGB c) { setPixelColor(n, c.red, c.green, c.blue); } - inline void fill(uint32_t c) { for (unsigned i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) + setTargetFps(unsigned fps), + setupEffectData(), // add default effects to the list; defined in FX.cpp + waitForIt(); // wait until frame is over (service() has finished or time for 1 frame has passed) + + void setRealtimePixelColor(unsigned i, uint32_t c); + inline void setPixelColor(unsigned n, uint32_t c) const { if (n < getLengthTotal()) _pixels[n] = c; } // paints absolute strip pixel with index n and color c + inline void resetTimebase() { timebase = 0UL - millis(); } + inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) const + { setPixelColor(n, RGBW32(r,g,b,w)); } + inline void setPixelColor(unsigned n, CRGB c) const { setPixelColor(n, c.red, c.green, c.blue); } + inline void fill(uint32_t c) const { for (size_t i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) inline void trigger() { _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; } // sets transition time (in ms) - inline void appendSegment(const Segment &seg = Segment()) { if (_segments.size() < getMaxSegments()) _segments.push_back(seg); } + inline void appendSegment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1) + { if (_segments.size() < getMaxSegments()) _segments.emplace_back(sStart,sStop,sStartY,sStopY); } inline void suspend() { _suspend = true; } // will suspend (and canacel) strip.service() execution inline void resume() { _suspend = false; } // will resume strip.service() execution - bool - paletteFade, - checkSegmentAlignment(), - hasRGBWBus() const, - hasCCTBus() const, - isUpdating() const, // return true if the strip is being sent pixel updates - deserializeMap(uint8_t n=0); + void restartRuntime(); + void setTransitionMode(bool t); + + bool checkSegmentAlignment() const; + bool hasRGBWBus() const; + bool hasCCTBus() const; + bool deserializeMap(unsigned n = 0); + inline bool isUpdating() const { return !BusManager::canAllShow(); } // return true if the strip is being sent pixel updates inline bool isServicing() const { return _isServicing; } // returns true if strip.service() is executing inline bool hasWhiteChannel() const { return _hasWhiteChannel; } // returns true if strip contains separate white chanel inline bool isOffRefreshRequired() const { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) inline bool isSuspended() const { return _suspend; } // returns true if strip.service() execution is suspended inline bool needsUpdate() const { return _triggered; } // returns true if strip received a trigger() request - uint8_t - paletteBlend, - cctBlending, - getActiveSegmentsNum() const, - getFirstSelectedSegId() const, - getLastActiveSegmentId() const, - getActiveSegsLightCapabilities(bool selectedOnly = false) const, - addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; + uint8_t paletteBlend; + uint8_t getActiveSegmentsNum() const; + uint8_t getFirstSelectedSegId() const; + uint8_t getLastActiveSegmentId() const; + uint8_t getActiveSegsLightCapabilities(bool selectedOnly = false) const; + uint8_t addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; inline uint8_t getBrightness() const { return _brightness; } // returns current strip brightness - inline uint8_t getMaxSegments() const { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) + inline static constexpr unsigned getMaxSegments() { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) inline uint8_t getSegmentsNum() const { return _segments.size(); } // returns currently present segments inline uint8_t getCurrSegmentId() const { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) inline uint8_t getMainSegmentId() const { return _mainSegment; } // returns main segment index - inline uint8_t getPaletteCount() const { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } inline uint8_t getTargetFps() const { return _targetFps; } // returns rough FPS value for las 2s interval inline uint8_t getModeCount() const { return _modeCount; } // returns number of registered modes/effects - uint16_t - getLengthPhysical() const, - getLengthTotal() const, // will include virtual/nonexistent pixels in matrix - getFps() const, - getMappedPixelIndex(uint16_t index) const; + uint16_t getLengthPhysical() const; + uint16_t getLengthTotal() const; // will include virtual/nonexistent pixels in matrix + inline uint16_t getFps() const { return (millis() - _lastShow > 2000) ? 0 : (FPS_MULTIPLIER * _cumulativeFps) >> FPS_CALC_SHIFT; } // Returns the refresh rate of the LED strip (_cumulativeFps is stored in fixed point) inline uint16_t getFrameTime() const { return _frametime; } // returns amount of time a frame should take (in ms) - inline uint16_t getMinShowDelay() const { return MIN_SHOW_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) + inline uint16_t getMinShowDelay() const { return MIN_FRAME_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) inline uint16_t getLength() const { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) inline uint16_t getTransition() const { return _transitionDur; } // returns currently set transition time (in ms) + inline uint16_t getMappedPixelIndex(uint16_t index) const { // convert logical address to physical + if (index < customMappingSize && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; + return index; + }; unsigned long now, timebase; - uint32_t getPixelColor(unsigned) const; - - inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call - inline uint32_t segColor(uint8_t i) const { return _colors_t[i]; } // returns currently valid color (for slot i) AKA SEGCOLOR(); may be blended between two colors while in transition - - const char * - getModeData(uint8_t id = 0) const { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } + inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds + inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table + inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call - const char ** - getModeDataSrc() { return &(_modeData[0]); } // vectors use arrays for underlying data + const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); } + inline const char **getModeDataSrc() { return &(_modeData[0]); } // vectors use arrays for underlying data - Segment& getSegment(uint8_t id); + Segment& getSegment(unsigned id); inline Segment& getFirstSelectedSeg() { return _segments[getFirstSelectedSegId()]; } // returns reference to first segment that is "selected" inline Segment& getMainSegment() { return _segments[getMainSegmentId()]; } // returns reference to main segment inline Segment* getSegments() { return &(_segments[0]); } // returns pointer to segment vector structure (warning: use carefully) // 2D support (panels) - bool - isMatrix; #ifndef WLED_DISABLE_2D - #define WLED_MAX_PANELS 18 - uint8_t - panels; - - typedef struct panel_t { + struct Panel { 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 @@ -890,55 +993,49 @@ class WS2812FX { // 96 bytes bool serpentine : 1; // is serpentine? }; }; - panel_t() - : xOffset(0) - , yOffset(0) - , width(8) - , height(8) - , options(0) + Panel() + : xOffset(0) + , yOffset(0) + , width(8) + , height(8) + , options(0) {} - } Panel; + }; std::vector panel; #endif void setUpMatrix(); // sets up automatic matrix ledmap from panel configuration - // outsmart the compiler :) by correctly overloading - inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor((unsigned)(y * Segment::maxWidth + x), c); } - inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } - inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } - - inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(isMatrix ? y * Segment::maxWidth + x : x); } + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColor(y * Segment::maxWidth + x, c); } + inline void setPixelColorXY(unsigned x, unsigned y, byte r, byte g, byte b, byte w = 0) const { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + inline uint32_t getPixelColorXY(unsigned x, unsigned y) const { return getPixelColor(y * Segment::maxWidth + x); } // end 2D support - void loadCustomPalettes(); // loads custom palettes from JSON - std::vector customPalettes; // TODO: move custom palettes out of WS2812FX class - + bool isMatrix; struct { bool autoSegments : 1; bool correctWB : 1; bool cctFromRgb : 1; }; - // 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; + Segment *_currentSegment; private: + uint32_t *_pixels; + uint8_t *_pixelCCT; + std::vector _segments; + volatile bool _suspend; - uint16_t _length; uint8_t _brightness; + uint16_t _length; uint16_t _transitionDur; - uint8_t _targetFps; uint16_t _frametime; uint16_t _cumulativeFps; + uint8_t _targetFps; // will require only 1 byte struct { @@ -948,6 +1045,9 @@ class WS2812FX { // 96 bytes bool _triggered : 1; }; + uint8_t _segment_index; + uint8_t _mainSegment; + uint8_t _modeCount; std::vector _mode; // SRAM footprint: 4 bytes per element std::vector _modeData; // mode (effect) name and its slider control data array @@ -958,9 +1058,9 @@ class WS2812FX { // 96 bytes uint16_t customMappingSize; unsigned long _lastShow; + unsigned long _lastServiceShow; - uint8_t _segment_index; - uint8_t _mainSegment; + friend class Segment; }; extern const char JSON_mode_names[]; diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 7c1ae366b7..77e466588a 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -8,8 +8,6 @@ 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 @@ -19,6 +17,7 @@ // note: matrix may be comprised of multiple panels each with different orientation // but ledmap takes care of that. ledmap is constructed upon initialization // so matrix should disable regular ledmap processing +// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context void WS2812FX::setUpMatrix() { #ifndef WLED_DISABLE_2D // isMatrix is set in cfg.cpp or set.cpp @@ -26,8 +25,7 @@ void WS2812FX::setUpMatrix() { // calculate width dynamically because it may have gaps Segment::maxWidth = 1; Segment::maxHeight = 1; - for (size_t i = 0; i < panel.size(); i++) { - Panel &p = panel[i]; + for (const Panel &p : panel) { if (p.xOffset + p.width > Segment::maxWidth) { Segment::maxWidth = p.xOffset + p.width; } @@ -37,21 +35,24 @@ void WS2812FX::setUpMatrix() { } // safety check - if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth <= 1 || Segment::maxHeight <= 1) { + if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth > 255 || Segment::maxHeight > 255 || 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 + panel.shrink_to_fit(); // release memory if allocated resetSegments(); return; } customMappingSize = 0; // prevent use of mapping if anything goes wrong - if (customMappingTable) delete[] customMappingTable; - customMappingTable = new uint16_t[getLengthTotal()]; + d_free(customMappingTable); + // Segment::maxWidth and Segment::maxHeight are set according to panel layout + // and the product will include at least all leds in matrix + // if actual LEDs are more, getLengthTotal() will return correct number of LEDs + customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer to not use SPI RAM if (customMappingTable) { customMappingSize = getLengthTotal(); @@ -68,12 +69,12 @@ void WS2812FX::setUpMatrix() { // 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 + char fileName[32]; strcpy_P(fileName, PSTR("/2d-gaps.json")); bool isFile = WLED_FS.exists(fileName); size_t gapSize = 0; int8_t *gapTable = nullptr; - if (isFile && requestJSONBufferLock(20)) { + if (isFile && requestJSONBufferLock(JSON_LOCK_LEDGAP)) { DEBUG_PRINT(F("Reading LED gap from ")); DEBUG_PRINTLN(fileName); // read the array into global JSON buffer @@ -85,7 +86,7 @@ void WS2812FX::setUpMatrix() { JsonArray map = pDoc->as(); gapSize = map.size(); if (!map.isNull() && gapSize >= matrixSize) { // not an empty map - gapTable = new int8_t[gapSize]; + gapTable = static_cast(p_malloc(gapSize)); if (gapTable) for (size_t i = 0; i < gapSize; i++) { gapTable[i] = constrain(map[i], -1, 1); } @@ -96,8 +97,7 @@ void WS2812FX::setUpMatrix() { } unsigned x, y, pix=0; //pixel - for (size_t pan = 0; pan < panel.size(); pan++) { - Panel &p = panel[pan]; + for (const Panel &p : panel) { unsigned h = p.vertical ? p.height : p.width; unsigned v = p.vertical ? p.width : p.height; for (size_t j = 0; j < v; j++){ @@ -113,20 +113,22 @@ void WS2812FX::setUpMatrix() { } // delete gap array as we no longer need it - if (gapTable) delete[] gapTable; + p_free(gapTable); #ifdef WLED_DEBUG DEBUG_PRINT(F("Matrix ledmap:")); for (unsigned i=0; i= 1) - return isActive() ? (x%width) + (y%height) * width : 0; +// pixel is clipped if it falls outside clipping range +// if clipping start > stop the clipping range is inverted +bool Segment::isPixelXYClipped(int x, int y) const { + if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) { + const bool invertX = _clipStart > _clipStop; + const bool invertY = _clipStartY > _clipStopY; + const int cStartX = invertX ? _clipStop : _clipStart; + const int cStopX = invertX ? _clipStart : _clipStop; + const int cStartY = invertY ? _clipStopY : _clipStartY; + const int cStopY = invertY ? _clipStartY : _clipStopY; + if (blendingStyle == BLEND_STYLE_FAIRY_DUST) { + const unsigned width = cStopX - cStartX; // assumes full segment width (faster than virtualWidth()) + const unsigned len = width * (cStopY - cStartY); // assumes full segment height (faster than virtualHeight()) + if (len < 2) return false; + const unsigned shuffled = hashInt(x + y * width) % len; + const unsigned pos = (shuffled * 0xFFFFU) / len; + return progress() <= pos; + } + if (blendingStyle == BLEND_STYLE_CIRCULAR_IN || blendingStyle == BLEND_STYLE_CIRCULAR_OUT) { + const int cx = (cStopX-cStartX+1) / 2; + const int cy = (cStopY-cStartY+1) / 2; + const bool out = (blendingStyle == BLEND_STYLE_CIRCULAR_OUT); + const unsigned prog = out ? progress() : 0xFFFFU - progress(); + int radius2 = max(cx, cy) * prog / 0xFFFF; + radius2 = 2 * radius2 * radius2; + if (radius2 == 0) return out; + const int dx = x - cx; + const int dy = y - cy; + const bool outside = dx * dx + dy * dy > radius2; + return out ? outside : !outside; + } + bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside; + bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside; + const bool clip = blendingStyle == BLEND_STYLE_OUTSIDE_IN ? xInside || yInside : xInside && yInside; + return !clip; + } + return false; } -void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) +void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) const { if (!isActive()) return; // not active - if ((unsigned)x >= virtualWidth() || (unsigned)y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit - - uint8_t _bri_t = currentBri(); - if (_bri_t < 255) { - col = color_fade(col, _bri_t); - } - - if (reverse ) x = virtualWidth() - x - 1; - if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed - - x *= groupLength(); // expand to physical pixels - y *= groupLength(); // expand to physical pixels - - int W = width(); - int H = height(); - if (x >= W || y >= H) return; // if pixel would fall out of segment just exit - - uint32_t tmpCol = col; - for (int j = 0; j < grouping; j++) { // groupping vertically - for (int g = 0; g < grouping; g++) { // groupping horizontally - int xX = (x+g), yY = (y+j); - if (xX >= W || yY >= H) continue; // we have reached one dimension's end - -#ifndef WLED_DISABLE_MODE_BLEND - // if blending modes, blend with underlying pixel - if (_modeBlend) tmpCol = color_blend(strip.getPixelColorXY(start + xX, startY + yY), col, 0xFFFFU - progress(), true); -#endif - - strip.setPixelColorXY(start + xX, startY + yY, tmpCol); - - if (mirror) { //set the corresponding horizontally mirrored pixel - if (transpose) strip.setPixelColorXY(start + xX, startY + height() - yY - 1, tmpCol); - else strip.setPixelColorXY(start + width() - xX - 1, startY + yY, tmpCol); - } - if (mirror_y) { //set the corresponding vertically mirrored pixel - if (transpose) strip.setPixelColorXY(start + width() - xX - 1, startY + yY, tmpCol); - else strip.setPixelColorXY(start + xX, startY + height() - yY - 1, tmpCol); - } - if (mirror_y && mirror) { //set the corresponding vertically AND horizontally mirrored pixel - strip.setPixelColorXY(start + width() - xX - 1, startY + height() - yY - 1, tmpCol); - } - } - } + if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return; // if pixel would fall out of virtual segment just exit + setPixelColorXYRaw(x, y, col); } #ifdef WLED_USE_AA_PIXELS // anti-aliased version of setPixelColorXY() -void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) +void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) const { if (!isActive()) return; // not active if (x<0.0f || x>1.0f || y<0.0f || y>1.0f) return; // not normalized - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - - float fX = x * (cols-1); - float fY = y * (rows-1); + float fX = x * (vWidth()-1); + float fY = y * (vHeight()-1); if (aa) { unsigned xL = roundf(fX-0.49f); unsigned xR = roundf(fX+0.49f); @@ -251,138 +238,64 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) // returns RGBW values of pixel uint32_t IRAM_ATTR_YN Segment::getPixelColorXY(int x, int y) const { if (!isActive()) return 0; // not active - if ((unsigned)x >= virtualWidth() || (unsigned)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) { std::swap(x,y); } // 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); + if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return 0; // if pixel would fall out of virtual segment just exit + return getPixelColorXYRaw(x,y); } -// blurRow: perform a blur on a row of a rectangular matrix -void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ - if (!isActive() || blur_amount == 0) return; // not active - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - - if (row >= rows) return; - // blur one row - uint8_t keep = smear ? 255 : 255 - blur_amount; - uint8_t seep = blur_amount >> 1; - uint32_t carryover = BLACK; - uint32_t lastnew; - uint32_t last; - uint32_t curnew = BLACK; - for (unsigned x = 0; x < cols; x++) { - uint32_t cur = getPixelColorXY(x, row); - uint32_t part = color_fade(cur, seep); - curnew = color_fade(cur, keep); - if (x > 0) { - if (carryover) - curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); - if (last != prev) // optimization: only set pixel if color has changed - setPixelColorXY(x - 1, row, prev); - } else // first pixel - setPixelColorXY(x, row, curnew); - lastnew = curnew; - last = cur; // save original value for comparison on next iteration - carryover = part; - } - setPixelColorXY(cols-1, row, curnew); // set last pixel -} - -// blurCol: perform a blur on a column of a rectangular matrix -void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { - if (!isActive() || blur_amount == 0) return; // not active - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - - if (col >= cols) return; - // blur one column - uint8_t keep = smear ? 255 : 255 - blur_amount; - uint8_t seep = blur_amount >> 1; - uint32_t carryover = BLACK; - uint32_t lastnew; - uint32_t last; - uint32_t curnew = BLACK; - for (unsigned y = 0; y < rows; y++) { - uint32_t cur = getPixelColorXY(col, y); - uint32_t part = color_fade(cur, seep); - curnew = color_fade(cur, keep); - if (y > 0) { - if (carryover) - curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); - if (last != prev) // optimization: only set pixel if color has changed - setPixelColorXY(col, y - 1, prev); - } else // first pixel - setPixelColorXY(col, y, curnew); - lastnew = curnew; - last = cur; //save original value for comparison on next iteration - carryover = part; - } - setPixelColorXY(col, rows - 1, curnew); -} - -void Segment::blur2D(uint8_t blur_amount, bool smear) { - if (!isActive() || blur_amount == 0) return; // not active - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - - const uint8_t keep = smear ? 255 : 255 - blur_amount; - const uint8_t seep = blur_amount >> (1 + smear); - uint32_t lastnew; - uint32_t last; - for (unsigned row = 0; row < rows; row++) { - uint32_t carryover = BLACK; - uint32_t curnew = BLACK; - for (unsigned x = 0; x < cols; x++) { - uint32_t cur = getPixelColorXY(x, row); - uint32_t part = color_fade(cur, seep); - curnew = color_fade(cur, keep); - if (x > 0) { - if (carryover) curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); - // optimization: only set pixel if color has changed - if (last != prev) setPixelColorXY(x - 1, row, prev); - } else setPixelColorXY(x, row, curnew); // first pixel - lastnew = curnew; - last = cur; // save original value for comparison on next iteration - carryover = part; +// 2D blurring, can be asymmetrical +void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) const { + if (!isActive()) return; // not active + const unsigned cols = vWidth(); + const unsigned rows = vHeight(); + const auto XY = [&](unsigned x, unsigned y){ return x + y*cols; }; + if (blur_x) { + const uint8_t keepx = smear ? 255 : 255 - blur_x; + const uint8_t seepx = blur_x >> 1; + for (unsigned row = 0; row < rows; row++) { // blur rows (x direction) + // handle first pixel in row to avoid conditional in loop (faster) + uint32_t cur = getPixelColorRaw(XY(0, row)); + uint32_t carryover = fast_color_scale(cur, seepx); + setPixelColorRaw(XY(0, row), fast_color_scale(cur, keepx)); + for (unsigned x = 1; x < cols; x++) { + cur = getPixelColorRaw(XY(x, row)); + uint32_t part = fast_color_scale(cur, seepx); + cur = fast_color_scale(cur, keepx); + cur = color_add(cur, carryover); + setPixelColorRaw(XY(x - 1, row), color_add(getPixelColorRaw(XY(x-1, row)), part)); // previous pixel + setPixelColorRaw(XY(x, row), cur); // current pixel + carryover = part; + } } - setPixelColorXY(cols-1, row, curnew); // set last pixel } - for (unsigned col = 0; col < cols; col++) { - uint32_t carryover = BLACK; - uint32_t curnew = BLACK; - for (unsigned y = 0; y < rows; y++) { - uint32_t cur = getPixelColorXY(col, y); - uint32_t part = color_fade(cur, seep); - curnew = color_fade(cur, keep); - if (y > 0) { - if (carryover) curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); - // optimization: only set pixel if color has changed - if (last != prev) setPixelColorXY(col, y - 1, prev); - } else setPixelColorXY(col, y, curnew); // first pixel - lastnew = curnew; - last = cur; //save original value for comparison on next iteration - carryover = part; + if (blur_y) { + const uint8_t keepy = smear ? 255 : 255 - blur_y; + const uint8_t seepy = blur_y >> 1; + for (unsigned col = 0; col < cols; col++) { + // handle first pixel in column + uint32_t cur = getPixelColorRaw(XY(col, 0)); + uint32_t carryover = fast_color_scale(cur, seepy); + setPixelColorRaw(XY(col, 0), fast_color_scale(cur, keepy)); + for (unsigned y = 1; y < rows; y++) { + cur = getPixelColorRaw(XY(col, y)); + uint32_t part = fast_color_scale(cur, seepy); + cur = fast_color_scale(cur, keepy); + cur = color_add(cur, carryover); + setPixelColorRaw(XY(col, y - 1), color_add(getPixelColorRaw(XY(col, y-1)), part)); // previous pixel + setPixelColorRaw(XY(col, y), cur); // current pixel + carryover = part; + } } - setPixelColorXY(col, rows - 1, curnew); } } +/* // 2D Box blur void Segment::box_blur(unsigned radius, bool smear) { if (!isActive() || radius == 0) return; // not active if (radius > 3) radius = 3; const unsigned d = (1 + 2*radius) * (1 + 2*radius); // averaging divisor - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); + const unsigned cols = vWidth(); + const unsigned rows = vHeight(); uint16_t *tmpRSum = new uint16_t[cols*rows]; uint16_t *tmpGSum = new uint16_t[cols*rows]; uint16_t *tmpBSum = new uint16_t[cols*rows]; @@ -448,40 +361,58 @@ void Segment::box_blur(unsigned radius, bool smear) { delete[] tmpBSum; delete[] tmpWSum; } - -void Segment::moveX(int8_t delta, bool wrap) { - if (!isActive()) return; // not active - const int cols = virtualWidth(); - const int 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); +*/ +void Segment::moveX(int delta, bool wrap) const { + if (!isActive() || !delta) return; // not active + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; }; + int absDelta = abs(delta); + if (absDelta >= vW) return; + uint32_t newPxCol[vW]; + int newDelta; + int stop = vW; + int start = 0; + if (wrap) newDelta = (delta + vW) % vW; // +cols in case delta < 0 + else { + if (delta < 0) start = absDelta; + stop = vW - absDelta; + newDelta = delta > 0 ? delta : 0; + } + for (int y = 0; y < vH; y++) { + for (int x = 0; x < stop; x++) { + int srcX = x + newDelta; + if (wrap) srcX %= vW; // Wrap using modulo when `wrap` is true + newPxCol[x] = getPixelColorRaw(XY(srcX, y)); } - for (int x = 0; x < cols; x++) setPixelColorXY(x, y, newPxCol[x]); + for (int x = 0; x < stop; x++) setPixelColorRaw(XY(x + start, y), newPxCol[x]); } } -void Segment::moveY(int8_t delta, bool wrap) { - if (!isActive()) return; // not active - const int cols = virtualWidth(); - const int 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); +void Segment::moveY(int delta, bool wrap) const { + if (!isActive() || !delta) return; // not active + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; }; + int absDelta = abs(delta); + if (absDelta >= vH) return; + uint32_t newPxCol[vH]; + int newDelta; + int stop = vH; + int start = 0; + if (wrap) newDelta = (delta + vH) % vH; // +rows in case delta < 0 + else { + if (delta < 0) start = absDelta; + stop = vH - absDelta; + newDelta = delta > 0 ? delta : 0; + } + for (int x = 0; x < vW; x++) { + for (int y = 0; y < stop; y++) { + int srcY = y + newDelta; + if (wrap) srcY %= vH; // Wrap using modulo when `wrap` is true + newPxCol[y] = getPixelColorRaw(XY(x, srcY)); } - for (int y = 0; y < rows; y++) setPixelColorXY(x, y, newPxCol[y]); + for (int y = 0; y < stop; y++) setPixelColorRaw(XY(x, y + start), newPxCol[y]); } } @@ -489,7 +420,7 @@ void Segment::moveY(int8_t delta, bool wrap) { // @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) { +void Segment::move(unsigned dir, unsigned delta, bool wrap) const { if (delta==0) return; switch (dir) { case 0: moveX( delta, wrap); break; @@ -503,35 +434,37 @@ void Segment::move(uint8_t dir, uint8_t delta, bool wrap) { } } -void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { +void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const { if (!isActive() || radius == 0) return; // not active if (soft) { // Xiaolin Wu’s algorithm - int rsq = radius*radius; + const int rsq = radius*radius; int x = 0; int y = radius; unsigned oldFade = 0; while (x < y) { float yf = sqrtf(float(rsq - x*x)); // needs to be floating point - unsigned fade = float(0xFFFF) * (ceilf(yf) - yf); // how much color to keep + uint8_t fade = float(0xFF) * (ceilf(yf) - yf); // how much color to keep if (oldFade > fade) y--; oldFade = fade; - setPixelColorXY(cx+x, cy+y, color_blend(col, getPixelColorXY(cx+x, cy+y), fade, true)); - setPixelColorXY(cx-x, cy+y, color_blend(col, getPixelColorXY(cx-x, cy+y), fade, true)); - setPixelColorXY(cx+x, cy-y, color_blend(col, getPixelColorXY(cx+x, cy-y), fade, true)); - setPixelColorXY(cx-x, cy-y, color_blend(col, getPixelColorXY(cx-x, cy-y), fade, true)); - setPixelColorXY(cx+y, cy+x, color_blend(col, getPixelColorXY(cx+y, cy+x), fade, true)); - setPixelColorXY(cx-y, cy+x, color_blend(col, getPixelColorXY(cx-y, cy+x), fade, true)); - setPixelColorXY(cx+y, cy-x, color_blend(col, getPixelColorXY(cx+y, cy-x), fade, true)); - setPixelColorXY(cx-y, cy-x, color_blend(col, getPixelColorXY(cx-y, cy-x), fade, true)); - setPixelColorXY(cx+x, cy+y-1, color_blend(getPixelColorXY(cx+x, cy+y-1), col, fade, true)); - setPixelColorXY(cx-x, cy+y-1, color_blend(getPixelColorXY(cx-x, cy+y-1), col, fade, true)); - setPixelColorXY(cx+x, cy-y+1, color_blend(getPixelColorXY(cx+x, cy-y+1), col, fade, true)); - setPixelColorXY(cx-x, cy-y+1, color_blend(getPixelColorXY(cx-x, cy-y+1), col, fade, true)); - setPixelColorXY(cx+y-1, cy+x, color_blend(getPixelColorXY(cx+y-1, cy+x), col, fade, true)); - setPixelColorXY(cx-y+1, cy+x, color_blend(getPixelColorXY(cx-y+1, cy+x), col, fade, true)); - setPixelColorXY(cx+y-1, cy-x, color_blend(getPixelColorXY(cx+y-1, cy-x), col, fade, true)); - setPixelColorXY(cx-y+1, cy-x, color_blend(getPixelColorXY(cx-y+1, cy-x), col, fade, true)); + int px, py; + for (uint8_t i = 0; i < 16; i++) { + int swaps = (i & 0x4 ? 1 : 0); // 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1 + int adj = (i < 8) ? 0 : 1; // 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1 + int dx = (i & 1) ? -1 : 1; // 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1 + int dy = (i & 2) ? -1 : 1; // 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1 + if (swaps) { + px = cx + (y - adj) * dx; + py = cy + x * dy; + } else { + px = cx + x * dx; + py = cy + (y - adj) * dy; + } + uint32_t pixCol = getPixelColorXY(px, py); + setPixelColorXY(px, py, adj ? + color_blend(pixCol, col, fade) : + color_blend(col, pixCol, fade)); + } x++; } } else { @@ -539,14 +472,12 @@ void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, 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); + for (int i = 0; i < 4; i++) { + int dx = (i & 1) ? -x : x; + int dy = (i & 2) ? -y : y; + setPixelColorXY(cx + dx, cy + dy, col); + setPixelColorXY(cx + dy, cy + dx, col); + } x++; if (d > 0) { y--; @@ -559,29 +490,29 @@ void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, } // by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs -void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { +void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const { if (!isActive() || radius == 0) return; // not active + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) // draw soft bounding circle if (soft) drawCircle(cx, cy, radius, col, soft); // fill it - const int cols = virtualWidth(); - const int rows = virtualHeight(); for (int y = -radius; y <= radius; y++) { for (int x = -radius; x <= radius; x++) { if (x * x + y * y <= radius * radius && - int(cx)+x>=0 && int(cy)+y>=0 && - int(cx)+x= 0 && int(cy)+y >= 0 && + int(cx)+x < vW && int(cy)+y < vH) setPixelColorXY(cx + x, cy + y, col); } } } //line function -void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) { +void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) const { if (!isActive()) return; // not active - const int cols = virtualWidth(); - const int rows = virtualHeight(); - if (x0 >= cols || x1 >= cols || y0 >= rows || y1 >= rows) return; + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + if (x0 >= vW || x1 >= vW || y0 >= vH || y1 >= vH) return; const int dx = abs(x1-x0), sx = x0 126) return; // only ASCII 32-126 supported chr -= 32; // align with font table entries - const int cols = virtualWidth(); - const int rows = virtualHeight(); const int font = w*h; - CRGB col = CRGB(color); - CRGBPalette16 grad = CRGBPalette16(col, col2 ? CRGB(col2) : col); + // if col2 == BLACK then use currently selected palette for gradient otherwise create gradient from color and col2 + CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE; // selected palette as gradient - //if (w<5 || w>6 || h!=8) return; for (int i = 0; i= cols || y0 < 0 || y0 >= rows) continue; // drawing off-screen + if (x0 < 0 || x0 >= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen if (((bits>>(j+(8-w))) & 0x01)) { // bit set - setPixelColorXY(x0, y0, col); + setPixelColorXYRaw(x0, y0, c.color32); } } } } #define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8)) -void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel procedure by reddit u/sutaburosu +void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu_pixel procedure by reddit u/sutaburosu if (!isActive()) return; // not active // extract the fractional parts and derive their inverses unsigned xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 7177ca89e8..e8f40fdf19 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -10,8 +10,7 @@ Modified heavily for WLED */ #include "wled.h" -#include "FX.h" -#include "palettes.h" +#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? /* Custom per-LED mapping has moved! @@ -29,67 +28,56 @@ 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25]} */ -#ifndef PIXEL_COUNTS - #define PIXEL_COUNTS DEFAULT_LED_COUNT -#endif - -#ifndef DATA_PINS - #define DATA_PINS DEFAULT_LED_PIN -#endif - -#ifndef LED_TYPES - #define LED_TYPES DEFAULT_LED_TYPE -#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 -static constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) { - return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0; -} - -static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) { - // Pins provided < pins required -> always invalid - // Pins provided = pins required -> always valid - // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated - return (sumPinsRequired(types, numTypes) > numPins) ? false : - (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; -} - /////////////////////////////////////////////////////////////////////////////// // 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; - +unsigned Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] +uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; +uint16_t Segment::maxHeight = 1; +unsigned Segment::_vLength = 0; +unsigned Segment::_vWidth = 0; +unsigned Segment::_vHeight = 0; +uint32_t Segment::_currentColors[NUM_COLORS] = {0,0,0}; CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black); CRGBPalette16 Segment::_randomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); CRGBPalette16 Segment::_newRandomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); -uint16_t Segment::_lastPaletteChange = 0; // perhaps it should be per segment -uint16_t Segment::_lastPaletteBlend = 0; //in millis (lowest 16 bits only) +uint16_t Segment::_lastPaletteChange = 0; // in seconds; perhaps it should be per segment +uint16_t Segment::_nextPaletteBlend = 0; // in millis -#ifndef WLED_DISABLE_MODE_BLEND -bool Segment::_modeBlend = false; -#endif +bool Segment::_modeBlend = false; +uint16_t Segment::_clipStart = 0; +uint16_t Segment::_clipStop = 0; +uint8_t Segment::_clipStartY = 0; +uint8_t Segment::_clipStopY = 1; // copy constructor Segment::Segment(const Segment &orig) { //DEBUG_PRINTF_P(PSTR("-- Copy segment constructor: %p -> %p\n"), &orig, this); memcpy((void*)this, (void*)&orig, sizeof(Segment)); - _t = nullptr; // copied segment cannot be in transition + _t = nullptr; // copied segment cannot be in transition name = nullptr; data = nullptr; _dataLen = 0; - if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } - if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } + pixels = nullptr; + if (!stop) return; // nothing to do if segment is inactive/invalid + if (orig.pixels) { + // allocate pixel buffer: prefer IRAM/PSRAM + pixels = static_cast(allocate_buffer(orig.length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS)); + if (pixels) { + memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length()); + if (orig.name) { name = static_cast(allocate_buffer(strlen(orig.name)+1, BFRALLOC_PREFER_PSRAM)); if (name) strcpy(name, orig.name); } + if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } + } else { + DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + errorFlag = ERR_NORAM_PX; + stop = 0; // mark segment as inactive/invalid + } + } else stop = 0; // mark segment as inactive/invalid } // move constructor @@ -100,6 +88,7 @@ Segment::Segment(Segment &&orig) noexcept { orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; + orig.pixels = nullptr; } // copy assignment @@ -107,17 +96,31 @@ Segment& Segment::operator= (const Segment &orig) { //DEBUG_PRINTF_P(PSTR("-- Copying segment: %p -> %p\n"), &orig, this); if (this != &orig) { // clean destination - if (name) { delete[] name; name = nullptr; } - stopTransition(); + if (name) { p_free(name); name = nullptr; } + if (_t) stopTransition(); // also erases _t deallocateData(); + p_free(pixels); + pixels = nullptr; // copy source memcpy((void*)this, (void*)&orig, sizeof(Segment)); // erase pointers to allocated data data = nullptr; _dataLen = 0; + if (!stop) return *this; // nothing to do if segment is inactive/invalid // 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.pixels) { + // allocate pixel buffer: prefer IRAM/PSRAM + pixels = static_cast(allocate_buffer(orig.length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS)); + if (pixels) { + memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length()); + if (orig.name) { name = static_cast(allocate_buffer(strlen(orig.name)+1, BFRALLOC_PREFER_PSRAM)); if (name) strcpy(name, orig.name); } + if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } + } else { + DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + errorFlag = ERR_NORAM_PX; + stop = 0; // mark segment as inactive/invalid + } + } else stop = 0; // mark segment as inactive/invalid } return *this; } @@ -126,48 +129,70 @@ Segment& Segment::operator= (const Segment &orig) { Segment& Segment::operator= (Segment &&orig) noexcept { //DEBUG_PRINTF_P(PSTR("-- Moving segment: %p -> %p\n"), &orig, this); if (this != &orig) { - if (name) { delete[] name; name = nullptr; } // free old name - stopTransition(); + if (name) { p_free(name); name = nullptr; } // free old name + if (_t) stopTransition(); // also erases _t deallocateData(); // free old runtime data + p_free(pixels); // free old pixel buffer + // move source data memcpy((void*)this, (void*)&orig, sizeof(Segment)); orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; - orig._t = nullptr; // old segment cannot be in transition + orig.pixels = nullptr; + orig._t = nullptr; // old segment cannot be in transition } return *this; } // allocates effect data buffer on heap and initialises (erases) it -bool IRAM_ATTR_YN Segment::allocateData(size_t len) { - if (len == 0) return false; // nothing to do - if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation - return true; +bool Segment::allocateData(size_t len) { + if (len == 0) return false; // nothing to do + if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) + if (call == 0) { + if (_dataLen < FAIR_DATA_PER_SEG) { // segment data is small + //DEBUG_PRINTF_P(PSTR("-- Clearing data (%d): %p\n"), len, this); + memset(data, 0, len); // erase buffer if called during effect initialisation + return true; // no need to reallocate + } + } + else + return true; } - //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); - deallocateData(); // if the old buffer was smaller release it first - if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { + //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n"), len, this); + // limit to MAX_SEGMENT_DATA if there is no PSRAM, otherwise prefer functionality over speed + #ifndef BOARD_HAS_PSRAM + if (Segment::getUsedSegmentData() + len - _dataLen > MAX_SEGMENT_DATA) { // not enough memory - DEBUG_PRINT(F("!!! Effect RAM depleted: ")); - DEBUG_PRINTF_P(PSTR("%d/%d !!!\n"), len, Segment::getUsedSegmentData()); + DEBUG_PRINTF_P(PSTR("SegmentData limit reached: %d/%d\n"), len, Segment::getUsedSegmentData()); errorFlag = ERR_NORAM; return false; } - // do not use SPI RAM on ESP32 since it is slow - data = (byte*)calloc(len, sizeof(byte)); - if (!data) { DEBUG_PRINTLN(F("!!! Allocation failed. !!!")); return false; } // allocation failed - Segment::addUsedSegmentData(len); - //DEBUG_PRINTF_P(PSTR("--- Allocated data (%p): %d/%d -> %p\n"), this, len, Segment::getUsedSegmentData(), data); - _dataLen = len; - return true; + #endif + + if (data) { + d_free(data); // free data and try to allocate again (segment buffer may be blocking contiguous heap) + Segment::addUsedSegmentData(-_dataLen); // subtract buffer size + } + + data = static_cast(allocate_buffer(len, BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR)); // prefer DRAM over PSRAM for speed + + if (data) { + Segment::addUsedSegmentData(len); + _dataLen = len; + //DEBUG_PRINTF_P(PSTR("--- Allocated data (%p): %d/%d -> %p\n"), this, len, Segment::getUsedSegmentData(), data); + return true; + } + // allocation failed + DEBUG_PRINTLN(F("!!! Allocation failed. !!!")); + errorFlag = ERR_NORAM; + return false; } -void IRAM_ATTR_YN Segment::deallocateData() { +void Segment::deallocateData() { if (!data) { _dataLen = 0; return; } - //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer - free(data); + //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); + d_free(data); } else { DEBUG_PRINTF_P(PSTR("---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\n"), this, _dataLen, Segment::getUsedSegmentData()); } @@ -184,78 +209,70 @@ void IRAM_ATTR_YN Segment::deallocateData() { * may free that data buffer. */ void Segment::resetIfRequired() { - if (!reset) return; + if (!reset || !isActive()) return; //DEBUG_PRINTF_P(PSTR("-- Segment reset: %p\n"), this); - if (data && _dataLen > 0) memset(data, 0, _dataLen); // prevent heap fragmentation (just erase buffer instead of deallocateData()) - next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; + if (data && _dataLen > 0) { + if (_dataLen > FAIR_DATA_PER_SEG) deallocateData(); // do not keep large allocations + else memset(data, 0, _dataLen); // can prevent heap fragmentation + DEBUG_PRINTF_P(PSTR("-- Segment %p reset, data cleared\n"), this); + } + if (pixels) for (size_t i = 0; i < length(); i++) pixels[i] = BLACK; // clear pixel buffer + step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; + #ifdef WLED_ENABLE_GIF + endImagePlayback(this); + #endif } CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { - if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; - if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // TODO remove strip dependency by moving customPalettes out of strip + // there is one randomy generated palette (1) followed by 4 palettes created from segment colors (2-5) + // those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71) + // then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette) + // palette 0 is a varying palette depending on effect and may be replaced by segment's color if so + // instructed in color_from_palette() + if (pal >= FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette //default palette. Differs depending on effect - if (pal == 0) 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 - } + if (pal == 0) pal = _default_palette; // _default_palette is set in setMode() switch (pal) { case 0: //default palette. Exceptions for specific effects above - targetPalette = PartyColors_p; break; + targetPalette = PartyColors_p; + break; case 1: //randomly generated palette - targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette() + targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette() break; case 2: {//primary color only - CRGB prim = gamma32(colors[0]); - targetPalette = CRGBPalette16(prim); break;} + CRGB prim = colors[0]; + targetPalette = CRGBPalette16(prim); + break;} case 3: {//primary + secondary - CRGB prim = gamma32(colors[0]); - CRGB sec = gamma32(colors[1]); - targetPalette = CRGBPalette16(prim,prim,sec,sec); break;} + CRGB prim = colors[0]; + CRGB sec = colors[1]; + targetPalette = CRGBPalette16(prim,prim,sec,sec); + break;} case 4: {//primary + secondary + tertiary - CRGB prim = gamma32(colors[0]); - CRGB sec = gamma32(colors[1]); - CRGB ter = gamma32(colors[2]); - targetPalette = CRGBPalette16(ter,sec,prim); break;} + CRGB prim = colors[0]; + CRGB sec = colors[1]; + CRGB ter = colors[2]; + targetPalette = CRGBPalette16(ter,sec,prim); + break;} case 5: {//primary + secondary (+tertiary if not off), more distinct - CRGB prim = gamma32(colors[0]); - CRGB sec = gamma32(colors[1]); + CRGB prim = colors[0]; + CRGB sec = colors[1]; if (colors[2]) { - CRGB ter = gamma32(colors[2]); + CRGB ter = colors[2]; targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,ter,ter,ter,ter,ter,prim); } else { targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,sec,sec,sec); } break;} - case 6: //Party colors - targetPalette = PartyColors_p; break; - case 7: //Cloud colors - targetPalette = CloudColors_p; break; - case 8: //Lava colors - targetPalette = LavaColors_p; break; - case 9: //Ocean colors - targetPalette = OceanColors_p; break; - case 10: //Forest colors - targetPalette = ForestColors_p; break; - case 11: //Rainbow colors - targetPalette = RainbowColors_p; break; - case 12: //Rainbow stripe colors - targetPalette = RainbowStripeColors_p; break; default: //progmem palettes - if (pal>245) { - targetPalette = strip.customPalettes[255-pal]; // we checked bounds above + if (pal > 255 - customPalettes.size()) { + targetPalette = customPalettes[255-pal]; // we checked bounds above + } else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes + targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT]; } else { byte tcp[72]; - memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72); + memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp)); targetPalette.loadDynamicGradientPalette(tcp); } break; @@ -263,212 +280,153 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { return targetPalette; } -void Segment::startTransition(uint16_t dur) { - if (dur == 0) { - if (isInTransition()) _t->_dur = dur; // this will stop transition in next handleTransition() +// starting a transition has to occur before change so we get current values 1st +void Segment::startTransition(uint16_t dur, bool segmentCopy) { + if (dur == 0 || !isActive()) { + if (isInTransition()) _t->_dur = 0; return; } - if (isInTransition()) return; // already in transition no need to store anything - - // starting a transition has to occur before change so we get current values 1st - _t = new Transition(dur); // no previous transition running - if (!_t) return; // failed to allocate data - - //DEBUG_PRINTF_P(PSTR("-- Started transition: %p (%p)\n"), this, _t); - loadPalette(_t->_palT, palette); - _t->_briT = on ? opacity : 0; - _t->_cctT = cct; -#ifndef WLED_DISABLE_MODE_BLEND - if (modeBlending) { - swapSegenv(_t->_segT); - _t->_modeT = mode; - _t->_segT._dataLenT = 0; - _t->_segT._dataT = nullptr; - if (_dataLen > 0 && data) { - _t->_segT._dataT = (byte *)malloc(_dataLen); - if (_t->_segT._dataT) { - //DEBUG_PRINTF_P(PSTR("-- Allocated duplicate data (%d) for %p: %p\n"), _dataLen, this, _t->_segT._dataT); - memcpy(_t->_segT._dataT, data, _dataLen); - _t->_segT._dataLenT = _dataLen; + if (isInTransition()) { + if (segmentCopy && !_t->_oldSegment) { + // already in transition but segment copy requested and not yet created + _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + _t->_start = millis(); // restart countdown + _t->_dur = dur; + _t->_prevPaletteBlends = 0; + if (_t->_oldSegment) { + _t->_oldSegment->palette = _t->_palette; // restore original palette and colors (from start of transition) + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = _t->_colors[i]; + DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); + if (!_t->_oldSegment->isActive()) stopTransition(); } } - } else { - for (size_t i=0; i_segT._colorT[i] = colors[i]; + return; } -#else - for (size_t i=0; i_colorT[i] = colors[i]; -#endif + + // no previous transition running, start by allocating memory for segment copy + _t = new(std::nothrow) Transition(dur); + if (_t) { + _t->_bri = on ? opacity : 0; + _t->_cct = cct; + _t->_palette = palette; + #ifndef WLED_SAVE_RAM + loadPalette(_t->_palT, palette); + #endif + for (int i=0; i_colors[i] = colors[i]; + if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + if (_t->_oldSegment) { + DEBUGFX_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); + if (!_t->_oldSegment->isActive()) stopTransition(); + } else { + DEBUGFX_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t); + } + }; } void Segment::stopTransition() { - if (isInTransition()) { - //DEBUG_PRINTF_P(PSTR("-- Stopping transition: %p\n"), this); - #ifndef WLED_DISABLE_MODE_BLEND - if (_t->_segT._dataT && _t->_segT._dataLenT > 0) { - //DEBUG_PRINTF_P(PSTR("-- Released duplicate data (%d) for %p: %p\n"), _t->_segT._dataLenT, this, _t->_segT._dataT); - free(_t->_segT._dataT); - _t->_segT._dataT = nullptr; - _t->_segT._dataLenT = 0; - } - #endif - delete _t; - _t = nullptr; - } + DEBUG_PRINTF_P(PSTR("-- Stopping transition: S=%p T(%p) O[%p]\n"), this, _t, _t->_oldSegment); + delete _t; + _t = nullptr; } -// transition progression between 0-65535 -uint16_t IRAM_ATTR Segment::progress() const { +// sets transition progress variable (0-65535) based on time passed since transition start +void Segment::updateTransitionProgress() const { if (isInTransition()) { + _t->_progress = 0xFFFF; unsigned diff = millis() - _t->_start; - if (_t->_dur > 0 && diff < _t->_dur) return diff * 0xFFFFU / _t->_dur; - } - return 0xFFFFU; -} - -#ifndef WLED_DISABLE_MODE_BLEND -void Segment::swapSegenv(tmpsegd_t &tmpSeg) { - //DEBUG_PRINTF_P(PSTR("-- Saving temp seg: %p->(%p) [%d->%p]\n"), this, &tmpSeg, _dataLen, data); - tmpSeg._optionsT = options; - for (size_t i=0; i_segT)) { - // swap SEGENV with transitional data - options = _t->_segT._optionsT; - for (size_t i=0; i_segT._colorT[i]; - speed = _t->_segT._speedT; - intensity = _t->_segT._intensityT; - custom1 = _t->_segT._custom1T; - custom2 = _t->_segT._custom2T; - custom3 = _t->_segT._custom3T; - check1 = _t->_segT._check1T; - check2 = _t->_segT._check2T; - check3 = _t->_segT._check3T; - aux0 = _t->_segT._aux0T; - aux1 = _t->_segT._aux1T; - step = _t->_segT._stepT; - call = _t->_segT._callT; - data = _t->_segT._dataT; - _dataLen = _t->_segT._dataLenT; - } -} - -void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { - //DEBUG_PRINTF_P(PSTR("-- Restoring temp seg: %p->(%p) [%d->%p]\n"), &tmpSeg, this, _dataLen, data); - if (_t && &(_t->_segT) != &tmpSeg) { - // update possibly changed variables to keep old effect running correctly - _t->_segT._aux0T = aux0; - _t->_segT._aux1T = aux1; - _t->_segT._stepT = step; - _t->_segT._callT = call; - //if (_t->_segT._dataT != data) DEBUG_PRINTF_P(PSTR("--- data re-allocated: (%p) %p -> %p\n"), this, _t->_segT._dataT, data); - _t->_segT._dataT = data; - _t->_segT._dataLenT = _dataLen; - } - options = tmpSeg._optionsT; - for (size_t i=0; i_dur > 0 && diff < _t->_dur) _t->_progress = diff * 0xFFFFU / _t->_dur; + } } -#endif -uint8_t IRAM_ATTR Segment::currentBri(bool useCct) const { +// will return segment's CCT during a transition +// isPreviousMode() is actually not implemented for CCT in strip.service() as WLED does not support per-pixel CCT +uint8_t Segment::currentCCT() const { unsigned prog = progress(); if (prog < 0xFFFFU) { - unsigned curBri = (useCct ? cct : (on ? opacity : 0)) * prog; - curBri += (useCct ? _t->_cctT : _t->_briT) * (0xFFFFU - prog); - return curBri / 0xFFFFU; + if (blendingStyle == BLEND_STYLE_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU; + //else return Segment::isPreviousMode() ? _t->_cct : cct; } - return (useCct ? cct : (on ? opacity : 0)); + return cct; } -uint8_t Segment::currentMode() const { -#ifndef WLED_DISABLE_MODE_BLEND +// will return segment's opacity during a transition (blending it with old in case of FADE transition) +uint8_t Segment::currentBri() const { unsigned prog = progress(); - if (modeBlending && prog < 0xFFFFU) return _t->_modeT; -#endif - return mode; -} - -uint32_t IRAM_ATTR_YN Segment::currentColor(uint8_t slot) const { - if (slot >= NUM_COLORS) slot = 0; -#ifndef WLED_DISABLE_MODE_BLEND - return isInTransition() ? color_blend(_t->_segT._colorT[slot], colors[slot], progress(), true) : colors[slot]; -#else - return isInTransition() ? color_blend(_t->_colorT[slot], colors[slot], progress(), true) : colors[slot]; -#endif -} - -void Segment::setCurrentPalette() { - loadPalette(_currentPalette, palette); - unsigned prog = progress(); - if (strip.paletteFade && prog < 0xFFFFU) { + unsigned curBri = on ? opacity : 0; + if (prog < 0xFFFFU) { + // this will blend opacity in new mode if style is FADE (single effect call) + if (blendingStyle == BLEND_STYLE_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; + else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; + } + return curBri; +} + +// pre-calculate drawing parameters for faster access (based on the idea from @softhack007 from MM fork) +// and blends colors and palettes if necessary +// prog is the progress of the transition (0-65535) and is passed to the function as it may be called in the context of old segment +// which does not have transition structure +void Segment::beginDraw(uint16_t prog) { + setDrawDimensions(); + // load colors into _currentColors + for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = colors[i]; + // load palette into _currentPalette + loadPalette(Segment::_currentPalette, palette); + if (isInTransition() && prog < 0xFFFFU && blendingStyle == BLEND_STYLE_FADE) { + // blend colors + for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog); // 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 + #ifndef WLED_SAVE_RAM unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); - _currentPalette = _t->_palT; // copy transitioning/temporary palette + if(noOfBlends > 255) noOfBlends = 255; // safety check + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48); + Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette + #else + unsigned noOfBlends = ((255U * prog) / 0xFFFFU); + CRGBPalette16 tmpPalette; + loadPalette(tmpPalette, _t->_palette); + for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(tmpPalette, Segment::_currentPalette, 48); + Segment::_currentPalette = tmpPalette; // copy transitioning/temporary palette + #endif } } // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { + unsigned long now = millis(); + uint16_t now_s = now / 1000; // we only need seconds (and @dedehai hated shift >> 10) + now = (now_s)*1000 + (now % 1000); // ignore days (now is limited to 18 hours as now_s can only store 65535s ~ 18h 12min) + if (now_s < Segment::_lastPaletteChange) Segment::_lastPaletteChange = 0; // handle overflow (will cause 2*randomPaletteChangeTime glitch at most) // is it time to generate a new palette? - if ((uint16_t)((uint16_t)(millis() / 1000U) - _lastPaletteChange) > randomPaletteChangeTime){ - _newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(_randomPalette) : generateRandomPalette(); - _lastPaletteChange = (uint16_t)(millis() / 1000U); - _lastPaletteBlend = (uint16_t)((uint16_t)millis() - 512); // starts blending immediately - } - - // if palette transitions is enabled, blend it according to Transition Time (if longer than minimum given by service calls) - if (strip.paletteFade) { - // assumes that 128 updates are sufficient to blend a palette, so shift by 7 (can be more, can be less) - // in reality there need to be 255 blends to fully blend two entirely different palettes - if ((uint16_t)((uint16_t)millis() - _lastPaletteBlend) < strip.getTransition() >> 7) return; // not yet time to fade, delay the update - _lastPaletteBlend = (uint16_t)millis(); - } - nblendPaletteTowardPalette(_randomPalette, _newRandomPalette, 48); -} - -// segId is given when called from network callback, changes are queued if that segment is currently in its effect function -void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y) { + if (now_s > Segment::_lastPaletteChange + randomPaletteChangeTime) { + Segment::_newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(Segment::_randomPalette) : generateRandomPalette(); + Segment::_lastPaletteChange = now_s; + Segment::_nextPaletteBlend = now; // starts blending immediately + } + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in strip.getTransition() time) + // if randomPaletteChangeTime is shorter than strip.getTransition() palette will never fully blend + unsigned frameTime = strip.getFrameTime(); // in ms [8-1000] + unsigned transitionTime = strip.getTransition(); // in ms [100-65535] + if ((uint16_t)now < Segment::_nextPaletteBlend || now > ((Segment::_lastPaletteChange*1000) + transitionTime + 2*frameTime)) return; // not yet time or past transition time, no need to blend + unsigned transitionFrames = frameTime > transitionTime ? 1 : transitionTime / frameTime; // i.e. 700ms/23ms = 30 or 20000ms/8ms = 2500 or 100ms/1000ms = 0 -> 1 + unsigned noOfBlends = transitionFrames > 255 ? 1 : (255 + (transitionFrames>>1)) / transitionFrames; // we do some rounding here + for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(Segment::_randomPalette, Segment::_newRandomPalette, 48); + Segment::_nextPaletteBlend = now + ((transitionFrames >> 8) * frameTime); // postpone next blend if necessary +} + +// sets Segment geometry (length or width/height and grouping, spacing and offset as well as 2D mapping) +// strip must be suspended (strip.suspend()) before calling this function +// this function may call fill() to clear pixels if spacing or mapping changed (which requires setting _vWidth, _vHeight, _vLength or beginDraw()) +void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y, uint8_t m12) { // 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 + boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D #endif - if (boundsUnchanged - && (!grp || (grouping == grp && spacing == spc)) - && (ofs == UINT16_MAX || ofs == offset)) return; + boundsUnchanged &= (grouping == grp && spacing == spc); // changing grouping and/or spacing changes virtual segment length (painting dimensions) - stateChanged = true; // send UDP/WS broadcast - - if (stop) fill(BLACK); // turn old segment range off (clears pixels if changing spacing) + if (stop && (spc > 0 || m12 != map1D2D)) clear(); if (grp) { // prevent assignment of 0 grouping = grp; spacing = spc; @@ -477,34 +435,66 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t spacing = 0; } if (ofs < UINT16_MAX) offset = ofs; + map1D2D = constrain(m12, 0, 7); - DEBUG_PRINT(F("setUp segment: ")); DEBUG_PRINT(i1); - DEBUG_PRINT(','); DEBUG_PRINT(i2); - DEBUG_PRINT(F(" -> ")); DEBUG_PRINT(i1Y); - DEBUG_PRINT(','); DEBUG_PRINTLN(i2Y); - markForReset(); if (boundsUnchanged) return; + unsigned oldLength = length(); + + DEBUGFX_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d [%d,%d]\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y, (int)grp, (int)spc); + markForReset(); + if (_t) stopTransition(); // we can't use transition if segment dimensions changed + stateChanged = true; // send UDP/WS broadcast + // apply change immediately if (i2 <= i1) { //disable segment + #ifdef WLED_ENABLE_GIF + endImagePlayback(this); + #endif + deallocateData(); + p_free(pixels); + pixels = nullptr; 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)); + stop = i2 > Segment::maxWidth*Segment::maxHeight && i1 >= Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth); // check for 2D trailing strip startY = 0; stopY = 1; #ifndef WLED_DISABLE_2D if (Segment::maxHeight>1) { // 2D if (i1Y < Segment::maxHeight) startY = i1Y; - stopY = i2Y > Segment::maxHeight ? Segment::maxHeight : MAX(1,i2Y); + stopY = constrain(i2Y, 1, Segment::maxHeight); } #endif // safety check if (start >= stop || startY >= stopY) { + #ifdef WLED_ENABLE_GIF + endImagePlayback(this); + #endif + deallocateData(); + p_free(pixels); + pixels = nullptr; stop = 0; return; } + // allocate FX render buffer + if (length() != oldLength) { + // allocate render buffer (always entire segment), prefer IRAM/PSRAM. Note: impact on FPS with PSRAM buffer is low (<2% with QSPI PSRAM) on S2/S3 + p_free(pixels); + pixels = static_cast(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS)); + if (!pixels) { + DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + #ifdef WLED_ENABLE_GIF + endImagePlayback(this); + #endif + deallocateData(); + errorFlag = ERR_NORAM_PX; + stop = 0; + return; + } + + } refreshLightCapabilities(); } @@ -515,7 +505,8 @@ Segment &Segment::setColor(uint8_t slot, uint32_t c) { if (slot == 0 && c == BLACK) return *this; // on/off segment cannot have primary color black if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black } - if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change + //DEBUG_PRINTF_P(PSTR("- Starting color transition: %d [0x%X]\n"), slot, c); + startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return *this; @@ -528,8 +519,8 @@ Segment &Segment::setCCT(uint16_t k) { k = (k - 1900) >> 5; } if (cct != k) { - //DEBUGFX_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); - startTransition(strip.getTransition()); // start transition prior to change + //DEBUG_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); + startTransition(strip.getTransition(), false); // start transition prior to change (no need to copy segment) cct = k; stateChanged = true; // send UDP/WS broadcast } @@ -538,8 +529,8 @@ Segment &Segment::setCCT(uint16_t k) { Segment &Segment::setOpacity(uint8_t o) { if (opacity != o) { - //DEBUGFX_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); - startTransition(strip.getTransition()); // start transition prior to change + //DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); + startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change opacity = o; stateChanged = true; // send UDP/WS broadcast } @@ -547,11 +538,13 @@ Segment &Segment::setOpacity(uint8_t o) { } Segment &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 + bool prev = (options >> n) & 0x01; + if (val == prev) return *this; + //DEBUG_PRINTF_P(PSTR("- Starting option transition: %d\n"), n); + if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change if (val) options |= 0x01 << n; else options &= ~(0x01 << n); - if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET)) stateChanged = true; // send UDP/WS broadcast + stateChanged = true; // send UDP/WS broadcast return *this; } @@ -561,13 +554,11 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { if (fx >= strip.getModeCount()) fx = 0; // set solid mode // if we have a valid mode & is not reserved if (fx != mode) { -#ifndef WLED_DISABLE_MODE_BLEND - if (modeBlending) startTransition(strip.getTransition()); // set effect transitions -#endif + startTransition(strip.getTransition(), true); // set effect transitions (must create segment copy) mode = fx; + int sOpt; // load default values from effect string if (loadDefaults) { - int 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; @@ -582,8 +573,11 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { 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); } + sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette + if (sOpt >= 0 && loadDefaults) setPalette(sOpt); + if (sOpt <= 0) sOpt = 6; // partycolors if zero or not set + _default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected) markForReset(); stateChanged = true; // send UDP/WS broadcast } @@ -591,25 +585,39 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { } Segment &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 <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette if (pal != palette) { - if (strip.paletteFade) startTransition(strip.getTransition()); + //DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal); + startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment) palette = pal; stateChanged = true; // send UDP/WS broadcast } return *this; } +Segment &Segment::setName(const char *newName) { + if (newName) { + const int newLen = min(strlen(newName), (size_t)WLED_MAX_SEGNAME_LEN); + if (newLen) { + if (name) p_free(name); // free old name + name = static_cast(allocate_buffer(newLen+1, BFRALLOC_PREFER_PSRAM)); + if (mode == FX_MODE_2DSCROLLTEXT) startTransition(strip.getTransition(), true); // if the name changes in scrolling text mode, we need to copy the segment for blending + if (name) strlcpy(name, newName, newLen+1); + return *this; + } + } + return clearName(); +} + // 2D matrix -unsigned IRAM_ATTR Segment::virtualWidth() const { +unsigned Segment::virtualWidth() const { unsigned groupLen = groupLength(); unsigned 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; } -unsigned IRAM_ATTR Segment::virtualHeight() const { +unsigned Segment::virtualHeight() const { unsigned groupLen = groupLength(); unsigned 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 @@ -618,42 +626,30 @@ unsigned IRAM_ATTR Segment::virtualHeight() const { // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D -constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16 -constexpr int Pinwheel_Size_Small = 16; // larger than this -> use "Medium" -constexpr int Pinwheel_Steps_Medium = 192; // no holes up to 32x32 -constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" -constexpr int Pinwheel_Steps_Big = 304; // no holes up to 50x50 -constexpr int Pinwheel_Size_Big = 50; // larger than this -> use "XL" -constexpr int Pinwheel_Steps_XL = 368; -constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...72 to Radians -constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...192 to Radians -constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...304 to Radians -constexpr float Int_to_Rad_XL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XL; // conversion: from 0...368 to Radians - -constexpr int Fixed_Scale = 512; // fixpoint scaling factor (9bit for fraction) - -// Pinwheel helper function: pixel index to radians -static float getPinwheelAngle(int i, int vW, int vH) { - int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return float(i) * Int_to_Rad_Small; - if (maxXY <= Pinwheel_Size_Medium) return float(i) * Int_to_Rad_Med; - if (maxXY <= Pinwheel_Size_Big) return float(i) * Int_to_Rad_Big; - // else - return float(i) * Int_to_Rad_XL; -} +constexpr int Fixed_Scale = 16384; // fixpoint scaling factor (14bit for fraction) // Pinwheel helper function: matrix dimensions to number of rays static int getPinwheelLength(int vW, int vH) { - int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return Pinwheel_Steps_Small; - if (maxXY <= Pinwheel_Size_Medium) return Pinwheel_Steps_Medium; - if (maxXY <= Pinwheel_Size_Big) return Pinwheel_Steps_Big; - // else - return Pinwheel_Steps_XL; + // Returns multiple of 8, prevents over drawing + return (max(vW, vH) + 15) & ~7; +} +static void setPinwheelParameters(int i, int vW, int vH, int& startx, int& starty, int* cosVal, int* sinVal, bool getPixel = false) { + int steps = getPinwheelLength(vW, vH); + int baseAngle = ((0xFFFF + steps / 2) / steps); // 360° / steps, in 16 bit scale round to nearest integer + int rotate = 0; + if (getPixel) rotate = baseAngle / 2; // rotate by half a ray width when reading pixel color + for (int k = 0; k < 2; k++) // angular steps for two consecutive rays + { + int angle = (i + k) * baseAngle + rotate; + cosVal[k] = (cos16_t(angle) * Fixed_Scale) >> 15; // step per pixel in fixed point, cos16 output is -0x7FFF to +0x7FFF + sinVal[k] = (sin16_t(angle) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable) + } + startx = (vW * Fixed_Scale) / 2; // + cosVal[0] / 4; // starting position = center + 1/4 pixel (in fixed point) + starty = (vH * Fixed_Scale) / 2; // + sinVal[0] / 4; } #endif // 1D strip -uint16_t IRAM_ATTR Segment::virtualLength() const { +uint16_t Segment::virtualLength() const { #ifndef WLED_DISABLE_2D if (is2D()) { unsigned vW = virtualWidth(); @@ -667,7 +663,7 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { vLen = max(vW,vH); // get the longest dimension break; case M12_pArc: - vLen = sqrt16(vH*vH + vW*vW); // use diagonal + vLen = sqrt32_bw(vH*vH + vW*vW); // use diagonal break; case M12_sPinwheel: vLen = getPinwheelLength(vW, vH); @@ -685,34 +681,71 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { return vLength; } -void IRAM_ATTR_YN 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) +// maximum length of a mapped 1D segment, used in PS for buffer allocation +uint16_t Segment::maxMappingLength() const { + uint32_t vW = virtualWidth(); + uint32_t vH = virtualHeight(); + return max(sqrt32_bw(vH*vH + vW*vW), (uint32_t)getPinwheelLength(vW, vH)); // use diagonal +} #endif - i &= 0xFFFF; +// pixel is clipped if it falls outside clipping range +// if clipping start > stop the clipping range is inverted +bool Segment::isPixelClipped(int i) const { + if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) { + bool invert = _clipStart > _clipStop; // ineverted start & stop + int start = invert ? _clipStop : _clipStart; + int stop = invert ? _clipStart : _clipStop; + if (blendingStyle == BLEND_STYLE_FAIRY_DUST) { + unsigned len = stop - start; + if (len < 2) return false; + unsigned shuffled = hashInt(i) % len; + unsigned pos = (shuffled * 0xFFFFU) / len; + return progress() <= pos; + } + const bool iInside = (i >= start && i < stop); + return !iInside ^ invert; // thanks @willmmiles (https://github.com/wled/WLED/pull/3877#discussion_r1554633876) + } + return false; +} - if (i >= virtualLength() || i<0) return; // if pixel would fall out of segment just exit +void WLED_O2_ATTR Segment::setPixelColor(int i, uint32_t col) const +{ + if (!isActive() || i < 0) return; // not active or invalid index +#ifndef WLED_DISABLE_2D + int vStrip = 0; +#endif + const int vL = vLength(); + // if the 1D effect is using virtual strips "i" will have virtual strip id stored in upper 16 bits + // in such case "i" will be > virtualLength() + if (i >= vL) { + // check if this is a virtual strip + #ifndef WLED_DISABLE_2D + vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) + #endif + i &= 0xFFFF; // truncate vstrip index. note: vStrip index is 1 even in 1D, still need to truncate + if (i >= vL) return; // if pixel would still fall out of segment just exit + } #ifndef WLED_DISABLE_2D if (is2D()) { - int vH = virtualHeight(); // segment height in logical pixels - int vW = virtualWidth(); + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + const auto XY = [&](unsigned x, unsigned y){ return x + y*vW;}; switch (map1D2D) { case M12_Pixels: // use all available pixels as a long strip - setPixelColorXY(i % vW, i / vW, col); + setPixelColorRaw(XY(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); + if (vStrip > 0) setPixelColorRaw(XY(vStrip - 1, vH - i - 1), col); + else for (int x = 0; x < vW; x++) setPixelColorRaw(XY(x, vH - i - 1), col); break; case M12_pArc: // expand in circular fashion from center - if (i==0) - setPixelColorXY(0, 0, col); + if (i == 0) + setPixelColorRaw(XY(0, 0), col); else { float r = i; float step = HALF_PI / (2.8284f * r + 4); // we only need (PI/4)/(r/sqrt(2)+1) steps @@ -740,113 +773,123 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) } break; case M12_pCorner: - for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); + for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); // note: <= to include i=0. Relies on overflow check in sPC() for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); break; case M12_sPinwheel: { - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); - - // avoid re-painting the same pixel - int lastX = INT_MIN; // impossible position - int lastY = INT_MIN; // impossible position - // draw line at angle, starting at center and ending at the segment edge - // we use fixed point math for better speed. Starting distance is 0.5 for better rounding - // int_fast16_t and int_fast32_t types changed to int, minimum bits commented - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit - - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint - - // Odd rays start further from center if prevRay started at center. - static int prevRay = INT_MIN; // previous ray number - if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) { - int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel - posx += inc_x * jump; - posy += inc_y * jump; + // Uses Bresenham's algorithm to place coordinates of two lines in arrays then draws between them + int startX, startY, cosVal[2], sinVal[2]; // in fixed point scale + setPinwheelParameters(i, vW, vH, startX, startY, cosVal, sinVal); + + unsigned maxLineLength = max(vW, vH) + 2; // pixels drawn is always smaller than dx or dy, +1 pair for rounding errors + uint16_t lineCoords[2][maxLineLength]; // uint16_t to save ram + int lineLength[2] = {0}; + + static int prevRays[2] = {INT_MAX, INT_MAX}; // previous two ray numbers + int closestEdgeIdx = INT_MAX; // index of the closest edge pixel + + for (int lineNr = 0; lineNr < 2; lineNr++) { + int x0 = startX; // x, y coordinates in fixed scale + int y0 = startY; + int x1 = (startX + (cosVal[lineNr] << 9)); // outside of grid + int y1 = (startY + (sinVal[lineNr] << 9)); // outside of grid + const int dx = abs(x1-x0), sx = x0= (unsigned)vW || (unsigned)y0 >= (unsigned)vH) { + closestEdgeIdx = min(closestEdgeIdx, idx-2); + break; // stop if outside of grid (exploit unsigned int overflow) + } + coordinates[idx++] = x0; + coordinates[idx++] = y0; + (*length)++; + // note: since endpoint is out of grid, no need to check if endpoint is reached + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } } - prevRay = i; - - // draw ray until we hit any edge - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - int x = posx / Fixed_Scale; - int y = posy / Fixed_Scale; - // set pixel - if (x != lastX || y != lastY) setPixelColorXY(x, y, col); // only paint if pixel position is different - lastX = x; - lastY = y; - // advance to next position - posx += inc_x; - posy += inc_y; + + // fill up the shorter line with missing coordinates, so block filling works correctly and efficiently + int diff = lineLength[0] - lineLength[1]; + int longLineIdx = (diff > 0) ? 0 : 1; + int shortLineIdx = longLineIdx ? 0 : 1; + if (diff != 0) { + int idx = (lineLength[shortLineIdx] - 1) * 2; // last valid coordinate index + int lastX = lineCoords[shortLineIdx][idx++]; + int lastY = lineCoords[shortLineIdx][idx++]; + bool keepX = lastX == 0 || lastX == vW - 1; + for (int d = 0; d < abs(diff); d++) { + lineCoords[shortLineIdx][idx] = keepX ? lastX :lineCoords[longLineIdx][idx]; + idx++; + lineCoords[shortLineIdx][idx] = keepX ? lineCoords[longLineIdx][idx] : lastY; + idx++; + } + } + + // draw and block-fill the line coordinates. Note: block filling only efficient if angle between lines is small + closestEdgeIdx += 2; + int max_i = getPinwheelLength(vW, vH) - 1; + bool drawFirst = !(prevRays[0] == i - 1 || (i == 0 && prevRays[0] == max_i)); // draw first line if previous ray was not adjacent including wrap + bool drawLast = !(prevRays[0] == i + 1 || (i == max_i && prevRays[0] == 0)); // same as above for last line + for (int idx = 0; idx < lineLength[longLineIdx] * 2;) { //!! should be long line idx! + int x1 = lineCoords[0][idx]; + int x2 = lineCoords[1][idx++]; + int y1 = lineCoords[0][idx]; + int y2 = lineCoords[1][idx++]; + int minX, maxX, minY, maxY; + (x1 < x2) ? (minX = x1, maxX = x2) : (minX = x2, maxX = x1); + (y1 < y2) ? (minY = y1, maxY = y2) : (minY = y2, maxY = y1); + + // fill the block between the two x,y points + bool alwaysDraw = (drawFirst && drawLast) || // No adjacent rays, draw all pixels + (idx > closestEdgeIdx) || // Edge pixels on uneven lines are always drawn + (i == 0 && idx == 2) || // Center pixel special case + (i == prevRays[1]); // Effect drawing twice in 1 frame + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + bool onLine1 = x == x1 && y == y1; + bool onLine2 = x == x2 && y == y2; + if ((alwaysDraw) || + (!onLine1 && (!onLine2 || drawLast)) || // Middle pixels and line2 if drawLast + (!onLine2 && (!onLine1 || drawFirst)) // Middle pixels and line1 if drawFirst + ) { + setPixelColorXY(x, y, col); + } + } + } } + prevRays[1] = prevRays[0]; + prevRays[0] = i; break; } } return; - } else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) { + } 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; + if (vHeight() > 1) y = i; + if (vWidth() > 1) x = i; setPixelColorXY(x, y, col); return; } } #endif - - unsigned len = length(); - uint8_t _bri_t = currentBri(); - if (_bri_t < 255) { - col = color_fade(col, _bri_t); - } - - // expand pixel (taking into account start, grouping, spacing [and offset]) - i = i * groupLength(); - if (reverse) { // is segment reversed? - if (mirror) { // is segment mirrored? - i = (len - 1) / 2 - i; //only need to index half the pixels - } else { - i = (len - 1) - i; - } - } - i += start; // starting pixel in a group - - uint32_t tmpCol = col; - // set all the pixels in the group - for (int j = 0; j < grouping; j++) { - unsigned indexSet = i + ((reverse) ? -j : j); - if (indexSet >= start && indexSet < stop) { - if (mirror) { //set the corresponding mirrored pixel - unsigned indexMir = stop - indexSet + start - 1; - indexMir += offset; // offset/phase - if (indexMir >= stop) indexMir -= len; // wrap -#ifndef WLED_DISABLE_MODE_BLEND - if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexMir), col, 0xFFFFU - progress(), true); -#endif - strip.setPixelColor(indexMir, tmpCol); - } - indexSet += offset; // offset/phase - if (indexSet >= stop) indexSet -= len; // wrap -#ifndef WLED_DISABLE_MODE_BLEND - if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexSet), col, 0xFFFFU - progress(), true); -#endif - strip.setPixelColor(indexSet, tmpCol); - } - } + setPixelColorRaw(i, col); } #ifdef WLED_USE_AA_PIXELS // anti-aliased normalized version of setPixelColor() -void Segment::setPixelColor(float i, uint32_t col, bool aa) +void Segment::setPixelColor(float i, uint32_t col, bool aa) const { if (!isActive()) return; // not active int vStrip = int(i/10.0f); // hack to allow running on virtual strips (2D segment columns/rows) @@ -879,148 +922,93 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) } #endif -uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const +uint32_t WLED_O2_ATTR Segment::getPixelColor(int i) const { - if (!isActive()) return 0; // not active + if (!isActive() || i < 0) return 0; // not active or invalid index + #ifndef WLED_DISABLE_2D - int vStrip = i>>16; -#endif + int vStrip = i>>16; // virtual strips are only relevant in Bar expansion mode i &= 0xFFFF; +#endif + if (i >= (int)vLength()) return 0; #ifndef WLED_DISABLE_2D if (is2D()) { - int vH = virtualHeight(); // segment height in logical pixels - int vW = virtualWidth(); + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + int x = 0, y = 0; switch (map1D2D) { case M12_Pixels: - return getPixelColorXY(i % vW, i / vW); + x = i % vW; + y = i / vW; break; case M12_pBar: - if (vStrip>0) return getPixelColorXY(vStrip - 1, vH - i -1); - else return getPixelColorXY(0, vH - i -1); + if (vStrip > 0) { x = vStrip - 1; y = vH - i - 1; } + else { y = vH - i - 1; }; break; case M12_pArc: - if (i >= vW && i >= vH) { - unsigned vI = sqrt16(i*i/2); - return getPixelColorXY(vI,vI); // use diagonal + if (i > vW && i > vH) { + x = y = sqrt32_bw(i*i/2); + break; // use diagonal } + // otherwise fallthrough case M12_pCorner: // use longest dimension - return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); + if (vW > vH) x = i; + else y = i; break; - case M12_sPinwheel: + case M12_sPinwheel: { // not 100% accurate, returns pixel at outer edge - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); - - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint - - // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor - int x = INT_MIN; - int y = INT_MIN; - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - x = posx / Fixed_Scale; - y = posy / Fixed_Scale; - // advance to next position - posx += inc_x; - posy += inc_y; + int cosVal[2], sinVal[2]; + setPinwheelParameters(i, vW, vH, x, y, cosVal, sinVal, true); + int maxX = (vW-1) * Fixed_Scale; + int maxY = (vH-1) * Fixed_Scale; + // trace ray from center until we hit any edge - to avoid rounding problems, we use fixed point coordinates + while ((x < maxX) && (y < maxY) && (x > Fixed_Scale) && (y > Fixed_Scale)) { + x += cosVal[0]; // advance to next position + y += sinVal[0]; } - return getPixelColorXY(x, y); + x /= Fixed_Scale; + y /= Fixed_Scale; break; } - return 0; + } + return getPixelColorXY(x, y); } #endif + return getPixelColorRaw(i); +} - if (reverse) i = virtualLength() - i - 1; - i *= groupLength(); - i += start; - // offset/phase - i += offset; - if (i >= stop) i -= length(); - return strip.getPixelColor(i); -} - -uint8_t Segment::differs(Segment& b) const { - uint8_t d = 0; - if (start != b.start) d |= SEG_DIFFERS_BOUNDS; - if (stop != b.stop) d |= SEG_DIFFERS_BOUNDS; - if (offset != b.offset) d |= SEG_DIFFERS_GSO; - if (grouping != b.grouping) d |= SEG_DIFFERS_GSO; - if (spacing != b.spacing) d |= SEG_DIFFERS_GSO; - if (opacity != b.opacity) d |= SEG_DIFFERS_BRI; - if (mode != b.mode) d |= SEG_DIFFERS_FX; - if (speed != b.speed) d |= SEG_DIFFERS_FX; - if (intensity != b.intensity) d |= SEG_DIFFERS_FX; - if (palette != b.palette) d |= SEG_DIFFERS_FX; - if (custom1 != b.custom1) d |= SEG_DIFFERS_FX; - if (custom2 != b.custom2) d |= SEG_DIFFERS_FX; - if (custom3 != b.custom3) d |= SEG_DIFFERS_FX; - if (startY != b.startY) d |= SEG_DIFFERS_BOUNDS; - if (stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; - - //bit pattern: (msb first) - // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected] - if ((options & 0b1111111111011110U) != (b.options & 0b1111111111011110U)) d |= SEG_DIFFERS_OPT; - if ((options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; - for (unsigned i = 0; i < NUM_COLORS; i++) if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; - - return d; -} - -void Segment::refreshLightCapabilities() { +void Segment::refreshLightCapabilities() const { unsigned capabilities = 0; - unsigned segStartIdx = 0xFFFFU; - unsigned 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++) { - unsigned index = strip.getMappedPixelIndex(x + Segment::maxWidth * y); // convert logical address to physical - if (index < 0xFFFFU) { - if (segStartIdx > index) segStartIdx = index; - if (segStopIdx < index) segStopIdx = index; + // we must traverse each pixel in segment to determine its capabilities (as pixel may be mapped) + for (unsigned y = startY; y < stopY; y++) for (unsigned x = start; x < stop; x++) { + unsigned index = x + Segment::maxWidth * y; + index = strip.getMappedPixelIndex(index); // convert logical address to physical + if (index == 0xFFFF) continue; // invalid/missing pixel + for (unsigned b = 0; b < BusManager::getNumBusses(); b++) { + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; + if (bus->containsPixel(index)) { + if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; + if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; + if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) + if (bus->hasWhite()) { + unsigned 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; + } + break; } - if (segStartIdx == segStopIdx) segStopIdx++; // we only have 1 pixel segment - } - } else { - // we are on the strip located after the matrix - segStartIdx = start; - segStopIdx = stop; - } - - for (unsigned b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::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; - - if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; - if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; - if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) - if (bus->hasWhite()) { - unsigned 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; @@ -1029,149 +1017,128 @@ void Segment::refreshLightCapabilities() { /* * Fills segment with color */ -void Segment::fill(uint32_t c) { +void Segment::fill(uint32_t c) const { if (!isActive()) return; // not active - const int cols = is2D() ? virtualWidth() : virtualLength(); - const int rows = virtualHeight(); // will be 1 for 1D - for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { - if (is2D()) setPixelColorXY(x, y, c); - else setPixelColor(x, c); - } + for (unsigned i = 0; i < length(); i++) setPixelColorRaw(i,c); // always fill all pixels (blending will take care of grouping, spacing and clipping) } /* * fade out function, higher rate = quicker fade + * fading is highly dependant on frame rate (higher frame rates, faster fading) + * each frame will fade at max 9% or as little as 0.8% */ -void Segment::fade_out(uint8_t rate) { +void Segment::fade_out(uint8_t rate) const { if (!isActive()) return; // not active - const int cols = is2D() ? virtualWidth() : virtualLength(); - const int rows = virtualHeight(); // will be 1 for 1D - - rate = (255-rate) >> 1; - float mappedRate = 1.0f / (float(rate) + 1.1f); - - uint32_t color = colors[1]; // SEGCOLOR(1); // target color - int w2 = W(color); - int r2 = R(color); - int g2 = G(color); - int b2 = B(color); - - for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { - color = is2D() ? getPixelColorXY(x, y) : getPixelColor(x); + rate = (256-rate) >> 1; + const int mappedRate = 256 / (rate + 1); + const size_t rlength = rawLength(); // calculate only once + for (unsigned j = 0; j < rlength; j++) { + uint32_t color = getPixelColorRaw(j); if (color == colors[1]) continue; // already at target color - 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); + for (int i = 0; i < 32; i += 8) { + uint8_t c2 = (colors[1]>>i); // get background channel + uint8_t c1 = (color>>i); // get foreground channel + // we can't use bitshift since we are using int + int delta = (c2 - c1) * mappedRate / 256; + // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) + if (delta == 0) delta += (c2 == c1) ? 0 : (c2 > c1) ? 1 : -1; + // stuff new value back into color + color &= ~(0xFF< 215 this function does not work properly (creates alternating pattern) */ -void Segment::blur(uint8_t blur_amount, bool smear) { +void Segment::blur(uint8_t blur_amount, bool smear) const { if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D - blur2D(blur_amount, smear); + blur2D(blur_amount, blur_amount, smear); // symmetrical 2D blur //box_blur(map(blur_amount,1,255,1,3), smear); return; } #endif uint8_t keep = smear ? 255 : 255 - blur_amount; - uint8_t seep = blur_amount >> (1 + smear); - unsigned vlength = virtualLength(); - uint32_t carryover = BLACK; - uint32_t lastnew; - uint32_t last; - uint32_t curnew = BLACK; - for (unsigned i = 0; i < vlength; i++) { - uint32_t cur = getPixelColor(i); - uint32_t part = color_fade(cur, seep); - curnew = color_fade(cur, keep); - if (i > 0) { - if (carryover) curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); - // optimization: only set pixel if color has changed - if (last != prev) setPixelColor(i - 1, prev); - } else // first pixel - setPixelColor(i, curnew); - lastnew = curnew; - last = cur; // save original value for comparison on next iteration + uint8_t seep = blur_amount >> 1; + unsigned vlength = vLength(); + // handle first pixel to avoid conditional in loop (faster) + uint32_t cur = getPixelColorRaw(0); + uint32_t carryover = fast_color_scale(cur, seep); + setPixelColorRaw(0, fast_color_scale(cur, keep)); + for (unsigned i = 1; i < vlength; i++) { + cur = getPixelColorRaw(i); + uint32_t part = fast_color_scale(cur, seep); + cur = fast_color_scale(cur, keep); + cur = color_add(cur, carryover); + setPixelColorRaw(i - 1, color_add(getPixelColorRaw(i - 1), part)); // previous pixel + setPixelColorRaw(i, cur); // current pixel carryover = part; } - setPixelColor(vlength - 1, curnew); } /* * 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. + * Rotates the color in HSV space, where pos is H. (0=0deg, 256=360deg) */ uint32_t Segment::color_wheel(uint8_t pos) const { - if (palette) return color_from_palette(pos, false, true, 0); // perhaps "strip.paletteBlend < 2" should be better instead of "true" - uint8_t w = W(currentColor(0)); - pos = 255 - pos; - if (pos < 85) { - return RGBW32((255 - pos * 3), 0, (pos * 3), w); - } else if(pos < 170) { - pos -= 85; - return RGBW32(0, (pos * 3), (255 - pos * 3), w); - } else { - pos -= 170; - return RGBW32((pos * 3), (255 - pos * 3), 0, w); - } + if (palette) return color_from_palette(pos, false, false, 0); // only wrap if "always wrap" is set + uint8_t w = W(getCurrentColor(0)); + uint32_t rgb; + hsv2rgb(CHSV32(static_cast(pos << 8), 255, 255), rgb); + return rgb | (w << 24); // add white channel } /* * 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 moving FastLED palettes will usually wrap back to the start smoothly. Set to true if effect has moving palette and you want wrap. * @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) const { - uint32_t color = gamma32(currentColor(mcol)); - +uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool moving, uint8_t mcol, uint8_t pbri) const { + uint32_t color = getCurrentColor(mcol); // default palette or no RGB support on segment - if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) return (pbri == 255) ? color : color_fade(color, pbri, true); + if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) { + return color_fade(color, pbri, true); + } unsigned paletteIndex = i; - if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1); - // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) - if (!wrap && strip.paletteBlend != 3) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" - CRGB fastled_col = ColorFromPalette(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global + if (mapping) paletteIndex = min((i*255)/vLength(), 255U); + // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined/no interpolation of palette entries) + // ColorFromPalette interpolations are: NOBLEND, LINEARBLEND, LINEARBLEND_NOWRAP + TBlendType blend = NOBLEND; + switch (paletteBlend) { + case 0: blend = moving ? LINEARBLEND : LINEARBLEND_NOWRAP; break; + case 1: blend = LINEARBLEND; break; + case 2: blend = LINEARBLEND_NOWRAP; break; + } + CRGBW palcol = ColorFromPalette(_currentPalette, paletteIndex, pbri, blend); + palcol.w = W(color); - return RGBW32(fastled_col.r, fastled_col.g, fastled_col.b, W(color)); + return palcol.color32; } @@ -1191,105 +1158,94 @@ void WS2812FX::finalizeInit() { enumerateLedmaps(); _hasWhiteChannel = _isOffRefreshRequired = false; + BusManager::removeAll(); + // TODO: ideally we would free everything segment related here to reduce fragmentation (pixel buffers, ledamp, segments, etc) but that somehow leads to heap corruption if touchig any of the buffers. + unsigned digitalCount = 0; + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + // validate the bus config: count I2S buses and check if they meet requirements + unsigned i2sBusCount = 0; + + for (const auto &bus : busConfigs) { + if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) { + digitalCount++; + if (bus.driverType == 1) + i2sBusCount++; + } + } + DEBUG_PRINTF_P(PSTR("Digital buses: %u, I2S buses: %u\n"), digitalCount, i2sBusCount); - //if busses failed to load, add default (fresh install, FS issue, ...) - if (BusManager::getNumBusses() == 0) { - DEBUG_PRINTLN(F("No busses, init default")); - constexpr unsigned defDataTypes[] = {LED_TYPES}; - constexpr unsigned defDataPins[] = {DATA_PINS}; - constexpr unsigned defCounts[] = {PIXEL_COUNTS}; - constexpr unsigned defNumTypes = ((sizeof defDataTypes) / (sizeof defDataTypes[0])); - constexpr unsigned defNumPins = ((sizeof defDataPins) / (sizeof defDataPins[0])); - constexpr unsigned defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); - - static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins), - "The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES"); - - unsigned prevLen = 0; - unsigned pinsIndex = 0; - for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - uint8_t defPin[OUTPUT_MAX_PINS]; - // if we have less types than requested outputs and they do not align, use last known type to set current type - unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1]; - unsigned busPins = Bus::getNumberOfPins(dataType); - - // if we need more pins than available all outputs have been configured - if (pinsIndex + busPins > defNumPins) break; - - // Assign all pins first so we can check for conflicts on this bus - for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j]; - - for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) { - bool validPin = true; - // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware - // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. - // Pin should not be already allocated, read/only or defined for current bus - while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { - if (validPin) { - DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); - defPin[j] = 1; // start with GPIO1 and work upwards - validPin = false; - } else if (defPin[j] < WLED_NUM_PINS) { - defPin[j]++; - } else { - DEBUG_PRINTLN(F("No available pins left! Can't configure output.")); - return; - } - // is the newly assigned pin already defined or used previously? - // try next in line until there are no clashes or we run out of pins - bool clash; - do { - clash = false; - // check for conflicts on current bus - for (const auto &pin : defPin) { - if (&pin != &defPin[j] && pin == defPin[j]) { - clash = true; - break; - } - } - // We already have a clash on current bus, no point checking next buses - if (!clash) { - // check for conflicts in defined pins - for (const auto &pin : defDataPins) { - if (pin == defPin[j]) { - clash = true; - break; - } - } - } - if (clash) defPin[j]++; - if (defPin[j] >= WLED_NUM_PINS) break; - } while (clash); - } - } - pinsIndex += busPins; - - unsigned start = prevLen; - // if we have less counts than pins and they do not align, use last known count to set current count - unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; - // analog always has length 1 - if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; - prevLen += count; - BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer); - if (BusManager::add(defCfg) == -1) break; + // Determine parallel vs single I2S usage (used for memory calculation only) + bool useParallelI2S = false; + #if defined(CONFIG_IDF_TARGET_ESP32S3) + // ESP32-S3 always uses parallel LCD driver for I2S + if (i2sBusCount > 0) { + useParallelI2S = true; + } + #else + if (i2sBusCount > 1) { + useParallelI2S = true; + } + #endif + #endif + + DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize()); + // create buses/outputs + unsigned mem = 0; // memory estimation including DMA buffer for I2S and pixel buffers + unsigned I2SdmaMem = 0; + for (auto &bus : busConfigs) { + // assign bus types: call to getI() determines bus types/drivers, allocates and tracks polybus channels + // store the result in iType for later use during bus creation (getI() must only be called once per BusConfig) + // note: this needs to be determined for all buses prior to creating them as it also determines parallel I2S usage + bus.iType = BusManager::getI(bus.type, bus.pins, bus.driverType); + } + for (auto &bus : busConfigs) { + bool use_placeholder = false; + unsigned busMemUsage = bus.memUsage(); // does not include DMA/RMT buffer but includes pixel buffers (segment buffer + global buffer) + mem += busMemUsage; + // estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled) + #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) + bool usesI2S = (bus.iType & 0x01) == 0; // I2S bus types are even numbered, can't use bus.driverType == 1 as getI() may have defaulted to RMT + if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && usesI2S) { + #ifdef NPB_CONF_4STEP_CADENCE + constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit) + #else + constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit) + #endif + unsigned i2sCommonMem = (stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1)); + if (useParallelI2S) i2sCommonMem *= 8; // parallel I2S uses 8 channels, requiring 8x the DMA buffer size (common buffer shared between all parallel busses) + if (i2sCommonMem > I2SdmaMem) I2SdmaMem = i2sCommonMem; + } + #endif + if (mem + I2SdmaMem > MAX_LED_MEMORY + 1024) { // +1k to allow some margin to not drop buses that are allowed in UI (calculation here includes bus overhead) + DEBUG_PRINTF_P(PSTR("Bus %d with %d LEDS memory usage exceeds limit\n"), (int)bus.type, bus.count); + errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: not enough memory for bus + use_placeholder = true; + } + if (BusManager::add(bus, use_placeholder) != -1) { + mem += BusManager::busses.back()->getBusSize(); + if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count } } + DEBUG_PRINTF_P(PSTR("Estimated buses + pixel-buffers size: %uB\n"), mem + I2SdmaMem); + busConfigs.clear(); + busConfigs.shrink_to_fit(); _length = 0; - for (int i=0; igetStart() + bus->getLength() > MAX_LEDS) break; + if (!bus || !bus->isOk() || bus->getStart() + 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() && !bus->isPWM(); // use refresh bit for phase shift with analog unsigned busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; - // This must be done after all buses have been created, as some kinds (parallel I2S) interact bus->begin(); + bus->setBrightness(scaledBri(bri)); } + BusManager::initializeABL(); // init brightness limiter + DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); Segment::maxWidth = _length; Segment::maxHeight = 1; @@ -1299,21 +1255,33 @@ void WS2812FX::finalizeInit() { loadCustomPalettes(); // (re)load all custom palettes DEBUG_PRINTLN(F("Loading custom ledmaps")); deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) + + // allocate frame buffer after matrix has been set up (gaps!) + p_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it + // use PSRAM if available: there is no measurable perfomance impact between PSRAM and DRAM on S2/S3 with QSPI PSRAM for this buffer + _pixels = static_cast(allocate_buffer(getLengthTotal() * sizeof(uint32_t), BFRALLOC_ENFORCE_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR)); + DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), getLengthTotal() * sizeof(uint32_t)); + DEBUG_PRINTF_P(PSTR("Heap after strip init: %uB\n"), getFreeHeapSize()); } void WS2812FX::service() { unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days now = nowUp + timebase; - if (nowUp - _lastShow < MIN_SHOW_DELAY || _suspend) return; + unsigned long elapsed = nowUp - _lastServiceShow; + if (_suspend || elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited + if (!_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime + if (elapsed < _frametime) return; // too early for service + } + bool doShow = false; _isServicing = true; _segment_index = 0; - for (segment &seg : _segments) { - if (_suspend) return; // immediately stop processing segments if suspend requested during service() + for (Segment &seg : _segments) { + if (_suspend) break; // immediately stop processing segments if suspend requested during service() - // process transition (mode changes in the middle of transition) + // process transition (also pre-calculates progress value) seg.handleTransition(); // reset the segment runtime data if needed seg.resetIfRequired(); @@ -1321,93 +1289,373 @@ void WS2812FX::service() { if (!seg.isActive()) continue; // last condition ensures all solid segments are updated at the same time - if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) + if (nowUp > _lastServiceShow + _frametime || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { doShow = true; - unsigned frameDelay = FRAMETIME; if (!seg.freeze) { //only run effect function if not frozen - int oldCCT = BusManager::getSegmentCCT(); // store original CCT value (actually it is not Segment based) - _virtualSegmentLength = seg.virtualLength(); //SEGLEN - _colors_t[0] = gamma32(seg.currentColor(0)); - _colors_t[1] = gamma32(seg.currentColor(1)); - _colors_t[2] = gamma32(seg.currentColor(2)); - seg.setCurrentPalette(); // load actual palette - // when correctWB is true we need to correct/adjust RGB value according to desired CCT value, but it will also affect actual WW/CW ratio - // when cctFromRgb is true we implicitly calculate WW and CW from RGB values - if (cctFromRgb) BusManager::setSegmentCCT(-1); - else BusManager::setSegmentCCT(seg.currentBri(true), correctWB); // Effect blending - // When two effects are being blended, each may have different segment data, this - // data needs to be saved first and then restored before running previous mode. - // The blending will largely depend on the effect behaviour since actual output (LEDs) may be - // overwritten by later effect. To enable seamless blending for every effect, additional LED buffer - // would need to be allocated for each effect and then blended together for each pixel. - [[maybe_unused]] uint8_t tmpMode = seg.currentMode(); // this will return old mode while in transition - frameDelay = (*_mode[seg.mode])(); // run new/current mode -#ifndef WLED_DISABLE_MODE_BLEND - if (modeBlending && seg.mode != tmpMode) { - Segment::tmpsegd_t _tmpSegData; - Segment::modeBlend(true); // set semaphore - seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state) - _virtualSegmentLength = seg.virtualLength(); // update SEGLEN (mapping may have changed) - unsigned d2 = (*_mode[tmpMode])(); // run old mode - seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state) - frameDelay = min(frameDelay,d2); // use shortest delay - Segment::modeBlend(false); // unset semaphore - } -#endif + uint16_t prog = seg.progress(); + seg.beginDraw(prog); // set up parameters for get/setPixelColor() (will also blend colors and palette if blend style is FADE) + _currentSegment = &seg; // set current segment for effect functions (SEGMENT & SEGENV) + // workaround for on/off transition to respect blending style + _mode[seg.mode](); // run new/current mode (needed for bri workaround) seg.call++; - if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition - BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments + // if segment is in transition and no old segment exists we don't need to run the old mode + // (blendSegments() takes care of On/Off transitions and clipping) + Segment *segO = seg.getOldSegment(); + if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE || + (segO->name != seg.name && segO->name && seg.name && strncmp(segO->name, seg.name, WLED_MAX_SEGNAME_LEN) != 0))) { + Segment::modeBlend(true); // set semaphore for beginDraw() to blend colors and palette + segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress + _currentSegment = segO; // set current segment + // workaround for on/off transition to respect blending style + _mode[segO->mode](); // run old mode (needed for bri workaround; semaphore!!) + segO->call++; // increment old mode run counter + Segment::modeBlend(false); // unset semaphore + } } - - seg.next_time = nowUp + frameDelay; } _segment_index++; } - _virtualSegmentLength = 0; - _isServicing = false; - _triggered = false; #ifdef WLED_DEBUG - if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); + if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif - if (doShow) { + if (doShow && !_suspend) { yield(); Segment::handleRandomPalette(); // slowly transition random palette; move it into for loop when each segment has individual random palette + _lastServiceShow = nowUp; // update timestamp, for precise FPS control show(); } #ifdef WLED_DEBUG - if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); + if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif -} -void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) { - i = getMappedPixelIndex(i); - if (i >= _length) return; - BusManager::setPixelColor(i, col); + _triggered = false; + _isServicing = false; } -uint32_t IRAM_ATTR WS2812FX::getPixelColor(unsigned i) const { - i = getMappedPixelIndex(i); - if (i >= _length) return 0; - return BusManager::getPixelColor(i); +// https://en.wikipedia.org/wiki/Blend_modes but using a for top layer & b for bottom layer +static uint8_t _top (uint8_t a, uint8_t b) { return a; } +static uint8_t _bottom (uint8_t a, uint8_t b) { return b; } +static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; } +static uint8_t _subtract (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; } +static uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); } +static uint8_t _average (uint8_t a, uint8_t b) { return (a + b) >> 1; } +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) +static uint8_t _multiply (uint8_t a, uint8_t b) { return ((a * b) + 255) >> 8; } // faster than division on C3 but slightly less accurate +#else +static uint8_t _multiply (uint8_t a, uint8_t b) { return (a * b) / 255; } // origianl uses a & b in range [0,1] +#endif +static uint8_t _divide (uint8_t a, uint8_t b) { return a > b ? (b * 255) / a : 255; } +static uint8_t _lighten (uint8_t a, uint8_t b) { return a > b ? a : b; } +static uint8_t _darken (uint8_t a, uint8_t b) { return a < b ? a : b; } +static uint8_t _screen (uint8_t a, uint8_t b) { return 255 - _multiply(~a,~b); } // 255 - (255-a)*(255-b)/255 +static uint8_t _overlay (uint8_t a, uint8_t b) { return b < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } +static uint8_t _hardlight (uint8_t a, uint8_t b) { return a < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) +static uint8_t _softlight (uint8_t a, uint8_t b) { return (((b * b * (255 - 2 * a))) + ((2 * a * b + 256) << 8)) >> 16; } // Pegtop's formula (1 - 2a)b^2 +#else +static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a) + 255 * 2 * a * b) / (255 * 255); } // Pegtop's formula (1 - 2a)b^2 + 2ab +#endif +static uint8_t _dodge (uint8_t a, uint8_t b) { return _divide(~a,b); } +static uint8_t _burn (uint8_t a, uint8_t b) { return ~_divide(a,~b); } + +void WS2812FX::blendSegment(const Segment &topSegment) const { + + typedef uint8_t(*FuncType)(uint8_t, uint8_t); + FuncType funcs[] = { + _top, _bottom, + _add, _subtract, _difference, _average, + _multiply, _divide, _lighten, _darken, _screen, _overlay, + _hardlight, _softlight, _dodge, _burn + }; + + const size_t blendMode = topSegment.blendMode < (sizeof(funcs) / sizeof(FuncType)) ? topSegment.blendMode : 0; + const auto func = funcs[blendMode]; // blendMode % (sizeof(funcs) / sizeof(FuncType)) + const auto blend = [&](uint32_t top, uint32_t bottom){ return RGBW32(func(R(top),R(bottom)), func(G(top),G(bottom)), func(B(top),B(bottom)), func(W(top),W(bottom))); }; + + const int length = topSegment.length(); // physical segment length (counts all pixels in 2D segment) + const int width = topSegment.width(); + const int height = topSegment.height(); + const auto XY = [](int x, int y){ return x + y*Segment::maxWidth; }; + const size_t matrixSize = Segment::maxWidth * Segment::maxHeight; + const size_t startIndx = XY(topSegment.start, topSegment.startY); + const size_t stopIndx = startIndx + length; + const unsigned progress = topSegment.progress(); + const unsigned progInv = 0xFFFFU - progress; + uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE + uint8_t cct = topSegment.currentCCT(); + if (gammaCorrectCol) opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details) + + Segment::setClippingRect(0, 0); // disable clipping by default + + const unsigned dw = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1; + const unsigned dh = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1; + const unsigned orgBS = blendingStyle; + if (width*height == 1) blendingStyle = BLEND_STYLE_FADE; // disable style for single pixel segments (use fade instead) + switch (blendingStyle) { + case BLEND_STYLE_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped()) + case BLEND_STYLE_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped()) + case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped()) + Segment::setClippingRect(0, width, 0, height); + break; + case BLEND_STYLE_SWIPE_RIGHT: // left-to-right + case BLEND_STYLE_PUSH_RIGHT: // left-to-right + Segment::setClippingRect(0, dw, 0, height); + break; + case BLEND_STYLE_SWIPE_LEFT: // right-to-left + case BLEND_STYLE_PUSH_LEFT: // right-to-left + Segment::setClippingRect(width - dw, width, 0, height); + break; + case BLEND_STYLE_OUTSIDE_IN: // corners + Segment::setClippingRect((width + dw)/2, (width - dw)/2, (height + dh)/2, (height - dh)/2); // inverted!! + break; + case BLEND_STYLE_INSIDE_OUT: // outward + Segment::setClippingRect((width - dw)/2, (width + dw)/2, (height - dh)/2, (height + dh)/2); + break; + case BLEND_STYLE_SWIPE_DOWN: // top-to-bottom (2D) + case BLEND_STYLE_PUSH_DOWN: // top-to-bottom (2D) + Segment::setClippingRect(0, width, 0, dh); + break; + case BLEND_STYLE_SWIPE_UP: // bottom-to-top (2D) + case BLEND_STYLE_PUSH_UP: // bottom-to-top (2D) + Segment::setClippingRect(0, width, height - dh, height); + break; + case BLEND_STYLE_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D + Segment::setClippingRect((width - dw)/2, (width + dw)/2, 0, height); + break; + case BLEND_STYLE_OPEN_V: // vertical-outward (2D) + Segment::setClippingRect(0, width, (height - dh)/2, (height + dh)/2); + break; + case BLEND_STYLE_SWIPE_TL: // TL-to-BR (2D) + case BLEND_STYLE_PUSH_TL: // TL-to-BR (2D) + Segment::setClippingRect(0, dw, 0, dh); + break; + case BLEND_STYLE_SWIPE_TR: // TR-to-BL (2D) + case BLEND_STYLE_PUSH_TR: // TR-to-BL (2D) + Segment::setClippingRect(width - dw, width, 0, dh); + break; + case BLEND_STYLE_SWIPE_BR: // BR-to-TL (2D) + case BLEND_STYLE_PUSH_BR: // BR-to-TL (2D) + Segment::setClippingRect(width - dw, width, height - dh, height); + break; + case BLEND_STYLE_SWIPE_BL: // BL-to-TR (2D) + case BLEND_STYLE_PUSH_BL: // BL-to-TR (2D) + Segment::setClippingRect(0, dw, height - dh, height); + break; + } + + if (isMatrix && stopIndx <= matrixSize) { +#ifndef WLED_DISABLE_2D + const int nCols = topSegment.virtualWidth(); + const int nRows = topSegment.virtualHeight(); + const Segment *segO = topSegment.getOldSegment(); + const int oCols = segO ? segO->virtualWidth() : nCols; + const int oRows = segO ? segO->virtualHeight() : nRows; + + const auto setMirroredPixel = [&](int x, int y, uint32_t c, uint8_t o) { + const int baseX = topSegment.start + x; + const int baseY = topSegment.startY + y; + size_t indx = XY(baseX, baseY); // absolute address on strip + _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o); + if (_pixelCCT) _pixelCCT[indx] = cct; + // Apply mirroring + if (topSegment.mirror || topSegment.mirror_y) { + const int mirrorX = topSegment.start + width - x - 1; + const int mirrorY = topSegment.startY + height - y - 1; + const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY); + const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY); + const size_t idxMM = XY(mirrorX, mirrorY); + if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], blend(c, _pixels[idxMX]), o); + if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], blend(c, _pixels[idxMY]), o); + if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], blend(c, _pixels[idxMM]), o); + if (_pixelCCT) { + if (topSegment.mirror) _pixelCCT[idxMX] = cct; + if (topSegment.mirror_y) _pixelCCT[idxMY] = cct; + if (topSegment.mirror && topSegment.mirror_y) _pixelCCT[idxMM] = cct; + } + } + }; + + // if we blend using "push" style we need to "shift" canvas to left/right/up/down + unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU; + unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU; + + // we only traverse new segment, not old one + for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) { + const bool clipped = topSegment.isPixelXYClipped(c, r); + // if segment is in transition and pixel is clipped take old segment's pixel and opacity + const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE + int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions + int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions + int x = c; + int y = r; + // if we blend using "push" style we need to "shift" canvas to left/right/up/down + switch (blendingStyle) { + case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break; + case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break; + case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break; + case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break; + } + uint32_t c_a = BLACK; + if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment + if (segO && blendingStyle == BLEND_STYLE_FADE + && (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0)) + && x < oCols && y < oRows) { + // we need to blend old segment using fade as pixels are not clipped + c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); + } else if (blendingStyle != BLEND_STYLE_FADE) { + // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp) + // workaround for On/Off transition + // (bri != briT) && !bri => from On to Off + // (bri != briT) && bri => from Off to On + if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; + } + // map it into frame buffer + x = c; // restore coordiates if we were PUSHing + y = r; + if (topSegment.reverse ) x = nCols - x - 1; + if (topSegment.reverse_y) y = nRows - y - 1; + if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed + // expand pixel + const unsigned groupLen = topSegment.groupLength(); + if (groupLen == 1) { + setMirroredPixel(x, y, c_a, opacity); + } else { + // handle grouping and spacing + x *= groupLen; // expand to physical pixels + y *= groupLen; // expand to physical pixels + const int maxX = std::min(x + topSegment.grouping, width); + const int maxY = std::min(y + topSegment.grouping, height); + while (y < maxY) { + int _x = x; + while (_x < maxX) setMirroredPixel(_x++, y, c_a, opacity); + y++; + } + } + } +#endif + } else { + const int nLen = topSegment.virtualLength(); + const Segment *segO = topSegment.getOldSegment(); + const int oLen = segO ? segO->virtualLength() : nLen; + + const auto setMirroredPixel = [&](int i, uint32_t c, uint8_t o) { + int indx = topSegment.start + i; + // Apply mirroring + if (topSegment.mirror) { + unsigned indxM = topSegment.stop - i - 1; + indxM += topSegment.offset; // offset/phase + if (indxM >= topSegment.stop) indxM -= length; // wrap + _pixels[indxM] = color_blend(_pixels[indxM], blend(c, _pixels[indxM]), o); + if (_pixelCCT) _pixelCCT[indxM] = cct; + } + indx += topSegment.offset; // offset/phase + if (indx >= topSegment.stop) indx -= length; // wrap + _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o); + if (_pixelCCT) _pixelCCT[indx] = cct; + }; + + // if we blend using "push" style we need to "shift" canvas to left/right/ + unsigned offsetI = progInv * nLen / 0xFFFFU; + + for (int k = 0; k < nLen; k++) { + const bool clipped = topSegment.isPixelClipped(k); + // if segment is in transition and pixel is clipped take old segment's pixel and opacity + const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE + const int vLen = seg == segO ? oLen : nLen; + int i = k; + // if we blend using "push" style we need to "shift" canvas to left or right + switch (blendingStyle) { + case BLEND_STYLE_PUSH_RIGHT: i = (i + offsetI) % nLen; break; + case BLEND_STYLE_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break; + } + uint32_t c_a = BLACK; + if (i < vLen) c_a = seg->getPixelColorRaw(i); // will get clipped pixel from old segment or unclipped pixel from new segment + if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && i < oLen) { + // we need to blend old segment using fade as pixels are not clipped + c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv); + } else if (blendingStyle != BLEND_STYLE_FADE) { + // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp) + // workaround for On/Off transition + // (bri != briT) && !bri => from On to Off + // (bri != briT) && bri => from Off to On + if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; + } + // map into frame buffer + i = k; // restore index if we were PUSHing + if (topSegment.reverse) i = nLen - i - 1; // is segment reversed? + // expand pixel + i *= topSegment.groupLength(); + // set all the pixels in the group + const int maxI = std::min(i + topSegment.grouping, length); // make sure to not go beyond physical length + while (i < maxI) setMirroredPixel(i++, c_a, opacity); + } + } + + blendingStyle = orgBS; + Segment::setClippingRect(0, 0); // disable clipping for overlays } void WS2812FX::show() { + if (!_pixels) { + DEBUGFX_PRINTLN(F("Error: no _pixels!")); + errorFlag = ERR_NORAM; + return; // no pixels allocated, nothing to show + } + + unsigned long showNow = millis(); + size_t diff = showNow - _lastShow; + + size_t totalLen = getLengthTotal(); + // WARNING: as WLED doesn't handle CCT on pixel level but on Segment level instead + // we need to keep track of each pixel's CCT when blending segments (if CCT is present) + // and then set appropriate CCT from that pixel during paint (see below). + if ((hasCCTBus() || correctWB) && !cctFromRgb) + _pixelCCT = static_cast(allocate_buffer(totalLen * sizeof(uint8_t), BFRALLOC_PREFER_PSRAM)); // allocate CCT buffer if necessary, prefer PSRAM + if (_pixelCCT) memset(_pixelCCT, 127, totalLen); // set neutral (50:50) CCT + + if (realtimeMode == REALTIME_MODE_INACTIVE || useMainSegmentOnly || realtimeOverride > REALTIME_OVERRIDE_NONE) { + // clear frame buffer + for (size_t i = 0; i < totalLen; i++) _pixels[i] = BLACK; // memset(_pixels, 0, sizeof(uint32_t) * getLengthTotal()); + // blend all segments into (cleared) buffer + for (Segment &seg : _segments) if (seg.isActive() && (seg.on || seg.isInTransition())) { + blendSegment(seg); // blend segment's buffer into frame buffer + } + } + // avoid race condition, capture _callback value show_callback callback = _callback; - if (callback) callback(); + if (callback) callback(); // will call setPixelColor or setRealtimePixelColor + + // paint actual pixels + int oldCCT = Bus::getCCT(); // store original CCT value (since it is global) + // when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1) + if (cctFromRgb) BusManager::setSegmentCCT(-1); + for (size_t i = 0; i < totalLen; i++) { + // when correctWB is true setSegmentCCT() will convert CCT into K with which we can then + // correct/adjust RGB value according to desired CCT value, it will still affect actual WW/CW ratio + if (_pixelCCT) { // cctFromRgb already exluded at allocation + if (i == 0 || _pixelCCT[i-1] != _pixelCCT[i]) BusManager::setSegmentCCT(_pixelCCT[i], correctWB); + } + + uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32) + if(c > 0 && !(realtimeMode && arlsDisableGammaCorrection)) + c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss + BusManager::setPixelColor(getMappedPixelIndex(i), c); + } + Bus::setCCT(oldCCT); // restore old CCT for ABL adjustments + + p_free(_pixelCCT); + _pixelCCT = nullptr; // 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 BusManager::show(); - unsigned long showNow = millis(); - size_t diff = showNow - _lastShow; - if (diff > 0) { // skip calculation if no time has passed size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math _cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1); // "+FPS_CALC_AVG/2" for proper rounding @@ -1415,51 +1663,53 @@ void WS2812FX::show() { } } -/** - * 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() const { - return !BusManager::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 accuracy varies - */ -uint16_t WS2812FX::getFps() const { - if (millis() - _lastShow > 2000) return 0; - return (FPS_MULTIPLIER * _cumulativeFps) >> FPS_CALC_SHIFT; // _cumulativeFps is stored in fixed point +void WS2812FX::setRealtimePixelColor(unsigned i, uint32_t c) { + if (useMainSegmentOnly) { + const Segment &seg = getMainSegment(); + if (seg.isActive() && i < seg.length()) seg.setPixelColorRaw(i, c); + } else { + setPixelColor(i, c); + } } -void WS2812FX::setTargetFps(uint8_t fps) { - if (fps > 0 && fps <= 120) _targetFps = fps; - _frametime = 1000 / _targetFps; +// reset all segments +void WS2812FX::restartRuntime() { + suspend(); + waitForIt(); + for (Segment &seg : _segments) seg.markForReset().resetIfRequired(); + resume(); } -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].setMode(m); // do not load defaults - } +// start or stop transition for all segments +void WS2812FX::setTransitionMode(bool t) { + suspend(); + waitForIt(); + for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); + resume(); } -//applies to all active and selected segments -void WS2812FX::setColor(uint8_t slot, uint32_t c) { - if (slot >= NUM_COLORS) return; +// wait until frame is over (service() has finished or time for 2 frames have passed; yield() crashes on 8266) +// the latter may, in rare circumstances, lead to incorrectly assuming strip is done servicing but will not block +// other processing "indefinitely" +// rare circumstances are: setting FPS to high number (i.e. 120) and have very slow effect that will need more +// time than 2 * _frametime (1000/FPS) to draw content +void WS2812FX::waitForIt() { + unsigned long waitStart = millis(); + unsigned long maxWait = 2*getFrameTime() + 100; // TODO: this needs a proper fix for timeout! see #4779 + while (isServicing() && (millis() - waitStart < maxWait)) delay(1); // safe even when millis() rolls over + #ifdef WLED_DEBUG + if (millis()-waitStart >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing.")); + #endif +}; - for (segment &seg : _segments) { - if (seg.isActive() && seg.isSelected()) { - seg.setColor(slot, c); - } - } +void WS2812FX::setTargetFps(unsigned fps) { + if (fps <= 250) _targetFps = fps; + if (_targetFps > 0) _frametime = 1000 / _targetFps; + else _frametime = MIN_FRAME_DELAY; // unlimited mode } void WS2812FX::setCCT(uint16_t k) { - for (segment &seg : _segments) { + for (Segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) { seg.setCCT(k); } @@ -1473,22 +1723,18 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { if (_brightness == b) return; _brightness = b; if (_brightness == 0) { //unfreeze all segments on power off - for (segment &seg : _segments) { - seg.freeze = false; - } + for (const Segment &seg : _segments) seg.freeze = false; // freeze is mutable } - // setting brightness with NeoPixelBusLg has no effect on already painted pixels, - // so we need to force an update to existing buffer - BusManager::setBrightness(b); + BusManager::setBrightness(scaledBri(b)); if (!direct) { unsigned long t = millis(); - if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon + if (t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon } } uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const { uint8_t totalLC = 0; - for (const segment &seg : _segments) { + for (const Segment &seg : _segments) { if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities(); } return totalLC; @@ -1496,7 +1742,7 @@ uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const { uint8_t WS2812FX::getFirstSelectedSegId() const { size_t i = 0; - for (const segment &seg : _segments) { + for (const Segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) return i; i++; } @@ -1504,9 +1750,9 @@ uint8_t WS2812FX::getFirstSelectedSegId() const { return getMainSegmentId(); } -void WS2812FX::setMainSegmentId(uint8_t n) { - _mainSegment = 0; - if (n < _segments.size()) { +void WS2812FX::setMainSegmentId(unsigned n) { + _mainSegment = getLastActiveSegmentId(); + if (n < _segments.size() && _segments[n].isActive()) { // only set if segment is active _mainSegment = n; } return; @@ -1520,10 +1766,8 @@ uint8_t WS2812FX::getLastActiveSegmentId() const { } uint8_t WS2812FX::getActiveSegmentsNum() const { - uint8_t c = 0; - for (size_t i = 0; i < _segments.size(); i++) { - if (_segments[i].isActive()) c++; - } + unsigned c = 0; + for (const Segment &seg : _segments) if (seg.isActive()) c++; return c; } @@ -1534,13 +1778,7 @@ uint16_t WS2812FX::getLengthTotal() const { } uint16_t WS2812FX::getLengthPhysical() const { - unsigned len = 0; - for (size_t b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus->isVirtual()) continue; //exclude non-physical network busses - len += bus->getLength(); - } - return len; + return BusManager::getTotalLength(true); } //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. @@ -1548,8 +1786,8 @@ uint16_t WS2812FX::getLengthPhysical() const { //not influenced by auto-white mode, also true if white slider does not affect output white channel bool WS2812FX::hasRGBWBus() const { for (size_t b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus == nullptr || bus->getLength()==0) break; + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; if (bus->hasRGB() && bus->hasWhite()) return true; } return false; @@ -1558,8 +1796,8 @@ bool WS2812FX::hasRGBWBus() const { bool WS2812FX::hasCCTBus() const { if (cctFromRgb && !correctWB) return false; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus == nullptr || bus->getLength()==0) break; + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; if (bus->hasCCT()) return true; } return false; @@ -1580,36 +1818,27 @@ void WS2812FX::purgeSegments() { } } -Segment& WS2812FX::getSegment(uint8_t id) { +Segment& WS2812FX::getSegment(unsigned 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 - } - suspend(); - _segments[segId].setUp(i1, i2, grouping, spacing, offset, startY, stopY); - resume(); - if (segId > 0 && segId == getSegmentsNum()-1 && i2 <= i1) _segments.pop_back(); // if last segment was deleted remove it from vector -} - +// WARNING: resetSegments(), makeAutoSegments() and fixInvalidSegments() must not be called while +// strip is being serviced (strip.service()), you must call suspend prior if changing segments outside +// loop() context void WS2812FX::resetSegments() { - _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); - _segments.shrink_to_fit(); // just in case ... + if (isServicing()) return; + _segments.clear(); // destructs all Segment as part of clearing + _segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1); + if(_segments.size() == 0) { + _segments.emplace_back(); // if out of heap, create a default segment + errorFlag = ERR_NORAM_PX; + } + _segments.shrink_to_fit(); // just in case ... _mainSegment = 0; } void WS2812FX::makeAutoSegments(bool forceReset) { + if (isServicing()) return; if (autoSegments) { //make one segment per bus unsigned segStarts[MAX_NUM_SEGMENTS] = {0}; unsigned segStops [MAX_NUM_SEGMENTS] = {0}; @@ -1625,10 +1854,11 @@ void WS2812FX::makeAutoSegments(bool forceReset) { #endif for (size_t i = s; i < BusManager::getNumBusses(); i++) { - Bus* b = BusManager::getBus(i); + const Bus *bus = BusManager::getBus(i); + if (!bus) break; - segStarts[s] = b->getStart(); - segStops[s] = segStarts[s] + b->getLength(); + segStarts[s] = bus->getStart(); + segStops[s] = segStarts[s] + bus->getLength(); #ifndef WLED_DISABLE_2D if (isMatrix && segStops[s] <= Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix @@ -1652,14 +1882,14 @@ void WS2812FX::makeAutoSegments(bool forceReset) { // 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)); + _segments.emplace_back(0, Segment::maxWidth, 0, Segment::maxHeight); else #endif - _segments.push_back(Segment(segStarts[0], segStops[0])); + _segments.emplace_back(segStarts[0], segStops[0]); for (size_t i = 1; i < s; i++) { - _segments.push_back(Segment(segStarts[i], segStops[i])); + _segments.emplace_back(segStarts[i], segStops[i]); } - DEBUG_PRINTF_P(PSTR("%d auto segments created.\n"), _segments.size()); + DEBUGFX_PRINTF_P(PSTR("%d auto segments created.\n"), _segments.size()); } else { @@ -1668,15 +1898,9 @@ void WS2812FX::makeAutoSegments(bool forceReset) { 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; + _segments[i].setGeometry(0, Segment::maxWidth, 1, 0, 0xFFFF, 0, Segment::maxHeight); #else - _segments[i].start = 0; - _segments[i].stop = _length; + _segments[i].setGeometry(0, _length); #endif } } @@ -1686,6 +1910,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) { } void WS2812FX::fixInvalidSegments() { + if (isServicing()) return; //make sure no segment is longer than total (sanity check) for (size_t i = getSegmentsNum()-1; i > 0; i--) { if (isMatrix) { @@ -1708,17 +1933,18 @@ void WS2812FX::fixInvalidSegments() { // if any segments were deleted free memory purgeSegments(); // this is always called as the last step after finalizeInit(), update covered bus types - for (segment &seg : _segments) + for (const 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 WS2812FX::checkSegmentAlignment() const { bool aligned = false; - for (segment &seg : _segments) { + for (const Segment &seg : _segments) { for (unsigned b = 0; bisOk()) break; if (seg.start == bus->getStart() && seg.stop == bus->getStart() + bus->getLength()) aligned = true; } if (seg.start == 0 && seg.stop == _length) aligned = true; @@ -1745,59 +1971,10 @@ void WS2812FX::printSize() { } #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()>3) { // not an empty palette (at least 2 entries) - if (pal[0].is() && pal[1].is()) { - // we have an array of index & hex strings - size_t palSize = MIN(pal.size(), 36); - palSize -= palSize % 2; // make sure size is multiple of 2 - for (size_t i=0, j=0; i()<256; i+=2, j+=4) { - uint8_t rgbw[] = {0,0,0,0}; - tcp[ j ] = (uint8_t) pal[ i ].as(); // index - colorFromHexString(rgbw, pal[i+1].as()); // will catch non-string entires - for (size_t c=0; c<3; c++) tcp[j+1+c] = gamma8(rgbw[c]); // only use RGB component - DEBUG_PRINTF_P(PSTR("%d(%d) : %d %d %d\n"), i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); - } - } else { - size_t palSize = MIN(pal.size(), 72); - palSize -= palSize % 4; // make sure size is multiple of 4 - for (size_t i=0; i()<256; i+=4) { - tcp[ i ] = (uint8_t) pal[ i ].as(); // index - tcp[i+1] = gamma8((uint8_t) pal[i+1].as()); // R - tcp[i+2] = gamma8((uint8_t) pal[i+2].as()); // G - tcp[i+3] = gamma8((uint8_t) pal[i+3].as()); // B - DEBUG_PRINTF_P(PSTR("%d(%d) : %d %d %d\n"), i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); - } - } - customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); - } else { - DEBUG_PRINTLN(F("Wrong palette format.")); - } - } - } 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. - +// load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) +// if this is a matrix set-up and default ledmap.json file does not exist, create mapping table using setUpMatrix() from panel information +// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context +bool WS2812FX::deserializeMap(unsigned n) { char fileName[32]; strcpy_P(fileName, PSTR("/ledmap")); if (n) sprintf(fileName +7, "%d", n); @@ -1809,36 +1986,77 @@ bool WS2812FX::deserializeMap(uint8_t n) { if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI) if (!isFile && n==0 && isMatrix) { + // 2D panel support creates its own ledmap (on the fly) if a ledmap.json does not exist setUpMatrix(); return false; } - if (!isFile || !requestJSONBufferLock(7)) return false; + if (!isFile || !requestJSONBufferLock(JSON_LOCK_LEDMAP)) return false; - if (!readObjectFromFile(fileName, nullptr, pDoc)) { - DEBUG_PRINT(F("ERROR Invalid ledmap in ")); DEBUG_PRINTLN(fileName); + StaticJsonDocument<64> filter; + filter[F("width")] = true; + filter[F("height")] = true; + if (!readObjectFromFile(fileName, nullptr, pDoc, &filter)) { + DEBUG_PRINTF_P(PSTR("ERROR Invalid ledmap in %s\n"), fileName); releaseJSONBufferLock(); return false; // if file does not load properly then exit - } + } else + DEBUG_PRINTF_P(PSTR("Reading LED map from %s\n"), fileName); JsonObject root = pDoc->as(); // if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps) - if (isMatrix && n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { - Segment::maxWidth = min(max(root[F("width")].as(), 1), 128); - Segment::maxHeight = min(max(root[F("height")].as(), 1), 128); + if (n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { + Segment::maxWidth = min(max(root[F("width")].as(), 1), 255); + Segment::maxHeight = min(max(root[F("height")].as(), 1), 255); + isMatrix = true; + DEBUG_PRINTF_P(PSTR("LED map width=%d, height=%d\n"), Segment::maxWidth, Segment::maxHeight); } - if (customMappingTable) delete[] customMappingTable; - customMappingTable = new uint16_t[getLengthTotal()]; + d_free(customMappingTable); + customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer DRAM for speed if (customMappingTable) { - DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); + DEBUG_PRINTF_P(PSTR("ledmap allocated: %uB\n"), sizeof(uint16_t)*getLengthTotal()); + File f = WLED_FS.open(fileName, "r"); + f.find("\"map\":["); + while (f.available()) { // f.position() < f.size() - 1 + char number[32]; + size_t numRead = f.readBytesUntil(',', number, sizeof(number)-1); // read a single number (may include array terminating "]" but not number separator ',') + number[numRead] = 0; + if (numRead > 0) { + char *end = strchr(number,']'); // we encountered end of array so stop processing if no digit found + bool foundDigit = (end == nullptr); + int i = 0; + if (end != nullptr) do { + if (number[i] >= '0' && number[i] <= '9') foundDigit = true; + if (foundDigit || &number[i++] == end) break; + } while (i < 32); + if (!foundDigit) break; + int index = atoi(number); + if (index < 0 || index > 65535) index = 0xFFFF; // prevent integer wrap around + customMappingTable[customMappingSize++] = index; + if (customMappingSize >= getLengthTotal()) break; + } else break; // there was nothing to read, stop + } + currentLedmap = n; + f.close(); + + #ifdef WLED_DEBUG + DEBUG_PRINT(F("Loaded ledmap:")); + for (unsigned i=0; i 0); } -uint16_t IRAM_ATTR WS2812FX::getMappedPixelIndex(uint16_t index) const { - // convert logical address to physical - if (index < customMappingSize - && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; - - return index; -} - - -WS2812FX* WS2812FX::instance = nullptr; const char JSON_mode_names[] PROGMEM = R"=====(["FX names moved"])====="; const char JSON_palette_names[] PROGMEM = R"=====([ @@ -1867,5 +2075,5 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Magenta","Magred","Yelmag","Yelblu","Orange & Teal","Tiamat","April Night","Orangery","C9","Sakura", "Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf", "Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", -"Candy2" +"Candy2","Traffic Light" ])====="; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp new file mode 100644 index 0000000000..fd6ce4c4ed --- /dev/null +++ b/wled00/FXparticleSystem.cpp @@ -0,0 +1,1948 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later +*/ + +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled +#include "FXparticleSystem.h" +// local shared functions (used both in 1D and 2D system) +static int32_t calcForce_dv(const int8_t force, uint8_t &counter); +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius +static uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, uint8_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D +ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { + PSPRINTLN("\n ParticleSystem2D constructor"); + numSources = numberofsources; // number of sources allocated in init + numParticles = numberofparticles; // number of particles allocated in init + usedParticles = numParticles; // use all particles by default + advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = nullptr; + setMatrixSize(width, height); + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) + setWallHardness(255); // set default wall hardness to max + setWallRoughness(0); // smooth walls by default + setGravity(0); //gravity disabled by default + setParticleSize(1); // 2x2 rendering size by default (disables per particle size control by default) + motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default + emitIndex = 0; + collisionStartIdx = 0; + + //initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numParticles; i++) { + particles[i].sat = 255; // full saturation + } + for (uint32_t i = 0; i < numSources; i++) { + sources[i].source.sat = 255; //set saturation to max by default + sources[i].source.ttl = 1; //set source alive + sources[i].sourceFlags.asByte = 0; // all flags disabled + } + +} + +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem2D::update(void) { + //apply gravity globally if enabled + if (particlesettings.useGravity) + applyGravity(); + + //update size settings before handling collisions + if (advPartSize != nullptr) { + for (uint32_t i = 0; i < usedParticles; i++) { + if (updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size + particles[i].ttl = 0; // kill particle + } + } + } + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (uint32_t i = 0; i < usedParticles; i++) { + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash + } + + render(); +} + +// update function for fire animation +void ParticleSystem2D::updateFire(const uint8_t intensity) { + fireParticleupdate(); + fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function + render(); +} + +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem2D::setUsedParticles(uint8_t percentage) { + usedParticles = max((uint32_t)1, (numParticles * ((int)percentage+1)) >> 8); // number of particles to use (percentage is 0-255, 255 = 100%) + PSPRINT(" SetUsedpaticles: allocated particles: "); + PSPRINT(numParticles); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); +} + +void ParticleSystem2D::setWallHardness(uint8_t hardness) { + wallHardness = hardness; +} + +void ParticleSystem2D::setWallRoughness(uint8_t roughness) { + wallRoughness = roughness; +} + +void ParticleSystem2D::setCollisionHardness(uint8_t hardness) { + collisionHardness = (int)hardness + 1; +} + +void ParticleSystem2D::setMatrixSize(uint32_t x, uint32_t y) { + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxYpixel = y - 1; + maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements + maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions +} + +void ParticleSystem2D::setWrapX(bool enable) { + particlesettings.wrapX = enable; +} + +void ParticleSystem2D::setWrapY(bool enable) { + particlesettings.wrapY = enable; +} + +void ParticleSystem2D::setBounceX(bool enable) { + particlesettings.bounceX = enable; +} + +void ParticleSystem2D::setBounceY(bool enable) { + particlesettings.bounceY = enable; +} + +void ParticleSystem2D::setKillOutOfBounds(bool enable) { + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem2D::setColorByAge(bool enable) { + particlesettings.colorByAge = enable; +} + +void ParticleSystem2D::setMotionBlur(uint8_t bluramount) { + motionBlur = bluramount; +} + +void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { + smearBlur = bluramount; +} + + +// set global particle size +void ParticleSystem2D::setParticleSize(uint8_t size) { + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel + perParticleSize = false; // disable per particle size control if global size is set + if (particlesize > 1) { + particleHardRadius = PS_P_MINHARDRADIUS + ((particlesize * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking) + } + else if (particlesize == 0) + particleHardRadius = PS_P_MINHARDRADIUS >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel) +} + +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem2D::setGravity(int8_t force) { + if (force) { + gforce = force; + particlesettings.useGravity = true; + } else { + particlesettings.useGravity = false; + } +} + +void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) { // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable + particlesettings.useCollisions = enable; + collisionHardness = (int)hardness + 1; +} + +// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) { + bool success = false; + for (uint32_t i = 0; i < usedParticles; i++) { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) { // find a dead particle + success = true; + int32_t dx = hw_random16(emitter.var << 1) - emitter.var; + int32_t dy = hw_random16(emitter.var << 1) - emitter.var; + if (emitter.var > 5) { // use circular random distribution for large variance to generate nicer "explosions" + while (dx*dx + dy*dy > emitter.var*emitter.var) { // reject points outside circle + dx = hw_random16(emitter.var << 1) - emitter.var; + dy = hw_random16(emitter.var << 1) - emitter.var; + } + } + particles[emitIndex].vx = emitter.vx + dx; + particles[emitIndex].vy = emitter.vy + dy; + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + if (advPartProps != nullptr) + advPartProps[emitIndex].size = emitter.size; + break; + } + } + if (success) + return emitIndex; + else + return -1; +} + +// Spray emitter for particles used for flames (particle TTL depends on source TTL) +void ParticleSystem2D::flameEmit(const PSsource &emitter) { + int emitIndex = sprayEmit(emitter); + if (emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; +} + +// Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var +// angle = 0 means in positive x-direction (i.e. to the right) +int32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, const int32_t speed) { + emitter.vx = ((int32_t)cos16_t(angle) * speed) / (int32_t)32600; // cos16_t() and sin16_t() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16_t(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! + return sprayEmit(emitter); +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is enabled, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) { + if (options == nullptr) + options = &particlesettings; //use PS system settings by default + + if (part.ttl > 0) { + if (!partFlags.perpetual) + part.ttl--; // age + if (options->colorByAge) + part.hue = min(part.ttl, (uint16_t)255); //set color to ttl + + int32_t renderradius = PS_P_HALFRADIUS - 1 + particlesize; // used to check out of bounds, if its more than half a radius out of bounds, it will render to x = -2/-1 or x=max/max+1 in standard 2x2 rendering + int32_t newX = part.x + (int32_t)part.vx; + int32_t newY = part.y + (int32_t)part.vy; + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) note: moving this to checks below adds code and is not faster + + if (perParticleSize && advancedproperties != nullptr) { // using individual particle size + renderradius = PS_P_HALFRADIUS - 1 + advancedproperties->size; // note: single pixel particles should be zero but OOB checks in rendering function handle this + if (advancedproperties->size > 0) + particleHardRadius = PS_P_MINHARDRADIUS + ((advancedproperties->size * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float") + else // single pixel particles use half the collision distance for walls + particleHardRadius = PS_P_MINHARDRADIUS >> 1; + } + // note: if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle does not go half out of view + if (options->bounceY) { + if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling + bounce(part.vy, part.vx, newY, maxY); + } + } + + if (!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped. if gravity is enabled, particles will never bounce at the top + partFlags.outofbounds = true; + if (options->killoutofbounds) { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options->useGravity) + part.ttl = 0; + } + } + + if (part.ttl) { //check x direction only if still alive + if (options->bounceX) { + if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall + bounce(part.vx, part.vy, newX, maxX); + } + else if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds + partFlags.outofbounds = true; + if (options->killoutofbounds) + part.ttl = 0; + } + } + + part.x = (int16_t)newX; // set new position + part.y = (int16_t)newY; // set new position + } +} + +// move function for fire particles +void ParticleSystem2D::fireParticleupdate() { + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl > 0) + { + particles[i].ttl--; // age + int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + int32_t newX = particles[i].x + (int32_t)particles[i].vx; + particleFlags[i].outofbounds = 0; // reset out of bounds flag note: moving this to checks below is not faster but adds code + // check if particle is out of bounds, wrap x around to other side if wrapping is enabled + // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds + if (newY < -PS_P_HALFRADIUS) + particleFlags[i].outofbounds = 1; + else if (newY > int32_t(maxY + PS_P_HALFRADIUS)) // particle moved out at the top + particles[i].ttl = 0; + else // particle is in frame in y direction, also check x direction now Note: using checkBoundsAndWrap() is slower, only saves a few bytes + { + if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap + if (particlesettings.wrapX) { + newX = newX % (maxX + 1); + if (newX < 0) // handle negative modulo + newX += maxX + 1; + } + else if ((newX < -PS_P_HALFRADIUS) || (newX > int32_t(maxX + PS_P_HALFRADIUS))) { //if fully out of view + particles[i].ttl = 0; + } + } + particles[i].x = newX; + } + particles[i].y = newY; + } + } +} + +// update advanced particle size control, returns false if particle shrinks to 0 size +bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { + if (advsize == nullptr) // safety check + return false; + // grow/shrink particle + int32_t newsize = advprops->size; + uint32_t counter = advsize->sizecounter; + uint32_t increment = 0; + // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + if (advsize->grow) increment = advsize->growspeed; + else if (advsize->shrink) increment = advsize->shrinkspeed; + if (increment < 9) { // 8 means +1 every frame + counter += increment; + if (counter > 7) { + counter -= 8; + increment = 1; + } else + increment = 0; + advsize->sizecounter = counter; + } else { + increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 + } + + if (advsize->grow) { + if (newsize < advsize->maxsize) { + newsize += increment; + if (newsize >= advsize->maxsize) { + advsize->grow = false; // stop growing, shrink from now on if enabled + newsize = advsize->maxsize; // limit + if (advsize->pulsate) advsize->shrink = true; + } + } + } else if (advsize->shrink) { + if (newsize > advsize->minsize) { + newsize -= increment; + if (newsize <= advsize->minsize) { + if (advsize->minsize == 0) + return false; // particle shrunk to zero + advsize->shrink = false; // disable shrinking + newsize = advsize->minsize; // limit + if (advsize->pulsate) advsize->grow = true; + } + } + } + advprops->size = newsize; + // handle wobbling + if (advsize->wobble) { + advsize->asymdir += advsize->wobblespeed; // note: if need better wobblespeed control a counter is already in the struct + } + return true; +} + +// calculate x and y size for asymmetrical particles (advanced size control) +void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { + if (advsize == nullptr) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers()) + return; + int32_t size = advprops->size; + int32_t asymdir = advsize->asymdir; + int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry + 255) >> 8; // deviation from symmetrical size + // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) + if (asymdir < 64) { + deviation = (asymdir * deviation) >> 6; + } else if (asymdir < 192) { + deviation = ((128 - asymdir) * deviation) >> 6; + } else { + deviation = ((asymdir - 255) * deviation) >> 6; + } + // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes) + xsize = min((size - deviation), (int32_t)255); + ysize = min((size + deviation), (int32_t)255);; +} + +// function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) +void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition) { + incomingspeed = -incomingspeed; + incomingspeed = (incomingspeed * wallHardness + 128) >> 8; // reduce speed as energy is lost on non-hard surface + if (position < (int32_t)particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + position = maxposition - particleHardRadius; + if (wallRoughness) { + int32_t incomingspeed_abs = abs((int32_t)incomingspeed); + int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); + // transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = ((hw_random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness + parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); + // give the remainder of the speed to perpendicular speed + donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same + incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; + } +} + +// apply a force in x,y direction to individual particle +// caller needs to provide a 8bit counter (for each particle) that holds its value between calls +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter) { + // for small forces, need to use a delay counter + uint8_t xcounter = counter & 0x0F; // lower four bits + uint8_t ycounter = counter >> 4; // upper four bits + + // velocity increase + int32_t dvx = calcForce_dv(xforce, xcounter); + int32_t dvy = calcForce_dv(yforce, ycounter); + + // save counter values back + counter = xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + counter |= (ycounter << 4) & 0xF0; // write upper four bits + + // apply the force to particle + part.vx = limitSpeed((int32_t)part.vx + dvx); + part.vy = limitSpeed((int32_t)part.vy + dvy); +} + +// apply a force in x,y direction to individual particle using advanced particle properties +void ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) { + if (advPartProps == nullptr) + return; // no advanced properties available + applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter); +} + +// apply a force in x,y direction to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem2D::applyForce(const int8_t xforce, const int8_t yforce) { + // for small forces, need to use a delay counter + uint8_t tempcounter; + // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough + for (uint32_t i = 0; i < usedParticles; i++) { + tempcounter = forcecounter; + applyForce(particles[i], xforce, yforce, tempcounter); + } + forcecounter = tempcounter; // save value back +} + +// apply a force in angular direction to single particle +// caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) +void ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter) { + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! + applyForce(part, xforce, yforce, counter); +} + +void ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) { + if (advPartProps == nullptr) + return; // no advanced properties available + applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter); +} + +// apply a force in angular direction to all particles +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) { + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! + applyForce(xforce, yforce); +} + +// apply gravity to all particles using PS global gforce setting +// force is in 3.4 fixed point notation, see note above +// note: faster than apply force since direction is always down and counter is fixed for all particles +void ParticleSystem2D::applyGravity() { + int32_t dv = calcForce_dv(gforce, gforcecounter); + if (dv == 0) return; + for (uint32_t i = 0; i < usedParticles; i++) { + // Note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv); + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem2D::applyGravity(PSparticle &part) { + uint32_t counterbkp = gforcecounter; // backup PS gravity counter + int32_t dv = calcForce_dv(gforce, gforcecounter); + gforcecounter = counterbkp; //save it back + part.vy = limitSpeed((int32_t)part.vy - dv); +} + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) { + // note: not checking if particle is dead can be done by caller (or can be omitted) + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t friction = 256 - coefficient; + part.vx = ((int32_t)part.vx * friction + (((int32_t)part.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + part.vy = ((int32_t)part.vy * friction + (((int32_t)part.vy >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 + int32_t friction = 255 - coefficient; + part.vx = ((int32_t)part.vx * friction) / 255; + part.vy = ((int32_t)part.vy * friction) / 255; + #endif +} + +// apply friction to all particles +// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways +void ParticleSystem2D::applyFriction(const int32_t coefficient) { + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t friction = 256 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + particles[i].vy = ((int32_t)particles[i].vy * friction + (((int32_t)particles[i].vy >> 31) & 0xFF)) >> 8; + } + #else // division is faster on ESP32, S2 and S3 + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; + particles[i].vy = ((int32_t)particles[i].vy * friction) / 255; + } + #endif +} + +// attracts a particle to an attractor particle using the inverse square-law +void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) { + if (advPartProps == nullptr) + return; // no advanced properties available + + // Calculate the distance between the particle and the attractor + int32_t dx = attractor.x - particles[particleindex].x; + int32_t dy = attractor.y - particles[particleindex].y; + + // Calculate the force based on inverse square law + int32_t distanceSquared = dx * dx + dy * dy; + if (distanceSquared < 8192) { + if (swallow) { // particle is close, age it fast so it fades out, do not attract further + if (particles[particleindex].ttl > 7) + particles[particleindex].ttl -= 8; + else { + particles[particleindex].ttl = 0; + return; + } + } + distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = ((int32_t)strength << 16) / distanceSquared; + int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting + int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! + applyForce(particleindex, xforce, yforce); +} + +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +// firemode is only used for PS Fire FX +void ParticleSystem2D::render() { + if (framebuffer == nullptr) { + PSPRINTLN(F("PS render: no framebuffer!")); + return; + } + CRGBW baseRGB; + uint32_t brightness; // particle brightness, fades if dying + TBlendType blend = LINEARBLEND; // default color rendering: wrap palette + if (particlesettings.colorByAge) { + blend = LINEARBLEND_NOWRAP; + } + + if (motionBlur) { // motion-blurring active + for (int32_t y = 0; y <= maxYpixel; y++) { + int index = y * (maxXpixel + 1); + for (int32_t x = 0; x <= maxXpixel; x++) { + framebuffer[index] = fast_color_scale(framebuffer[index], motionBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough + index++; + } + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, (maxXpixel+1) * (maxYpixel+1) * sizeof(CRGBW)); + } + + // go over particles and render them to the buffer + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl == 0 || particleFlags[i].outofbounds) + continue; + // generate RGB values for particle + if (fireIntesity) { // fire mode + brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 5; + brightness = min(brightness, (uint32_t)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP); // map hue to brightness for fire effect + } + else { + brightness = min((particles[i].ttl << 1), (int)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend); + if (particles[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv(baseRGB.color32, baseHSV); // convert to HSV + baseHSV.s = min(baseHSV.s, particles[i].sat); // set the saturation but don't increase it + hsv2rgb(baseHSV, baseRGB.color32); // convert back to RGB + } + } + if (gammaCorrectCol) brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution + renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); + } + + // apply 2D blur to rendered frame + if (smearBlur) { + SEGMENT.blur2D(smearBlur, smearBlur, true); + } +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void WLED_O2_ATTR ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY) { + uint32_t size = particlesize; + + if (perParticleSize && advPartProps != nullptr) // use advanced size properties + size = 1 + advPartProps[particleindex].size; // add 1 to avoid single pixel size particles (collisions do not support it) + + if (size == 0) { // single pixel rendering + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; + uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; + if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { + uint32_t index = x + (maxYpixel - y) * (maxXpixel + 1); // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer) + framebuffer[index] = fast_color_scaleAdd(framebuffer[index], color, brightness); + } + return; + } + + if (size > 1) { // size > 1: render as ellipse + renderLargeParticle(size, particleindex, brightness, color, wrapX, wrapY); // larger size rendering + return; + } + + // size = 1: standard 2x2 pixel rendering using bilinear interpolation (20% faster than ellipse rendering) + uint8_t pxlbrightness[4]; // brightness values for the four pixels representing a particle + struct { + int32_t x,y; + } pixco[4]; // particle pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] (thx @blazoncek for improved readability struct) + bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds + + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x--/y-- below) + // if sub-pixel position is 0-PS_P_HALFRADIUS it will render to x>>PS_P_RADIUS_SHIFT as the right pixel + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS; + int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS; + int32_t dx = xoffset & (PS_P_RADIUS - 1); // relativ particle position in subpixel space + int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2 + int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer) + int32_t y = (yoffset >> PS_P_RADIUS_SHIFT); + + // set the four raw pixel coordinates + pixco[1].x = pixco[2].x = x; // bottom right & top right + pixco[2].y = pixco[3].y = y; // top right & top left + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + y--; + pixco[0].x = pixco[3].x = x; // bottom left & top left + pixco[0].y = pixco[1].y = y; // bottom left & bottom right + + // calculate brightness values for all four pixels representing a particle using linear interpolation + // could check for out of frame pixels here but calculating them is faster (very few are out) + // precalculate values for speed optimization. Note: rounding is not perfect but close enough, some inaccuracy is traded for speed + int32_t precal1 = (int32_t)PS_P_RADIUS - dx; + int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness; + int32_t precal3 = dy * brightness; + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE + // adjust brightness such that distribution is linear after gamma correction: + // - scale brigthness with gamma correction (done in render()) + // - apply inverse gamma correction to brightness values + // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total + if (gammaCorrectCol) { + for (uint32_t i = 0; i < 4; i++) { + pxlbrightness[i] = gamma8inv(pxlbrightness[i]); // use look-up-table for invers gamma + } + } + + // standard rendering (2x2 pixels) + // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle + if (pixco[0].x < 0) { // left pixels out of frame + if (wrapX) { // wrap x to the other side if required + pixco[0].x = pixco[3].x = maxXpixel; + } else { + pixelvalid[0] = pixelvalid[3] = false; // out of bounds + if (pixco[0].x < -1) return; // both left pixels out of bounds, no need to continue (safety check) + } + } + else if (pixco[1].x > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame + if (wrapX) { // wrap y to the other side if required + pixco[1].x = pixco[2].x = 0; + } else { + pixelvalid[1] = pixelvalid[2] = false; // out of bounds + if (pixco[0].x > (int32_t)maxXpixel) return; // both pixels out of bounds, no need to continue (safety check) + } + } + + if (pixco[0].y < 0) { // bottom pixels out of frame + if (wrapY) { // wrap y to the other side if required + pixco[0].y = pixco[1].y = maxYpixel; + } else { + pixelvalid[0] = pixelvalid[1] = false; // out of bounds + if (pixco[0].y < -1) return; // both bottom pixels out of bounds, no need to continue (safety check) + } + } + else if (pixco[2].y > maxYpixel) { // top pixels + if (wrapY) { // wrap y to the other side if required + pixco[2].y = pixco[3].y = 0; + } else { + pixelvalid[2] = pixelvalid[3] = false; // out of bounds + if (pixco[2].y > (int32_t)maxYpixel + 1) return; // both top pixels out of bounds, no need to continue (safety check) + } + } + for (uint32_t i = 0; i < 4; i++) { + if (pixelvalid[i]) { + uint32_t idx = pixco[i].x + (maxYpixel - pixco[i].y) * (maxXpixel + 1); // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer) + framebuffer[idx] = fast_color_scaleAdd(framebuffer[idx], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } +} + +// render particle as ellipse/circle with linear brightness falloff and sub-pixel precision +void WLED_O2_ATTR ParticleSystem2D::renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY) { + // particle position with sub-pixel precision + int32_t x_subcenter = particles[particleindex].x; + int32_t y_subcenter = particles[particleindex].y; + + // example: for x = 128, a paticle is exacly between pixel 1 and 2, with a radius of 2 pixels, we draw pixels 0-3 + // integer center jumps when x = 127 -> pixel 1 goes to x = 128 -> pixel 2 + // when calculating the dx, we need to take this into account: at x = 128 the x offset is 1, the pixel center is at pixel 2: + // for pixel 1, dx = 1 * PS_P_RADIUS - 128 = -64 but the center of the pixel is actually only -32 from the particle center so need to add half a radius: + // dx = pixel_x * PS_P_RADIUS - x_subcenter + PS_P_HALFRADIUS + + // sub-pixel offset (0-63) + int32_t x_offset = x_subcenter & (PS_P_RADIUS - 1); // same as modulo PS_P_RADIUS but faster + int32_t y_offset = y_subcenter & (PS_P_RADIUS - 1); + // integer pixel position, this is rounded down + int32_t x_center = (x_subcenter) >> PS_P_RADIUS_SHIFT; + int32_t y_center = (y_subcenter) >> PS_P_RADIUS_SHIFT; + + // ellipse radii in pixels + uint32_t xsize = size; + uint32_t ysize = size; + if (advPartSize != nullptr && advPartSize[particleindex].asymmetry > 0) { + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + } + + int32_t rx_subpixel = xsize + PS_P_RADIUS + 1; // size = 1 means radius of just over 1 pixel, + PS_P_RADIUS (+1 to accoutn for bit-shift loss) + int32_t ry_subpixel = ysize + PS_P_RADIUS + 1; // size = 255 is radius of 5, so add 65 -> 65+255=320, 320>>6=5 pixels + + // rendering bounding box in pixels + int32_t rx_pixels = (rx_subpixel >> PS_P_RADIUS_SHIFT); + int32_t ry_pixels = (ry_subpixel >> PS_P_RADIUS_SHIFT); + + int32_t x_min = x_center - rx_pixels; // note: the "+1" extension needed for 1D is not required for 2D, it is smooth as-is + int32_t x_max = x_center + rx_pixels; + int32_t y_min = y_center - ry_pixels; + int32_t y_max = y_center + ry_pixels; + + // cache for speed + uint32_t matrixX = maxXpixel + 1; + uint32_t matrixY = maxYpixel + 1; + uint32_t rx_sq = rx_subpixel * rx_subpixel; + uint32_t ry_sq = ry_subpixel * ry_subpixel; + + // iterate over bounding box and render each pixel + for (int32_t py = y_min; py <= y_max; py++) { + for (int32_t px = x_min; px <= x_max; px++) { + // Check bounds and apply wrapping + int32_t render_x = px; + int32_t render_y = py; + if (render_x < 0) { + if (!wrapX) continue; + render_x += matrixX; + } else if (render_x > maxXpixel) { + if (!wrapX) continue; + render_x -= matrixX; + } + + if (render_y < 0) { + if (!wrapY) continue; + render_y += matrixY; + } else if (render_y > maxYpixel) { + if (!wrapY) continue; + render_y -= matrixY; + } + + // distance from particle center, explanation see above + int32_t dx_subpixel = (px << PS_P_RADIUS_SHIFT) - x_subcenter + PS_P_HALFRADIUS; + int32_t dy_subpixel = (py << PS_P_RADIUS_SHIFT) - y_subcenter + PS_P_HALFRADIUS; + + // calculate brightness based on squared distance to ellipse center + uint8_t pixel_brightness = calculateEllipseBrightness(dx_subpixel, dy_subpixel, rx_sq, ry_sq, brightness); + + if (pixel_brightness == 0) continue; // skip black pixels + + // apply inverse gamma correction if needed, if this is skipped, particles flicker due to changing total brightness + if (gammaCorrectCol) { + pixel_brightness = gamma8inv(pixel_brightness); // invert brigthess so brightness distribution is linear after gamma correction + } + + // Render pixel + uint32_t idx = render_x + (maxYpixel - render_y) * matrixX; // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer) + framebuffer[idx] = fast_color_scaleAdd(framebuffer[idx], color, pixel_brightness); + } + } +} + +// detect collisions in an array of particles and handle them +// uses binning by dividing the frame into slices in x direction which is efficient if using gravity in y direction (but less efficient for FX that use forces in x direction) +// for code simplicity, no y slicing is done, making very tall matrix configurations less efficient +// note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement +void ParticleSystem2D::handleCollisions() { + uint32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size + collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) + // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + int binWidth = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins + if (perParticleSize && advPartProps != nullptr) + overlap = 512; // max overlap for collision detection if using per-particle size, enough to catch all particles even at max speed + + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles + uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // number of bins in x direction + if (usedParticles < maxBinParticles) { + numBins = 1; // use single bin for small number of particles + binWidth = maxX + 1; + } + uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) + uint32_t binParticleCount; // number of particles in the current bin + uint32_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame + + // fill the binIndices array for this bin + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + binWidth + (overlap << 1); // add twice the overlap as start is start-overlap, note: last bin can be out of bounds, see above; + + // fill the binIndices array for this bin + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0) { // is alive + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if (particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; + } + } + } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around + } + + int32_t massratio1 = 0; // 0 means dont use mass ratio (equal mass) + int32_t massratio2 = 0; // TODO: if implementing "fixed" particles, set to 1 (fixed) and 255 (movable) + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles in this bin and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + if (perParticleSize && advPartProps != nullptr) { // using individual particle size + collDistSq = (PS_P_MINHARDRADIUS << 1) + ((((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) * 52) >> 6); // collision distance, use 80% of size for tighter stacking (slight overlap) + collDistSq = collDistSq * collDistSq; // square it for faster comparison + // calculate mass ratio for collision response + uint32_t mass1 = PS_P_RADIUS + advPartProps[idx_i].size; + uint32_t mass2 = PS_P_RADIUS + advPartProps[idx_j].size; + mass1 = mass1 * mass1; // mass proportional to area + mass2 = mass2 * mass2; + uint32_t totalmass = mass1 + mass2; + massratio1 = (mass2 << 8) / totalmass; // massratio 1 depends on mass of particle 2, i.e. if 2 is heavier -> higher velocity impact on 1 + massratio2 = (mass1 << 8) / totalmass; + } + // note: using the same logic as in 1D is much slower though it would be more accurate but it is not really needed in 2D: particles slipping through each other is much less visible + int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance with lookahead + if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) + int32_t dy = (particles[idx_j].y + particles[idx_j].vy) - (particles[idx_i].y + particles[idx_i].vy); // distance with lookahead + if (dy * dy < collDistSq) // particles are close + collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq, massratio1, massratio2); + } + } + } + } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame +} + +// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS +// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) +void WLED_O2_ATTR ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2) { + int32_t distanceSquared = dx * dx + dy * dy; + // Calculate relative velocity note: could zero check but that does not improve overall speed but deminish it as that is rarely the case and pushing is still required + int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; + int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy; + + // if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles apart if they are clumped before enabling collisions) + if (distanceSquared == 0) { + // Adjust positions based on relative velocity direction + dx = -1; + if (relativeVx < 0) // if true, particle2 is on the right side + dx = 1; + else if (relativeVx == 0) + relativeVx = 1; + + dy = -1; + if (relativeVy < 0) + dy = 1; + else if (relativeVy == 0) + relativeVy = 1; + + distanceSquared = 2; // 1 + 1 + } + + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other + + if (dotProduct < 0) {// particles are moving towards each other + // integer math is much faster than using floats (float divisions are slow on all ESPs) + // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen + // note: cannot use right shifts as bit shifting in right direction is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! the trick is: only shift positive numers + // Calculate new velocities after collision + int32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS >> 1); // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = (((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (is slightly faster) + + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t ximpulse = (impulse * dx + ((dx >> 31) & 0x7FFF)) >> 15; // note: extracting sign bit and adding rounding value to correct for asymmetry in right shifts + int32_t yimpulse = (impulse * dy + ((dy >> 31) & 0x7FFF)) >> 15; + #else + int32_t ximpulse = (impulse * dx) / 32767; + int32_t yimpulse = (impulse * dy) / 32767; + #endif + // if particles are not the same size, use a mass ratio. mass ratio is set to 0 if particles are the same size + if (massratio1) { + int32_t vx1 = (int32_t)particle1.vx - ((ximpulse * massratio1) >> 7); // mass ratio is in fixed point 8bit, multiply by two to account for the fact that we distribute the impulse to both particles + int32_t vy1 = (int32_t)particle1.vy - ((yimpulse * massratio1) >> 7); + int32_t vx2 = (int32_t)particle2.vx + ((ximpulse * massratio2) >> 7); + int32_t vy2 = (int32_t)particle2.vy + ((yimpulse * massratio2) >> 7); + // limit speeds to max speed (required if a lot of impulse is transferred from a large to a small particle) + particle1.vx = limitSpeed(vx1); + particle1.vy = limitSpeed(vy1); + particle2.vx = limitSpeed(vx2); + particle2.vy = limitSpeed(vy2); + } + else { + particle1.vx -= ximpulse; // note: impulse is inverted, so subtracting it + particle1.vy -= yimpulse; + particle2.vx += ximpulse; + particle2.vy += yimpulse; + } + if (collisionHardness < PS_P_MINSURFACEHARDNESS && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); + // Note: could call applyFriction, but this is faster and speed is key here + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + particle1.vx = ((int32_t)particle1.vx * coeff + (((int32_t)particle1.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + particle1.vy = ((int32_t)particle1.vy * coeff + (((int32_t)particle1.vy >> 31) & 0xFF)) >> 8; + particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8; + particle2.vy = ((int32_t)particle2.vy * coeff + (((int32_t)particle2.vy >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + particle1.vy = ((int32_t)particle1.vy * coeff) / 255; + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + particle2.vy = ((int32_t)particle2.vy * coeff) / 255; + #endif + } + } + // particles have volume, push particles apart if they are too close + // tried lots of configurations, what works best is to give one particle a little velocity. When adding hard pushing things tend to oscillate. + // when hard pushing by offsetting position without velocity, they tend to sink into each other under gravity. + // when using hard-pushing and velocity, there are some oscillations and softer particles do not pile nicely. + // oscillation get worse if pushing both particles so one is chosen somewhat randomly. + // softer collisions are not perfect on purpose: soft particles should pile up and overlap slightly, if separation is made perfect, it does not have the intended look + + if (distanceSquared < collDistSq && (relativeVx*relativeVx + relativeVy*relativeVy < 50)) { // too close and also slow, push them apart + bool fairlyrandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + int32_t pushamount = 1 + ((collDistSq - distanceSquared) >> 13); // found this by experimentation: it means push by 1, push more if overlapping more than 1.4 physical pixels (i.e. larger particles only) + int8_t pushx = dx > 0 ? -pushamount : pushamount; // particle 1 is on the left + int8_t pushy = dy > 0 ? -pushamount : pushamount; // particle 1 is below particle 2 + + // if they are very soft, stop slow particles completely to make them stick to each other + if (collisionHardness < 5) { + if (fairlyrandom) { // do not stop them every frame to avoid groups of particles hanging mid-air + particle1.vx = 0; + particle1.vy = 0; + particle2.vx = 0; + particle2.vy = 0; + // hard-push particle 1 only: if both are pushed, this oscillates ever so slightly + particle1.x += pushx; + particle1.y += pushy; + } + } + else { + if (fairlyrandom) { + particle1.vx += pushx; + //particle1.x += pushx; + particle1.vy += pushy; + //particle1.y += pushy; + } + else { + particle2.vx -= pushx; + //particle2.x -= pushx; + particle2.vy -= pushy; + //particle2.y -= pushy; + } + } + } +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) +void ParticleSystem2D::updateSystem(void) { + //PSPRINTLN("updateSystem2D"); + setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight()); + updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles + //PSPRINTLN("\n END update System2D, running FX..."); +} + +// set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) +// function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) +// FX handles the PSsources, need to tell this function how many there are +void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { + //PSPRINTLN("updatePSpointers"); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(this + 1); // pointer to particles + particleFlags = reinterpret_cast(particles + numParticles); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) + framebuffer = SEGMENT.getPixels(); // pointer to framebuffer + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data (already aligned to 4 byte boundary) + if (isadvanced) { + advPartProps = reinterpret_cast(PSdataEnd); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + if (sizecontrol) { + advPartSize = reinterpret_cast(PSdataEnd); + PSdataEnd = reinterpret_cast(advPartSize + numParticles); + } + } +#ifdef DEBUG_PS + Serial.printf_P(PSTR(" particles %p "), particles); + Serial.printf_P(PSTR(" sources %p "), sources); + Serial.printf_P(PSTR(" adv. props %p "), advPartProps); + Serial.printf_P(PSTR(" adv. ctrl %p "), advPartSize); + Serial.printf_P(PSTR("end %p\n"), PSdataEnd); + #endif + +} + +//non class functions to use for initialization +uint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanced, const bool sizecontrol) { + uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16) + uint32_t particlelimit = MAXPARTICLES_2D; // maximum number of paticles allowed + numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); // limit to 4 - particlelimit + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); + if (sizecontrol) // advanced property array needs ram, reduce number of particles + numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly + + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = (numberofParticles+3) & ~0x03; + return numberofParticles; +} + +uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) { + int numberofSources = min((pixels) / SOURCEREDUCTIONFACTOR, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, MAXSOURCES_2D)); // limit + // make sure it is a multiple of 4 for proper memory alignment + numberofSources = (numberofSources+3) & ~0x03; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX //TODO: add percentofparticles like in 1D to reduce memory footprint of some FX? +bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) { + PSPRINTLN("PS 2D alloc"); + PSPRINTLN("numparticles:" + String(numparticles) + " numsources:" + String(numsources) + " additionalbytes:" + String(additionalbytes)); + uint32_t requiredmemory = sizeof(ParticleSystem2D); + // functions above make sure numparticles is a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags) * numparticles; + requiredmemory += sizeof(PSparticle) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle) * numparticles; + if (sizecontrol) + requiredmemory += sizeof(PSsizeControl) * numparticles; + requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += additionalbytes; + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) { + PSPRINT("PS 2D init "); + if (!strip.isMatrix) { + errorFlag = ERR_NOT_IMPL; // TODO: need a better error code if more codes are added + SEGMENT.deallocateData(); // deallocate any data to make sure data is null (there is no valid PS in data and data can only be checked for null) + return false; // only for 2D + } + uint32_t cols = SEGMENT.virtualWidth(); + uint32_t rows = SEGMENT.virtualHeight(); + uint32_t pixels = cols * rows; + if (sizecontrol) + advanced = true; // size control needs advanced properties, prevent wrong usage + + uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); + PSPRINT(" segmentsize:" + String(cols) + " x " + String(rows)); + PSPRINTLN(" request numparticles:" + String(numparticles)); + uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); + bool allocsuccess = false; + while(numparticles >= 5) { // make sure we have at least 5 particles or quit + if (allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) { + PSPRINTLN(F("PS 2D alloc succeeded")); + allocsuccess = true; + break; // allocation succeeded + } + numparticles = ((numparticles / 2) + 3) & ~0x03; // cut number of particles in half and try again, must be 4 byte aligned + PSPRINTLN(F("PS 2D alloc failed, trying with less particles...")); + } + if (!allocsuccess) { + PSPRINTLN(F("PS 2D alloc failed, not enough memory!")); + return false; // allocation failed + } + + PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor + + PSPRINTLN(F("2D PS init done")); + return true; +} + +#endif // WLED_DISABLE_PARTICLESYSTEM2D + + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D + +ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) { + numSources = numberofsources; + numParticles = numberofparticles; // number of particles allocated in init + usedParticles = numParticles; // use all particles by default + advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) + //advPartSize = nullptr; + setSize(length); + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + setWallHardness(255); // set default wall hardness to max + setGravity(0); //gravity disabled by default + setParticleSize(0); // 1 pixel size by default + motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default + emitIndex = 0; + collisionStartIdx = 0; + // initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numSources; i++) { + sources[i].source.ttl = 1; //set source alive + sources[i].sourceFlags.asByte = 0; // all flags disabled + } + + if (isadvanced) { + for (uint32_t i = 0; i < numParticles; i++) { + advPartProps[i].sat = 255; // set full saturation + } + } +} + +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem1D::update(void) { + //apply gravity globally if enabled + if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse + applyGravity(); + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) { + handleCollisions(); + if (perParticleSize) + handleCollisions(); // second pass for per particle size (as impulse transfer can recoil at high speed, this improves "slip through" issues for small particles but is expensive) + } + + //move all particles + for (uint32_t i = 0; i < usedParticles; i++) { + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); + } + + if (particlesettings.colorByPosition) { + uint32_t scale = (255 << 16) / maxX; + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].hue = (scale * particles[i].x) >> 16; // note: x is > 0 if not out of bounds + } + } + + render(); +} + +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem1D::setUsedParticles(const uint8_t percentage) { + usedParticles = max((uint32_t)1, (numParticles * ((int)percentage+1)) >> 8); // number of particles to use (percentage is 0-255, 255 = 100%) + PSPRINT(" SetUsedpaticles: allocated particles: "); + PSPRINT(numParticles); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); +} + +void ParticleSystem1D::setWallHardness(const uint8_t hardness) { + wallHardness = hardness; +} + +void ParticleSystem1D::setSize(const uint32_t x) { + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements +} + +void ParticleSystem1D::setWrap(const bool enable) { + particlesettings.wrap = enable; +} + +void ParticleSystem1D::setBounce(const bool enable) { + particlesettings.bounce = enable; +} + +void ParticleSystem1D::setKillOutOfBounds(const bool enable) { + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem1D::setColorByAge(const bool enable) { + particlesettings.colorByAge = enable; +} + +void ParticleSystem1D::setColorByPosition(const bool enable) { + particlesettings.colorByPosition = enable; +} + +void ParticleSystem1D::setMotionBlur(const uint8_t bluramount) { + motionBlur = bluramount; +} + +void ParticleSystem1D::setSmearBlur(const uint8_t bluramount) { + smearBlur = bluramount; +} + +// render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), 255 = 18 pixel diameter +void ParticleSystem1D::setParticleSize(const uint8_t size) { + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS_1D; // ~1 pixel + perParticleSize = false; // disable per particle size control if global size is set + if (particlesize > 1) { + particleHardRadius = PS_P_MINHARDRADIUS_1D + ((particlesize * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking) + } + else if (particlesize == 0) + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel) +} + +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem1D::setGravity(const int8_t force) { + if (force) { + gforce = force; + particlesettings.useGravity = true; + } + else + particlesettings.useGravity = false; +} + +void ParticleSystem1D::enableParticleCollisions(const bool enable, const uint8_t hardness) { + particlesettings.useCollisions = enable; + collisionHardness = hardness; +} + +// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem1D::sprayEmit(const PSsource1D &emitter) { + for (uint32_t i = 0; i < usedParticles; i++) { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) { // find a dead particle + particles[emitIndex].vx = emitter.v + hw_random16(emitter.var << 1) - emitter.var; // random(-var,var) + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; // TODO: could just set all flags (asByte) but need to check if that breaks any of the FX + particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav; + particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual; + if (advPartProps) { + advPartProps[emitIndex].sat = emitter.sat; + advPartProps[emitIndex].size = emitter.size; + } + return emitIndex; + } + } + return -1; +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { + if (options == nullptr) + options = &particlesettings; // use PS system settings by default + + if (part.ttl > 0) { + if (!partFlags.perpetual) + part.ttl--; // age + if (options->colorByAge) + part.hue = min(part.ttl, (uint16_t)255); // set color to ttl + + int32_t renderradius = PS_P_HALFRADIUS_1D - 1 + particlesize; // used to check out of bounds, default for 2 pixel rendering + int32_t newX = part.x + (int32_t)part.vx; + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + if (perParticleSize && advancedproperties != nullptr) { // using individual particle size? + renderradius = PS_P_HALFRADIUS_1D - 1 + advancedproperties->size; // note: for single pixel particles, it should be zero, but it does not matter as out of bounds checking is done in rendering function + if (advancedproperties->size > 1) + particleHardRadius = PS_P_MINHARDRADIUS_1D + ((advancedproperties->size * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking) + else // single pixel particles use half the collision distance for walls + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; + } + + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + if (options->bounce) { + if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall + bool bouncethis = true; + if (options->useGravity) { + if (partFlags.reversegrav) { // skip bouncing at x = 0 + if (newX < (int32_t)particleHardRadius) + bouncethis = false; + } else if (newX > (int32_t)particleHardRadius) { // skip bouncing at x = max + bouncethis = false; + } + } + if (bouncethis) { + part.vx = -part.vx; // invert speed + part.vx = ((int32_t)part.vx * (int32_t)wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < (int32_t)particleHardRadius) + newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + newX = maxX - particleHardRadius; + } + } + } + + if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes + partFlags.outofbounds = true; + if (options->killoutofbounds) { + bool killthis = true; + if (options->useGravity) { // if gravity is used, only kill below 'floor level' + if (partFlags.reversegrav) { // skip at x = 0, do not skip far out of bounds + if (newX < 0 || newX > maxX << 2) + killthis = false; + } else { // skip at x = max, do not skip far out of bounds + if (newX > 0 && newX < maxX << 2) + killthis = false; + } + } + if (killthis) + part.ttl = 0; + } + } + + if (!partFlags.fixed) + part.x = newX; // set new position + else + part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away + } +} + +// apply a force in x direction to individual particle (or source) +// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame +void ParticleSystem1D::applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter) { + int32_t dv = calcForce_dv(xforce, counter); // velocity increase + part.vx = limitSpeed((int32_t)part.vx + dv); // apply the force to particle +} + +// apply a force to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem1D::applyForce(const int8_t xforce) { + int32_t dv = calcForce_dv(xforce, forcecounter); // velocity increase + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = limitSpeed((int32_t)particles[i].vx + dv); + } +} + +// apply gravity to all particles using PS global gforce setting +// gforce is in 3.4 fixed point notation, see note above +void ParticleSystem1D::applyGravity() { + int32_t dv_raw = calcForce_dv(gforce, gforcecounter); + for (uint32_t i = 0; i < usedParticles; i++) { + int32_t dv = dv_raw; + if (particleFlags[i].reversegrav) dv = -dv_raw; + // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags) { + uint32_t counterbkp = gforcecounter; + int32_t dv = calcForce_dv(gforce, gforcecounter); + if (partFlags.reversegrav) dv = -dv; + gforcecounter = counterbkp; //save it back + part.vx = limitSpeed((int32_t)part.vx - dv); +} + + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem1D::applyFriction(int32_t coefficient) { + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t friction = 256 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl) + particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + } + #else // division is faster on ESP32, S2 and S3 + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl) + particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; + } + #endif +} + + +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +void ParticleSystem1D::render() { + if (framebuffer == nullptr) { + PSPRINTLN(F("PS render: no framebuffer!")); + return; + } + CRGBW baseRGB; + uint32_t brightness; // particle brightness, fades if dying + TBlendType blend = LINEARBLEND; // default color rendering: wrap palette + if (particlesettings.colorByAge || particlesettings.colorByPosition) { + blend = LINEARBLEND_NOWRAP; + } + + if (motionBlur) { // blurring active + for (int32_t x = 0; x <= maxXpixel; x++) { + framebuffer[x] = fast_color_scale(framebuffer[x], motionBlur); + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, (maxXpixel+1) * sizeof(CRGBW)); + } + + // go over particles and render them to the buffer + for (uint32_t i = 0; i < usedParticles; i++) { + if ( particles[i].ttl == 0 || particleFlags[i].outofbounds) + continue; + + // generate RGB values for particle + brightness = min(particles[i].ttl << 1, (int)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend); + + if (advPartProps != nullptr) { //saturation is advanced property in 1D system + if (advPartProps[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv(baseRGB.color32, baseHSV); // convert to HSV + baseHSV.s = min(baseHSV.s, advPartProps[i].sat); // set the saturation but don't increase it + hsv2rgb(baseHSV, baseRGB.color32); // convert back to RGB + } + } + if (gammaCorrectCol) brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution + renderParticle(i, brightness, baseRGB, particlesettings.wrap); + } + // apply smear-blur to rendered frame + if (smearBlur) { + SEGMENT.blur(smearBlur, true); + } + + // add background color + CRGBW bg_color = SEGCOLOR(1); + if (bg_color > 0) { //if not black + for (int32_t i = 0; i <= maxXpixel; i++) { + framebuffer[i] = fast_color_scaleAdd(framebuffer[i], bg_color); + } + } +#ifndef WLED_DISABLE_2D + // transfer local buffer to segment if using 1D->2D mapping + if (SEGMENT.is2D() && SEGMENT.map1D2D) { + for (int x = 0; x <= maxXpixel; x++) { + //for (int x = 0; x < SEGMENT.vLength(); x++) { + SEGMENT.setPixelColor(x, framebuffer[x]); // this applies the mapping + } + } +#endif +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void WLED_O2_ATTR ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap) { + uint32_t size = particlesize; + if (perParticleSize && advPartProps != nullptr) // use advanced size properties + size = 1 + advPartProps[particleindex].size; // add 1 to avoid single pixel size particles (collisions do not support it) + + if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; + if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow + framebuffer[x] = fast_color_scaleAdd(framebuffer[x], color, brightness); + } + return; + } + //render larger particles + if (size > 1) { // size > 1: render as gradient line + renderLargeParticle(size, particleindex, brightness, color, wrap); // larger size rendering + return; + } + + // standard rendering (2 pixels per particle) + bool pxlisinframe[2] = {true, true}; + int32_t pxlbrightness[2]; + int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle + + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D; + int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space, modulo replaced with bitwise AND + int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) + + // set the raw pixel coordinates + pixco[1] = x; // right pixel + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + pixco[0] = x; // left pixel + + //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) + pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; + pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; + // adjust brightness such that distribution is linear after gamma correction: + // - scale brigthness with gamma correction (done in render()) + // - apply inverse gamma correction to brightness values + // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total -> fixes brightness fluctuations + if (gammaCorrectCol) { + pxlbrightness[0] = gamma8inv(pxlbrightness[0]); // use look-up-table for invers gamma + pxlbrightness[1] = gamma8inv(pxlbrightness[1]); + } + + // check if any pixels are out of frame + if (pixco[0] < 0) { // left pixels out of frame + if (wrap) // wrap x to the other side if required + pixco[0] = maxXpixel; + else { + pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render + if (pixco[0] < -1) + return; // both pixels out of frame (safety check) + } + } + else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow + if (wrap) // wrap y to the other side if required + pixco[1] = 0; + else { + pxlisinframe[1] = false; + if (pixco[0] > (int32_t)maxXpixel) + return; // both pixels out of frame (safety check) + } + } + for (uint32_t i = 0; i < 2; i++) { + if (pxlisinframe[i]) { + framebuffer[pixco[i]] = fast_color_scaleAdd(framebuffer[pixco[i]], color, pxlbrightness[i]); + } + } +} + +// render particle as a line with linear brightness falloff and sub-pixel precision, size is in 0-255 (1-9 pixel radius) +void WLED_O2_ATTR ParticleSystem1D::renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap) { + int32_t x_subcenter = particles[particleindex].x; // particle position in sub-pixel space + + // sub-pixel offset (0-31) + int32_t x_offset = x_subcenter & (PS_P_RADIUS_1D - 1); // same as modulo PS_P_RADIUS but faster + int32_t x_center = x_subcenter >> PS_P_RADIUS_SHIFT_1D; // integer pixel position, this is rounded down + + // particle radius in pixels, size = 1 means radius of just over 1 pixel + int32_t r_subpixel = size + PS_P_RADIUS_1D + 1; // size = 255 is radius of 9, so add 33 -> 33+255=288, 288>>5=9 pixels (i.e. the +1 is needed to correct for bitshift losses) + // rendering bounding box in pixels + int32_t r_pixels = r_subpixel >> PS_P_RADIUS_SHIFT_1D; + + int32_t x_min = x_center - r_pixels - 1; // extend by one for much smoother movement + int32_t x_max = x_center + r_pixels + 1; + + // cache for speed + uint32_t matrixX = maxXpixel + 1; + + // iterate over bounding box and render each pixel + for (int32_t px = x_min; px <= x_max; px++) { + // Check bounds and apply wrapping + int32_t render_x = px; + if (render_x < 0) { + if (!wrap) continue; // skip out of frame pixels + render_x += matrixX; + } else if (render_x > maxXpixel) { + if (!wrap) continue; + render_x -= matrixX; + } + // squared distance from particle center + int32_t dx_sq = ((px << PS_P_RADIUS_SHIFT_1D) - x_subcenter + PS_P_HALFRADIUS_1D); // explanation see 2D version + dx_sq = dx_sq * dx_sq; + int32_t rx_sq = r_subpixel * r_subpixel; + uint32_t dist_sq = (dx_sq << 8) / rx_sq; // normalized squared distance in fixed point (0-256) + + // calculate brightness based on distance from particle center with linear falloff + uint8_t pixel_brightness = dist_sq >= 256 ? 0 : ((256 - dist_sq) * brightness) >> 8; + //if (pixel_brightness == 0) continue; // skip black pixels note: very few pixels will be black, skipping this is usually faster + + // Render pixel + framebuffer[render_x] = fast_color_scaleAdd(framebuffer[render_x], color, pixel_brightness); + } +} + +// detect collisions in an array of particles and handle them +void ParticleSystem1D::handleCollisions() { + uint32_t collisiondistance = particleHardRadius << 1; // twice the radius is min distance between colliding particles + uint32_t checkDistSq = max(2 * PS_P_MAXSPEED, (int)collisiondistance); + if (perParticleSize && advPartProps != nullptr) // using individual particle size + checkDistSq = max(2 * PS_P_MAXSPEED, (512 * 52) >> 6); // max possible collision distance that catches all collisons + checkDistSq = checkDistSq * checkDistSq; // square it for distance comparison (faster than abs() ) + // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + int binWidth = 64 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy + int32_t overlap = collisiondistance + (2 * PS_P_MAXSPEED); // overlap bins to include edge particles to neighbouring bins (+ look-ahead of speed) + if (perParticleSize && advPartProps != nullptr) //may be using individual particle size + overlap = 512; // 2 * max radius, enough to catch all collisions even at full speed + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/4 of particles + uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // calculate number of bins + if (usedParticles < maxBinParticles) { + numBins = 1; // use single bin for small number of particles + binWidth = maxX + 1; + } + uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin + uint32_t binParticleCount; // number of particles in the current bin + uint32_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + binWidth + (overlap << 1); // add twice the overlap as start is start-overlap, note: last bin can be out of bounds, see above + + // fill the binIndices array for this bin + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0) { // alivee + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if (particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; + } + } + } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around + } + + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + int32_t dx = particles[idx_j].x - particles[idx_i].x; // distance between particles + uint32_t dx_sq = dx * dx; // square distance (faster than abs() and works the same) + if (dx_sq <= checkDistSq) { // possible collision imminent, check properly note: this is slower than using direct speed look-ahead (like in 2D) but more accurate and fast enough for 1D + collideParticles(idx_i, idx_j, dx, collisiondistance); // handle the collision + } + } + } + } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame +} +// handle a collision if close proximity is detected, i.e. dx smaller than 2*radius + speed look-ahead +void WLED_O2_ATTR ParticleSystem1D::collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance) { + int32_t massratio1 = 0; // 0 means dont use mass ratio (equal mass) + int32_t massratio2 = 0; + if (perParticleSize && advPartProps != nullptr) { // use advanced size properties, calculate collision distance and mass ratio + collisiondistance = (PS_P_MINHARDRADIUS_1D * 2) + ((((uint32_t)advPartProps[partIdx1].size + (uint32_t)advPartProps[partIdx2].size) * 52) >> 6); // collision distance, use 80% of size for tighter stacking (slight overlap) + // calculate mass ratio for collision response + uint32_t mass1 = PS_P_RADIUS_1D + advPartProps[partIdx1].size; + uint32_t mass2 = PS_P_RADIUS_1D + advPartProps[partIdx2].size; + uint32_t totalmass = mass1 + mass2 - 2; // -2 to account for rounding + massratio1 = (mass2 << 8) / totalmass; // massratio 1 depends on mass of particle 2, i.e. if 2 is heavier -> higher velocity impact on 1 + massratio2 = (mass1 << 8) / totalmass; + } + int32_t dv = (int)particles[partIdx2].vx - (int)particles[partIdx1].vx; + int32_t absdv = abs(dv); + int32_t dotProduct = (dx * dv); // is always negative if moving towards each other + uint32_t dx_abs = abs(dx); + + if (dotProduct < 0) { // particles are moving towards each other + uint32_t lookaheadDistance = collisiondistance + absdv; // add look-ahead: if reaching collisiondistance in this frame, collide + if (dx_abs <= lookaheadDistance) { + // if one of the particles is fixed, invert the other particle's velocity and multiply by hardness, also set its position to the edge of the fixed particle + if (particleFlags[partIdx1].fixed) { + particles[partIdx2].vx = -(particles[partIdx2].vx * collisionHardness) / 255; + particles[partIdx2].x = particles[partIdx1].x + (dx < 0 ? -collisiondistance : collisiondistance); // dv < 0 means particle2.x < particle1.x + return; + } + else if (particleFlags[partIdx2].fixed) { + particles[partIdx1].vx = -(particles[partIdx1].vx * collisionHardness) / 255; + particles[partIdx1].x = particles[partIdx2].x + (dx < 0 ? collisiondistance : -collisiondistance); + return; + } + int32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through + // Calculate new velocities after collision note: not using dot product like in 2D as impulse is purely speed depnedent + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t impulse = (dv * surfacehardness + ((dv >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + #else // division is faster on ESP32, S2 and S3 + int32_t impulse = (dv * surfacehardness) / 255; + #endif + + // if particles are not the same size, use a mass ratio. mass ratio is set to 0 if particles are the same size + if (massratio1) { + int vx1 = (int)particles[partIdx1].vx + ((impulse * massratio1) >> 7); // mass ratio is in fixed point 8bit + int vx2 = (int)particles[partIdx2].vx - ((impulse * massratio2) >> 7); + // limit speeds to max speed (required as a lot of impulse can be transferred from a large to a small particle) + particles[partIdx1].vx = limitSpeed(vx1); + particles[partIdx2].vx = limitSpeed(vx2); + } + else { + particles[partIdx1].vx += impulse; + particles[partIdx2].vx -= impulse; + } + + if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction + const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D); + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + particles[partIdx1].vx = ((int32_t)particles[partIdx1].vx * coeff + (((int32_t)particles[partIdx1].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + particles[partIdx2].vx = ((int32_t)particles[partIdx2].vx * coeff + (((int32_t)particles[partIdx2].vx >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 + particles[partIdx1].vx = ((int32_t)particles[partIdx1].vx * coeff) / 255; + particles[partIdx2].vx = ((int32_t)particles[partIdx2].vx * coeff) / 255; + #endif + } + } else { + return; // not close enough yet + } + } + // particles have volume, push particles apart if they are too close + // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer + + if (dx_abs < collisiondistance) { // too close, force push particles so they dont collapse + int32_t pushamount = 1 + ((collisiondistance - dx_abs) >> 3); // push by eighth of deviation (plus 1 to push at least a little), note: pushing too much leads to pass-throughs and more flickering + int32_t addspeed = 1; + if (dx < 0) { // particle2.x < particle1.x + pushamount = -pushamount; + addspeed = -addspeed; + } + if (absdv < 4) { // low relative speed, add speed to help with the pushing (less collapsing piles) + particles[partIdx1].vx -= addspeed; + particles[partIdx2].vx += addspeed; + } + // push only one particle to avoid oscillations + bool fairlyrandom = dotProduct & 0x01; + if (fairlyrandom) { + particles[partIdx1].x -= pushamount; + } + else { + particles[partIdx2].x += pushamount; + } + } +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) +void ParticleSystem1D::updateSystem(void) { + setSize(SEGMENT.vLength()); // update size + updatePSpointers(advPartProps != nullptr); +} + +// set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) +// function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) +// FX handles the PSsources, need to tell this function how many there are +void ParticleSystem1D::updatePSpointers(bool isadvanced) { + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(this + 1); // pointer to particles + particleFlags = reinterpret_cast(particles + numParticles); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data (already aligned to 4 byte boundary) +#ifndef WLED_DISABLE_2D + if (SEGMENT.is2D() && SEGMENT.map1D2D) { + framebuffer = reinterpret_cast(sources + numSources); // use local framebuffer for 1D->2D mapping + PSdataEnd = reinterpret_cast(framebuffer + SEGMENT.maxMappingLength()); // pointer to first available byte after the PS for FX additional data (still aligned to 4 byte boundary) + } + else +#endif + framebuffer = SEGMENT.getPixels(); // use segment buffer for standard 1D rendering + + if (isadvanced) { + advPartProps = reinterpret_cast(PSdataEnd); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); // since numParticles is a multiple of 4, this is always aligned to 4 bytes. No need to add padding bytes here + } + #ifdef WLED_DEBUG_PS + PSPRINTLN(" PS Pointers: "); + PSPRINT(" PS : 0x"); + Serial.println((uintptr_t)this, HEX); + PSPRINT(" Particleflags : 0x"); + Serial.println((uintptr_t)particleFlags, HEX); + PSPRINT(" Particles : 0x"); + Serial.println((uintptr_t)particles, HEX); + PSPRINT(" Sources : 0x"); + Serial.println((uintptr_t)sources, HEX); + #endif +} + +//non class functions to use for initialization, fraction is uint8_t: 255 means 100% +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced) { + uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) + uint32_t particlelimit = MAXPARTICLES_1D; // maximum number of paticles allowed + numberofParticles = min(numberofParticles, particlelimit); // limit to particlelimit + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D)); + numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles + numberofParticles = numberofParticles < 10 ? 10 : numberofParticles; // 10 minimum + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = (numberofParticles+3) & ~0x03; // note: with a separate particle buffer, this is probably unnecessary + PSPRINTLN(" calc numparticles:" + String(numberofParticles)); + return numberofParticles; +} + +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) { + int numberofSources = max(1, min((int)requestedsources,MAXSOURCES_1D)); // limit + // make sure it is a multiple of 4 for proper memory alignment (so minimum is acutally 4) + numberofSources = (numberofSources+3) & ~0x03; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) { + uint32_t requiredmemory = sizeof(ParticleSystem1D); + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags1D) * numparticles; + requiredmemory += sizeof(PSparticle1D) * numparticles; + requiredmemory += sizeof(PSsource1D) * numsources; +#ifndef WLED_DISABLE_2D + if (SEGMENT.is2D()) + requiredmemory += sizeof(uint32_t) * SEGMENT.maxMappingLength(); // need local buffer for mapped rendering +#endif + requiredmemory += additionalbytes; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +// note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { + if (SEGLEN == 1) { + errorFlag = ERR_NOT_IMPL; // TODO: need a better error code if more codes are added + SEGMENT.deallocateData(); // deallocate any data to make sure data is null (there is no valid PS in data and data can only be checked for null) + return false; // single pixel not supported + } + uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); + uint32_t numsources = calculateNumberOfSources1D(requestedsources); + bool allocsuccess = false; + while(numparticles >= 10) { // make sure we have at least 10 particles or quit + if (allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { + PSPRINT(F("PS 1D alloc succeeded")); + allocsuccess = true; + break; // allocation succeeded + } + numparticles = ((numparticles / 2) + 3) & ~0x03; // cut number of particles in half and try again, must be 4 byte aligned + PSPRINTLN(F("PS 1D alloc failed, trying with less particles...")); + } + if (!allocsuccess) { + PSPRINTLN(F("PS init failed: memory depleted")); + return false; // allocation failed + } + PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor + return true; +} +#endif // WLED_DISABLE_PARTICLESYSTEM1D + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled + +////////////////////////////// +// Shared Utility Functions // +////////////////////////////// + +// calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) +// force is in 3.4 fixedpoint notation, +/-127 +static int32_t calcForce_dv(const int8_t force, uint8_t &counter) { + if (force == 0) + return 0; + // for small forces, need to use a delay counter + int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) + int32_t dv = 0; + // for small forces, need to use a delay counter, apply force only if it overflows + if (force_abs < 16) { + counter += force_abs; + if (counter > 15) { + counter -= 16; + dv = force < 0 ? -1 : 1; // force is either 1 or -1 if it is small (zero force is handled above) + } + } + else + dv = force / 16; // MSBs, note: cannot use bitshift as dv can be negative + + return dv; +} + +// check if particle is out of bounds and wrap it around if required, returns false if out of bounds +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) { + if ((uint32_t)position > (uint32_t)max) { // check if particle reached an edge, cast to uint32_t to save negative checking (max is always positive) + if (wrap) { + position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled + if (position < 0) + position += max + 1; + } + else if (((position < -particleradius) || (position > max + particleradius))) // particle is leaving boundaries, out of bounds if it has fully left + return false; // out of bounds + } + return true; // particle is in bounds +} + +// this is a fast version for RGB color adding ignoring white channel (PS does not handle white) including scaling of second color +// note: function is mainly used to add scaled colors, so checking if one color is black is slower +static uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, const uint8_t scale) { + constexpr uint32_t MASK_RB = 0x00FF00FF; // red and blue mask + constexpr uint32_t MASK_G = 0x0000FF00; // green mask + + uint32_t rb = c2 & MASK_RB; // 0x00RR00BB + uint32_t g = c2 & MASK_G; // 0x0000GG00 + // scale second color + rb = ((rb * scale) >> 8) & MASK_RB; + g = ((g * scale) >> 8) & MASK_G; + // add colors + rb = (c1 & MASK_RB) + rb; + g = ((c1 & MASK_G) + g); + + // check for overflow by looking at the 9th bit of each channel + if ((rb | (g >> 8)) & 0x01000100) { + // find max among the three 16-bit values + g = g >> 8; // shift to get 0x000000GG + uint32_t max_val = (rb >> 16); // red + max_val = ((rb & 0xFFFF) > max_val) ? rb & 0xFFFF : max_val; // blue + max_val = (g > max_val) ? g : max_val; // green + // scale down to avoid saturation + uint32_t scale_factor = (255 << 8) / max_val; + rb = ((rb * scale_factor) >> 8) & MASK_RB; + g = (g * scale_factor) & MASK_G; + } + return rb | g; +} + +#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h new file mode 100644 index 0000000000..afe683c0b1 --- /dev/null +++ b/wled00/FXparticleSystem.h @@ -0,0 +1,420 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later +*/ + +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled + +#include +#include "wled.h" + +#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8), limiting below 127 to avoid overflows in collisions due to rounding errors +#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) + +//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash + +#ifdef WLED_DEBUG_PS + #define PSPRINT(x) Serial.print(x) + #define PSPRINTLN(x) Serial.println(x) +#else + #define PSPRINT(x) + #define PSPRINTLN(x) +#endif + +// limit speed of particles (used in 1D and 2D) +static inline int32_t limitSpeed(const int32_t speed) { + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this is slightly faster than using min/max at the cost of 50bytes of flash +} +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D +// memory allocation (based on reasonable segment size and available FX memory) +#ifdef ESP8266 + #define MAXPARTICLES_2D 256 + #define MAXSOURCES_2D 24 + #define SOURCEREDUCTIONFACTOR 8 +#elif ARDUINO_ARCH_ESP32S2 + #define MAXPARTICLES_2D 1024 + #define MAXSOURCES_2D 64 + #define SOURCEREDUCTIONFACTOR 6 +#else + #define MAXPARTICLES_2D 2048 + #define MAXSOURCES_2D 128 + #define SOURCEREDUCTIONFACTOR 4 +#endif + +// particle dimensions (subpixel division) +#define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement (must be a power of 2) +#define PS_P_HALFRADIUS (PS_P_RADIUS >> 1) +#define PS_P_RADIUS_SHIFT 6 // shift for RADIUS +#define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 +#define PS_P_MINHARDRADIUS 64 // minimum hard surface radius for collisions +#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky + +// struct for PS settings (shared for 1D and 2D class) +typedef union { + struct{ // one byte bit field for 2D settings + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings2D; + +//struct for a single particle +typedef struct { // 10 bytes + int16_t x; // x position in particle system + int16_t y; // y position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + int8_t vy; // vertical velocity + uint8_t hue; // color hue + uint8_t sat; // particle color saturation +} PSparticle; + +//struct for particle flags note: this is separate from the particle struct to save memory (ram alignment) +typedef union { + struct { // 1 byte + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + bool custom3 : 1; + bool custom4 : 1; + bool custom5 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags; + +// struct for additional particle settings (option) +typedef struct { // 2 bytes + uint8_t size; // particle size, 255 means 10 pixels in diameter, set perParticleSize = true to enable + uint8_t forcecounter; // counter for applying forces to individual particles +} PSadvancedParticle; + +// struct for advanced particle size control (option) +typedef struct { // 8 bytes + uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) + uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) + uint8_t maxsize; // target size for growing + uint8_t minsize; // target size for shrinking + uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) + uint8_t wobblecounter : 4; + uint8_t growspeed : 4; + uint8_t shrinkspeed : 4; + uint8_t wobblespeed : 4; + bool grow : 1; // flags + bool shrink : 1; + bool pulsate : 1; // grows & shrinks & grows & ... + bool wobble : 1; // alternate x and y size +} PSsizeControl; + + +//struct for a particle source (20 bytes) +typedef struct { + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle source; // use a particle as the emitter source (speed, position, color) + PSparticleFlags sourceFlags; // flags for the source particle + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t vx; // emitting speed + int8_t vy; + uint8_t size; // particle size (advanced property), global size is added on top to this size +} PSsource; + +// class uses approximately 60 bytes +class ParticleSystem2D { +public: + ParticleSystem2D(const uint32_t width, const uint32_t height, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false, const bool sizecontrol = false); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateFire(const uint8_t intensity); // update function for fire + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + // particle emitters + int32_t sprayEmit(const PSsource &emitter); + void flameEmit(const PSsource &emitter); + int32_t angleEmit(PSsource& emitter, const uint16_t angle, const int32_t speed); + //particle physics + void applyGravity(PSparticle &part); // applies gravity to single particle (use this for sources) + [[gnu::hot]] void applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter); + [[gnu::hot]] void applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce); // use this for advanced property particles + void applyForce(const int8_t xforce, const int8_t yforce); // apply a force to all particles + void applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter); + void applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle); // use this for advanced property particles + void applyAngleForce(const int8_t force, const uint16_t angle); // apply angular force to all particles + void applyFriction(PSparticle &part, const int32_t coefficient); // apply friction to specific particle + void applyFriction(const int32_t coefficient); // apply friction to all used particles + void pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow); + // set options note: inlining the set function uses more flash so dont optimize + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% + void setCollisionHardness(const uint8_t hardness); // hardness for particle collisions (255 means full hard) + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setWallRoughness(const uint8_t roughness); // wall roughness randomizes wall collisions + void setMatrixSize(const uint32_t x, const uint32_t y); + void setWrapX(const bool enable); + void setWrapY(const bool enable); + void setBounceX(const bool enable); + void setBounceY(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die + void setSaturation(const uint8_t sat); // set global color saturation + void setColorByAge(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 2D smeared blurring of full frame + void setParticleSize(const uint8_t size); + void setGravity(const int8_t force = 8); + void enableParticleCollisions(const bool enable, const uint8_t hardness = 255); + + PSparticle *particles; // pointer to particle array + PSparticleFlags *particleFlags; // pointer to particle flags array + PSsource *sources; // pointer to sources + PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) + PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 + uint32_t numSources; // number of sources + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' + bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize()) + //note: some variables are 32bit for speed and code size at the cost of ram + +private: + //rendering functions + void render(); + [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY); + void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY); + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + void collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2); + void fireParticleupdate(); + //utility functions + void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space + bool updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + // note: variables that are accessed often are 32bit for speed + uint32_t *framebuffer; // frame buffer for rendering. note: using CRGBW as the buffer is slower, ESP compiler seems to optimize this better giving more consistent FPS + PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + uint32_t numParticles; // total number of particles allocated by this system + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint32_t wallHardness; + uint32_t wallRoughness; // randomizes wall collisions + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) + uint16_t collisionStartIdx; // particle array start index for collision detection + uint8_t fireIntesity = 0; // fire intensity, used for fire mode (flash use optimization, better than passing an argument to render function) + uint8_t forcecounter; // counter for globally applied forces + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + // global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles, set to 0 or 1 for standard advanced particle rendering) + uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t smearBlur; // 2D smeared blurring of full frame +}; + +// initialization functions (not part of class) +bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false); +uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol); +uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources); +bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes); + +// distance-based brightness for ellipse rendering, returns brightness (0-255) based on distance from ellipse center +inline uint8_t calculateEllipseBrightness(int32_t dx, int32_t dy, int32_t rxsq, int32_t rysq, uint8_t maxBrightness) { + // square the distances + uint32_t dx_sq = dx * dx; + uint32_t dy_sq = dy * dy; + + uint32_t dist_sq = ((dx_sq << 8) / rxsq) + ((dy_sq << 8) / rysq); // normalized squared distance in fixed point: (dx²/rx²) * 256 + (dy²/ry²) * 256 + + if (dist_sq >= 256) return 0; // pixel is outside the ellipse, unit radius in fixed point: 256 = 1.0 + //if (dist_sq <= 96) return maxBrightness; // core at full brightness + int32_t falloff = 256 - dist_sq; + return (maxBrightness * falloff) >> 8; // linear falloff + //return (maxBrightness * falloff * falloff) >> 16; // squared falloff for even softer edges +} +#endif // WLED_DISABLE_PARTICLESYSTEM2D + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +// memory allocation +#ifdef ESP8266 + #define MAXPARTICLES_1D 320 + #define MAXSOURCES_1D 16 +#elif ARDUINO_ARCH_ESP32S2 + #define MAXPARTICLES_1D 1300 + #define MAXSOURCES_1D 32 +#else + #define MAXPARTICLES_1D 2600 + #define MAXSOURCES_1D 64 +#endif + + + +// particle dimensions (subpixel division) +#define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS_1D (PS_P_RADIUS_1D >> 1) +#define PS_P_RADIUS_SHIFT_1D 5 // 1 << PS_P_RADIUS_SHIFT = PS_P_RADIUS +#define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D +#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius note: do not change or hourglass effect will be broken +#define PS_P_MINSURFACEHARDNESS_1D 120 // minimum hardness used in collision impulse calculation + +// struct for PS settings (shared for 1D and 2D class) +typedef union { + struct{ + // one byte bit field for 1D settings + bool wrap : 1; + bool bounce : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment + bool unused : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings1D; + +//struct for a single particle (8 bytes) +typedef struct { + int32_t x; // x position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + uint8_t hue; // color hue +} PSparticle1D; + +//struct for particle flags +typedef union { + struct { // 1 byte + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool reversegrav : 1; // if set, gravity is reversed on this particle + bool forcedirection : 1; // direction the force was applied, 1 is positive x-direction (used for collision stacking, similar to reversegrav) TODO: not used anymore, can be removed + bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags1D; + +// struct for additional particle settings (optional) +typedef struct { + uint8_t sat; // color saturation + uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting + uint8_t forcecounter; // counter for applying forces to individual particles +} PSadvancedParticle1D; + +//struct for a particle source (20 bytes) +typedef struct { + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle1D source; // use a particle as the emitter source (speed, position, color) + PSparticleFlags1D sourceFlags; // flags for the source particle + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t v; // emitting speed + uint8_t sat; // color saturation (advanced property) + uint8_t size; // particle size (advanced property) + // note: there is 3 bytes of padding added here +} PSsource1D; + +class ParticleSystem1D +{ +public: + ParticleSystem1D(const uint32_t length, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + // particle emitters + int32_t sprayEmit(const PSsource1D &emitter); + void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function + //particle physics + [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + void applyForce(const int8_t xforce); // apply a force to all particles + void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) + void applyFriction(const int32_t coefficient); // apply friction to all used particles + // set options + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setSize(const uint32_t x); //set particle system size (= strip length) + void setWrap(const bool enable); + void setBounce(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die + // void setSaturation(uint8_t sat); // set global color saturation + void setColorByAge(const bool enable); + void setColorByPosition(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame + void setParticleSize(const uint8_t size); // particle diameter: size 0 = 1 pixel, size 1 = 2 pixels, size = 255 = 10 pixels, disables per particle size control if called + void setGravity(int8_t force = 8); + void enableParticleCollisions(bool enable, const uint8_t hardness = 255); + + + PSparticle1D *particles; // pointer to particle array + PSparticleFlags1D *particleFlags; // pointer to particle flags array + PSsource1D *sources; // pointer to sources + PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) + //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + int32_t maxX; // particle system size i.e. width-1, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + uint32_t numSources; // number of sources + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' + bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize()) + +private: + //rendering functions + void render(void); + void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap); + void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap); + + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + void collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance); + + //utility functions + void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space + //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + // note: variables that are accessed often are 32bit for speed + uint32_t *framebuffer; // frame buffer for rendering. note: using CRGBW as the buffer is slower, ESP compiler seems to optimize this better giving more consistent FPS + PSsettings1D particlesettings; // settings used when updating particles + uint32_t numParticles; // total number of particles allocated by this system + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection + uint32_t wallHardness; + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + uint8_t forcecounter; // counter for globally applied forces + uint16_t collisionStartIdx; // particle array start index for collision detection + //global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, is overruled by advanced particle size + uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations + uint8_t smearBlur; // smeared blurring of full frame +}; + +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles = 255, const uint32_t additionalbytes = 0, const bool advanced = false); +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced); +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources); +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes); + +#endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/NodeStruct.h b/wled00/NodeStruct.h index 34f73ab418..9647162054 100644 --- a/wled00/NodeStruct.h +++ b/wled00/NodeStruct.h @@ -15,6 +15,22 @@ #define NODE_TYPE_ID_ESP32S3 34 #define NODE_TYPE_ID_ESP32C3 35 +// updated node types from the ESP Easy project +// https://github.com/letscontrolit/ESPEasy/blob/mega/src/src/DataTypes/NodeTypeID.h +//#define NODE_TYPE_ID_ESP32 33 +//#define NODE_TYPE_ID_ESP32S2 34 +//#define NODE_TYPE_ID_ESP32C3 35 +//#define NODE_TYPE_ID_ESP32S3 36 +#define NODE_TYPE_ID_ESP32C2 37 +#define NODE_TYPE_ID_ESP32H2 38 +#define NODE_TYPE_ID_ESP32C6 39 +#define NODE_TYPE_ID_ESP32C61 40 +#define NODE_TYPE_ID_ESP32C5 41 +#define NODE_TYPE_ID_ESP32P4 42 +#define NODE_TYPE_ID_ESP32P4r3 45 +#define NODE_TYPE_ID_ESP32H21 43 +#define NODE_TYPE_ID_ESP32H4 44 + /*********************************************************************************************\ * NodeStruct \*********************************************************************************************/ diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index b108f294b3..81b9ec3469 100644 --- a/wled00/alexa.cpp +++ b/wled00/alexa.cpp @@ -126,10 +126,10 @@ void onAlexaChange(EspalexaDevice* dev) } else { colorKtoRGB(k, rgbw); } - strip.setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); + strip.getMainSegment().setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); } else { uint32_t color = dev->getRGB(); - strip.setColor(0, color); + strip.getMainSegment().setColor(0, color); } stateUpdated(CALL_MODE_ALEXA); } diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 5b031bebbf..7d0d976381 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -5,74 +5,63 @@ #include #include #ifdef ARDUINO_ARCH_ESP32 +#include +#include "src/dependencies/network/Network.h" // for isConnected() (& WiFi) #include "driver/ledc.h" #include "soc/ledc_struct.h" - #if !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)) - #define LEDC_MUTEX_LOCK() do {} while (xSemaphoreTake(_ledc_sys_lock, portMAX_DELAY) != pdPASS) - #define LEDC_MUTEX_UNLOCK() xSemaphoreGive(_ledc_sys_lock) - extern xSemaphoreHandle _ledc_sys_lock; - #else - #define LEDC_MUTEX_LOCK() - #define LEDC_MUTEX_UNLOCK() - #endif #endif -#include "const.h" -#include "pin_manager.h" -#include "bus_wrapper.h" +#ifdef ESP8266 +#include "core_esp8266_waveform.h" +#endif #include "bus_manager.h" +#include "bus_wrapper.h" +#include "wled.h" + +// functions to get/set bits in an array - based on functions created by Brandon for GOL +// toDo : make this a class that's completely defined in a header file +// note: these functions are automatically inline by the compiler +static inline bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit value + size_t byteIndex = position >> 3; // divide by 8 + unsigned bitIndex = position & 0x07; // modulo 8 + uint8_t byteValue = byteArray[byteIndex]; + return (byteValue >> bitIndex) & 1; +} -extern bool cctICused; - -//colors.cpp -uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t 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 +static inline void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr + //if (byteArray == nullptr) return; + size_t byteIndex = position >> 3; // divide by 8 + unsigned bitIndex = position & 0x07; // modulo 8 + if (value) + byteArray[byteIndex] |= (1 << bitIndex); + else + byteArray[byteIndex] &= ~(1 << bitIndex); +} -#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) - #define DEBUG_PRINTF_P(x...) DEBUGOUT.printf_P(x) -#else - #define DEBUG_PRINT(x) - #define DEBUG_PRINTLN(x) - #define DEBUG_PRINTF(x...) - #define DEBUG_PRINTF_P(x...) -#endif +static inline size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits + return (num_bits + 7) >> 3; +} -//color mangling macros -#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) -#define R(c) (byte((c) >> 16)) -#define G(c) (byte((c) >> 8)) -#define B(c) (byte(c)) -#define W(c) (byte((c) >> 24)) +static inline void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value + if (byteArray == nullptr) return; + size_t len = getBitArrayBytes(numBits); + if (value) memset(byteArray, 0xFF, len); + else memset(byteArray, 0x00, len); +} +static ColorOrderMap _colorOrderMap = {}; bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { if (count() >= WLED_MAX_COLOR_ORDER_MAPPINGS || len == 0 || (colorOrder & 0x0F) > COL_ORDER_MAX) return false; // upper nibble contains W swap information _mappings.push_back({start,len,colorOrder}); + DEBUGBUS_PRINTF_P(PSTR("Bus: Add COM (%d,%d,%d)\n"), (int)start, (int)len, (int)colorOrder); return true; } uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { // upper nibble contains W swap information // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used - for (unsigned i = 0; i < count(); i++) { - if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { - return _mappings[i].colorOrder | ((_mappings[i].colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); - } + for (const auto& map : _mappings) { + if (pix >= map.start && pix < (map.start + map.len)) return map.colorOrder | ((map.colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); } return defaultColorOrder; } @@ -88,83 +77,108 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { } else { cct = (approximateKelvinFromRGB(c) - 1900) >> 5; // convert K (from RGB value) to relative format } - - //0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold) - if (cct < _cctBlend) ww = 255; - else ww = ((255-cct) * 255) / (255 - _cctBlend); - if ((255-cct) < _cctBlend) cw = 255; - else cw = (cct * 255) / (255 - _cctBlend); + + // CCT blending modes (_cctBlend): + // blend<0: ww: ▓▓▒░__ | blend=0: ww: ▓▒▒░░ | blend>0 ww: ▓▓▓▒░ + // cw: __░▒▓▓ | cw: ░░▒▒▓ | cw: ░▒▓▓▓ + int32_t ww_val, cw_val; + if (_cctBlend < 0) { + uint16_t range = 255 - 2 * (uint16_t)(-_cctBlend); + if (range > 255) range = 255; // prevent overflow + ww_val = range ? ((int32_t)(255 + _cctBlend - cct) * 255) / range : (cct < 128 ? 255 : 0); // exclusive blending + cw_val = 255 - ww_val; + } else { + ww_val = _cctBlend ? ((int32_t)(255 - cct) * 255) / (255 - _cctBlend) : 255 - cct; // additive blending + cw_val = _cctBlend ? ((int32_t) cct * 255) / (255 - _cctBlend) : cct; + } + ww = (uint8_t)(ww_val < 0 ? 0 : ww_val > 255 ? 255 : ww_val); + cw = (uint8_t)(cw_val < 0 ? 0 : cw_val > 255 ? 255 : cw_val); ww = (w * ww) / 255; //brightness scaling cw = (w * cw) / 255; } -uint32_t Bus::autoWhiteCalc(uint32_t c) const { +// calculates white channel and CCT values based on given settings +uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { unsigned aWM = _autoWhiteMode; if (_gAWM < AW_GLOBAL_DISABLED) aWM = _gAWM; - if (aWM == RGBW_MODE_MANUAL_ONLY) return c; + CRGBW cIn = c; // save original color for CCT calculation unsigned w = W(c); - //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) - if (w > 0 && aWM == RGBW_MODE_DUAL) return c; - unsigned r = R(c); - unsigned g = G(c); - unsigned b = B(c); - if (aWM == RGBW_MODE_MAX) return RGBW32(r, g, b, r > g ? (r > b ? r : b) : (g > b ? g : b)); // brightest RGB channel - w = r < g ? (r < b ? r : b) : (g < b ? g : b); - if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode - return RGBW32(r, g, b, w); -} - -uint8_t *Bus::allocateData(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); + if (aWM != RGBW_MODE_MANUAL_ONLY) { + unsigned r = R(c); // note: using uint8_t generates larger code + unsigned g = G(c); + unsigned b = B(c); + if (aWM == RGBW_MODE_DUAL && w > 0) { + //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) + } else if (aWM == RGBW_MODE_MAX) { + w = r > g ? (r > b ? r : b) : (g > b ? g : b); // brightest RGB channel + } else { + w = r < g ? (r < b ? r : b) : (g < b ? g : b); // darkest RGB channel + if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode + } + c = RGBW32(r, g, b, w); + } + if (_hasCCT) { + cIn.w = w; // need original rgb values in case CCT is derived from RGB + calculateCCT(cIn, ww, cw); + } + return c; } -BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) +BusDigital::BusDigital(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814)) , _skip(bc.skipAmount) //sacrificial pixels , _colorOrder(bc.colorOrder) , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) -, _colorOrderMap(com) +, _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S) { - if (!isDigital(bc.type) || !bc.count) return; - if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; + DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); + if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } + _iType = bc.iType; // reuse the iType that was determined by polyBus in getI() in finalizeInit() + if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; } + + if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; } _frequencykHz = 0U; + _colorSum = 0; _pins[0] = bc.pins[0]; if (is2Pin(bc.type)) { if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { cleanup(); + DEBUGBUS_PRINTLN(F("Pin 1 allocated!")); 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; + _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = hasCCT(bc.type); - if (bc.doubleBuffer && !allocateData(bc.count * Bus::getNumberOfChannels(bc.type))) return; - //_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); - _valid = (_busPtr != nullptr); - DEBUG_PRINTF_P(PSTR("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u). mA=%d/%d\n"), _valid?"S":"Uns", nr, bc.count, bc.type, _pins[0], is2Pin(bc.type)?_pins[1]:255, _iType, _milliAmpsPerLed, _milliAmpsMax); + _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip); + _valid = (_busPtr != nullptr) && bc.count > 0; + // fix for wled#4759 + if (_valid) for (unsigned i = 0; i < _skip; i++) { + PolyBus::setPixelColor(_busPtr, _iType, i, 0, COL_ORDER_GRB); // set sacrificial pixels to black (CO does not matter here) + } + else { + cleanup(); + } + DEBUGBUS_PRINTF_P(PSTR("Bus len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [itype:%u, driver:%s] mA=%d/%d %s\n"), + (int)bc.count, + (int)bc.type, + (int)_hasRgb, (int)_hasWhite, (int)_hasCCT, + (unsigned)_pins[0], is2Pin(bc.type)?(unsigned)_pins[1]:255U, + (unsigned)_iType, + isI2S() ? "I2S" : "RMT", + (int)_milliAmpsPerLed, (int)_milliAmpsMax, + _valid ? " " : "FAILED" + ); } -//fine tune power estimation constants for your setup -//you can set it to 0 if the ESP is powered by USB and the LEDs by external -#ifndef MA_FOR_ESP - #ifdef ESP8266 - #define MA_FOR_ESP 80 //how much mA does the ESP use (Wemos D1 about 80mA) - #else - #define MA_FOR_ESP 120 //how much mA does the ESP use (ESP32 about 120mA) - #endif -#endif - //DISCLAIMER //The following function attemps to calculate the current LED power usage, //and will limit the brightness to stay below a set amperage threshold. @@ -172,118 +186,70 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) //Stay safe with high amperage and have a reasonable safety margin! //I am NOT to be held liable for burned down garages or houses! -// To disable brightness limiter we either set output max current to 0 or single LED current to 0 -uint8_t BusDigital::estimateCurrentAndLimitBri() { - bool useWackyWS2815PowerModel = false; - byte actualMilliampsPerLed = _milliAmpsPerLed; - - if (_milliAmpsMax < MA_FOR_ESP/BusManager::getNumBusses() || actualMilliampsPerLed == 0) { //0 mA per LED and too low numbers turn off calculation - return _bri; - } +// note on ABL implementation: +// ABL is set up in finalizeInit() +// scaled color channels are summed in BusDigital::setPixelColor() +// the used current is estimated and limited in BusManager::show() +// if limit is set too low, brightness is limited to 1 to at least show some light +// to disable brightness limiter for a bus, set LED current to 0 +void BusDigital::estimateCurrent() { + uint32_t actualMilliampsPerLed = _milliAmpsPerLed; if (_milliAmpsPerLed == 255) { - useWackyWS2815PowerModel = true; + // use wacky WS2815 power model, see WLED issue #549 + _colorSum *= 3; // sum is sum of max value for each color, need to multiply by three to account for clrUnitsPerChannel being 3*255 actualMilliampsPerLed = 12; // from testing an actual strip } + // _colorSum has all the values of color channels summed, max would be getLength()*(3*255 + (255 if hasWhite()): convert to milliAmps + uint32_t clrUnitsPerChannel = hasWhite() ? 4*255 : 3*255; + _milliAmpsTotal = ((uint64_t)_colorSum * actualMilliampsPerLed) / clrUnitsPerChannel + getLength(); // add 1mA standby current per LED to total (WS2812: ~0.7mA, WS2815: ~2mA) +} - size_t powerBudget = (_milliAmpsMax - MA_FOR_ESP/BusManager::getNumBusses()); //80/120mA for ESP power - if (powerBudget > getLength()) { //each LED uses about 1mA in standby, exclude that from power budget - powerBudget -= getLength(); - } else { - powerBudget = 0; - } - - uint32_t busPowerSum = 0; - for (unsigned i = 0; i < getLength(); i++) { //sum up the usage of each LED - uint32_t c = 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; +void BusDigital::applyBriLimit(uint8_t newBri) { + // a newBri of 0 means calculate per-bus brightness limit + _NPBbri = 255; // reset, intermediate value is set below, final value is calculated in bus::show() + if (newBri == 0) { + if (_milliAmpsLimit == 0 || _milliAmpsTotal == 0) return; // ABL not used for this bus + newBri = 255; + + if (_milliAmpsLimit > getLength()) { // each LED uses about 1mA in standby + if (_milliAmpsTotal > _milliAmpsLimit) { + // scale brightness down to stay in current limit + newBri = ((uint32_t)_milliAmpsLimit * 255) / _milliAmpsTotal + 1; // +1 to avoid 0 brightness + _milliAmpsTotal = _milliAmpsLimit; + } } else { - busPowerSum += (r + g + b + w); + newBri = 1; // limit too low, set brightness to 1, this will dim down all colors to minimum since we use video scaling + _milliAmpsTotal = getLength(); // estimate bus current as minimum } } - if (hasWhite()) { //RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less - busPowerSum *= 3; - busPowerSum >>= 2; //same as /= 4 + if (newBri < 255) { + _NPBbri = newBri; // store value so it can be updated in show() (must be updated even if ABL is not used) + uint16_t wwcw = 0; + unsigned hwLen = _len; + if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus + for (unsigned i = 0; i < hwLen; i++) { + uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); // need to revert color order for correct color scaling and CCT calc in case white is swapped + uint32_t c = PolyBus::getPixelColor(_busPtr, _iType, i, co); // Note: if ABL would be calculated as a seperate loop (as it was before) it is slower but could use original color, making it more color-accurate + if (hasCCT()) { + uint8_t cctWW, cctCW; + Bus::calculateCCT(c, cctWW, cctCW); // calculate CCT before fade (more accurate) | Note: if using "accurate" white calculation mode, approximateKelvinFromRGB can be very inaccurate (white is subtracted) + wwcw = ((cctCW + 1) * newBri) & 0xFF00; // apply brightness to CCT (leave it in upper byte for 16bit NeoPixelBus value) + wwcw |= ((cctWW + 1) * newBri) >> 8; + } + c = color_fade(c, newBri, true); // apply additional dimming note: using inline version is a bit faster but overhead of getPixelColor() dominates the speed impact by far + PolyBus::setPixelColor(_busPtr, _iType, i, c, co, wwcw); // repaint all pixels with new brightness + } } - // powerSum has all the values of channels summed (max would be getLength()*765 as white is excluded) so convert to milliAmps - busPowerSum = (busPowerSum * actualMilliampsPerLed) / 765; - _milliAmpsTotal = busPowerSum * _bri / 255; - - uint8_t newBri = _bri; - if (busPowerSum * _bri / 255 > powerBudget) { //scale brightness down to stay in current limit - float scale = (float)(powerBudget * 255) / (float)(busPowerSum * _bri); - if (scale >= 1.0f) return _bri; - _milliAmpsTotal = ceilf((float)_milliAmpsTotal * scale); - uint8_t scaleB = min((int)(scale * 255), 255); - newBri = unsigned(_bri * scaleB) / 256 + 1; - } - return newBri; + _colorSum = 0; // reset for next frame } void BusDigital::show() { - _milliAmpsTotal = 0; if (!_valid) return; - - uint8_t cctWW = 0, cctCW = 0; - unsigned newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal - if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, newBri); // limit brightness to stay within current limits - - if (_data) { - size_t channels = getNumberOfChannels(); - int16_t oldCCT = Bus::_cct; // temporarily save bus CCT - for (size_t i=0; i<_len; i++) { - size_t offset = i * channels; - unsigned 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 { - if (hasRGB()) c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); - else c = RGBW32(0, 0, 0, _data[offset]); - } - if (hasCCT()) { - // unfortunately as a segment may span multiple buses or a bus may contain multiple segments and each segment may have different CCT - // we need to extract and appy CCT value for each pixel individually even though all buses share the same _cct variable - // TODO: there is an issue if CCT is calculated from RGB value (_cct==-1), we cannot do that with double buffer - Bus::_cct = _data[offset+channels-1]; - Bus::calculateCCT(c, cctWW, cctCW); - } - unsigned pix = i; - if (_reversed) pix = _len - pix -1; - pix += _skip; - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); - } - #if !defined(STATUSLED) || STATUSLED>=0 - if (_skip) PolyBus::setPixelColor(_busPtr, _iType, 0, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black - #endif - for (int i=1; i<_skip; i++) PolyBus::setPixelColor(_busPtr, _iType, i, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black - Bus::_cct = oldCCT; - } else { - if (newBri < _bri) { - unsigned hwLen = _len; - if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus - for (unsigned i = 0; i < hwLen; i++) { - // 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), _bri); - if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW); // this will unfortunately corrupt (segment) CCT data on every bus - PolyBus::setPixelColor(_busPtr, _iType, i, c, 0, (cctCW<<8) | cctWW); // repaint all pixels with new brightness - } - } - } - PolyBus::show(_busPtr, _iType, !_data); // faster if buffer consistency is not important (use !_buffering this causes 20% FPS drop) - // 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 < _bri) PolyBus::setBrightness(_busPtr, _iType, _bri); + _NPBbri = (_NPBbri * _bri) / 255; // total applied brightness for use in restoreColorLossy (see applyBriLimit()) + PolyBus::show(_busPtr, _iType, _skip); // faster if buffer consistency is not important (no skipped LEDs) } bool BusDigital::canShow() const { @@ -291,12 +257,6 @@ bool BusDigital::canShow() const { return PolyBus::canShow(_busPtr, _iType); } -void BusDigital::setBrightness(uint8_t b) { - if (_bri == b) return; - Bus::setBrightness(b); - PolyBus::setBrightness(_busPtr, _iType, b); -} - //If LEDs are skipped, it is possible to use the first as a status LED. //TODO only show if no new show due in the next 50ms void BusDigital::setStatusPixel(uint32_t c) { @@ -306,85 +266,89 @@ void BusDigital::setStatusPixel(uint32_t c) { } } -void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { +// note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash +void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; - uint8_t cctWW = 0, cctCW = 0; - if (hasWhite()) c = autoWhiteCalc(c); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT - if (_data) { - size_t offset = pix * getNumberOfChannels(); - if (hasRGB()) { - _data[offset++] = R(c); - _data[offset++] = G(c); - _data[offset++] = B(c); + uint8_t cctWW = 0, cctCW = 0; + uint16_t wwcw = 0; + if (hasWhite()) c = autoWhiteCalc(c, cctWW, cctCW); + c = color_fade(c, _bri, true); // apply brightness + + if (hasCCT()) { + wwcw = ((cctCW + 1) * _bri) & 0xFF00; // apply brightness to CCT (store CW in upper byte) + wwcw |= ((cctWW + 1) * _bri) >> 8; + if (_type == TYPE_WS2812_WWA) c = RGBW32(wwcw, wwcw >> 8, 0, W(c)); // ww,cw, 0, w + } + + if (BusManager::_useABL) { + // if using ABL, sum all color channels to estimate current and limit brightness in show() + uint8_t r = R(c), g = G(c), b = B(c); + if (_milliAmpsPerLed < 255) { // normal ABL + _colorSum += r + g + b + W(c); + } else { // wacky WS2815 power model, ignore white channel, use max of RGB (issue #549) + _colorSum += ((r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b)); } - if (hasWhite()) _data[offset++] = W(c); - // unfortunately as a segment may span multiple buses or a bus may contain multiple segments and each segment may have different CCT - // we need to store CCT value for each pixel (if there is a color correction in play, convert K in CCT ratio) - if (hasCCT()) _data[offset] = Bus::_cct >= 1900 ? (Bus::_cct - 1900) >> 5 : (Bus::_cct < 0 ? 127 : Bus::_cct); // TODO: if _cct == -1 we simply ignore it - } else { - if (_reversed) pix = _len - pix -1; - pix += _skip; - unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); - if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - unsigned 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; - } + } + + if (_reversed) pix = _len - pix -1; + pix += _skip; + const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs + unsigned pOld = pix; + pix = IC_INDEX_WS2812_1CH_3X(pix); + uint32_t cOld = PolyBus::getPixelColor(_busPtr, _iType, pix, co); + 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; } - if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW); - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); } + + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); } -// returns original color if global buffering is enabled, else returns lossly restored color from bus -uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) const { +// returns lossly restored color from bus +uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { if (!_valid) return 0; - if (_data) { - size_t offset = pix * getNumberOfChannels(); - uint32_t c; - if (!hasRGB()) { - c = RGBW32(_data[offset], _data[offset], _data[offset], _data[offset]); - } else { - c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); - } - return c; - } else { - if (_reversed) pix = _len - pix -1; - pix += _skip; - unsigned 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 - unsigned r = R(c); - unsigned g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? - unsigned 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; - } + if (_reversed) pix = _len - pix -1; + pix += _skip; + const 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),_NPBbri); + 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; } + if (_type == TYPE_WS2812_WWA) { + uint8_t w = R(c) | G(c); + c = RGBW32(w, w, 0, w); + } + return c; } -uint8_t BusDigital::getPins(uint8_t* pinArray) const { +size_t BusDigital::getPins(uint8_t* pinArray) const { unsigned numPins = is2Pin(_type) + 1; if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } +size_t BusDigital::getBusSize() const { + return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) : 0); // does not include common I2S DMA buffer +} + void BusDigital::setColorOrder(uint8_t colorOrder) { // upper nibble contains W swap information if ((colorOrder & 0x0F) > 5) return; _colorOrder = colorOrder; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusDigital::getLEDTypes() { return { {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, @@ -400,8 +364,8 @@ std::vector BusDigital::getLEDTypes() { {TYPE_WS2805, "D", PSTR("WS2805 RGBCW")}, {TYPE_SM16825, "D", PSTR("SM16825 RGBCW")}, {TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")}, - //{TYPE_WS2812_2CH_X3, "D", PSTR("WS2811 CCT")}, // not implemented - //{TYPE_WS2812_WWA, "D", PSTR("WS2811 WWA")}, // not implemented + //{TYPE_WS2812_2CH_X3, "D", PSTR("WS281x CCT")}, // not implemented + {TYPE_WS2812_WWA, "D", PSTR("WS281x WWA")}, // amber ignored {TYPE_WS2801, "2P", PSTR("WS2801")}, {TYPE_APA102, "2P", PSTR("APA102")}, {TYPE_LPD8806, "2P", PSTR("LPD8806")}, @@ -410,18 +374,21 @@ std::vector BusDigital::getLEDTypes() { }; } +bool BusDigital::isI2S() { + return (_iType & 0x01) == 0; // I2S types have even iType values +} + void BusDigital::begin() { if (!_valid) return; PolyBus::begin(_busPtr, _iType, _pins, _frequencykHz); } void BusDigital::cleanup() { - DEBUG_PRINTLN(F("Digital Cleanup.")); + DEBUGBUS_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); } @@ -445,18 +412,18 @@ void BusDigital::cleanup() { #else #ifdef SOC_LEDC_TIMER_BIT_WIDE_NUM // C6/H2/P4: 20 bit, S2/S3/C2/C3: 14 bit - #define MAX_BIT_WIDTH SOC_LEDC_TIMER_BIT_WIDE_NUM + #define MAX_BIT_WIDTH SOC_LEDC_TIMER_BIT_WIDE_NUM #else // ESP32: 20 bit (but in reality we would never go beyond 16 bit as the frequency would be to low) #define MAX_BIT_WIDTH 14 #endif #endif -BusPwm::BusPwm(BusConfig &bc) +BusPwm::BusPwm(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed, bc.refreshReq) // hijack Off refresh flag to indicate usage of dithering { if (!isPWM(bc.type)) return; - unsigned numPins = numPWMPins(bc.type); + const unsigned numPins = numPWMPins(bc.type); [[maybe_unused]] const bool dithering = _needsRefresh; _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ; // duty cycle resolution (_depth) can be extracted from this formula: CLOCK_FREQUENCY > _frequency * 2^_depth @@ -464,53 +431,52 @@ BusPwm::BusPwm(BusConfig &bc) managed_pin_type pins[numPins]; for (unsigned i = 0; i < numPins; i++) pins[i] = {(int8_t)bc.pins[i], true}; - if (!PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) return; - -#ifdef ESP8266 - analogWriteRange((1<<_depth)-1); - analogWriteFreq(_frequency); -#else - // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer - _ledcStart = PinManager::allocateLedc(numPins); - if (_ledcStart == 255) { //no more free LEDC channels - PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); - return; - } - // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) - if (dithering) _depth = 12; // fixed 8 bit depth PWM with 4 bit dithering (ESP8266 has no hardware to support dithering) -#endif - - for (unsigned i = 0; i < numPins; i++) { - _pins[i] = bc.pins[i]; // store only after allocateMultiplePins() succeeded + if (PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) { #ifdef ESP8266 - pinMode(_pins[i], OUTPUT); + analogWriteRange((1<<_depth)-1); + analogWriteFreq(_frequency); #else - unsigned channel = _ledcStart + i; - ledcSetup(channel, _frequency, _depth - (dithering*4)); // with dithering _frequency doesn't really matter as resolution is 8 bit - ledcAttachPin(_pins[i], channel); - // LEDC timer reset credit @dedehai - uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup() - ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift) + // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer + _ledcStart = PinManager::allocateLedc(numPins); + if (_ledcStart == 255) { //no more free LEDC channels + PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); + DEBUGBUS_PRINTLN(F("No more free LEDC channels!")); + return; + } + // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) + if (dithering) _depth = 12; // fixed 8 bit depth PWM with 4 bit dithering (ESP8266 has no hardware to support dithering) #endif + + for (unsigned i = 0; i < numPins; i++) { + _pins[i] = bc.pins[i]; // store only after allocateMultiplePins() succeeded + #ifdef ESP8266 + pinMode(_pins[i], OUTPUT); + #else + unsigned channel = _ledcStart + i; + ledcSetup(channel, _frequency, _depth - (dithering*4)); // with dithering _frequency doesn't really matter as resolution is 8 bit + ledcAttachPin(_pins[i], channel); + // LEDC timer reset credit @dedehai + uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup() + ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift) + #endif + } + _hasRgb = hasRGB(bc.type); + _hasWhite = hasWhite(bc.type); + _hasCCT = hasCCT(bc.type); + _valid = true; } - _hasRgb = hasRGB(bc.type); - _hasWhite = hasWhite(bc.type); - _hasCCT = hasCCT(bc.type); - _data = _pwmdata; // avoid malloc() and use stack - _valid = true; - DEBUG_PRINTF_P(PSTR("%successfully inited PWM strip with type %u, frequency %u, bit depth %u and pins %u,%u,%u,%u,%u\n"), _valid?"S":"Uns", bc.type, _frequency, _depth, _pins[0], _pins[1], _pins[2], _pins[3], _pins[4]); + DEBUGBUS_PRINTF_P(PSTR("%successfully inited PWM strip with type %u, frequency %u, bit depth %u and pins %u,%u,%u,%u,%u\n"), _valid?"S":"Uns", bc.type, _frequency, _depth, _pins[0], _pins[1], _pins[2], _pins[3], _pins[4]); } -void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { +void BusPwm::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel - if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c); if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT } - uint8_t r = R(c); - uint8_t g = G(c); - uint8_t b = B(c); - uint8_t w = W(c); + uint8_t cctWW, cctCW; + if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c, cctWW, cctCW); + uint8_t r = R(c), g = G(c), b = B(c), w = W(c); + // note: no color scaling, brightness is applied in show() switch (_type) { case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation @@ -521,14 +487,18 @@ void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { _data[0] = w; _data[1] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct; } else { - Bus::calculateCCT(c, _data[0], _data[1]); + _data[0] = cctWW; + _data[1] = cctCW; } break; case TYPE_ANALOG_5CH: //RGB + warm white + cold white if (cctICused) _data[4] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct; - else - Bus::calculateCCT(c, w, _data[4]); + else { + w = cctWW; + _data[4] = cctCW; + } + // fall through to set RGBW channels case TYPE_ANALOG_4CH: //RGBW _data[3] = w; case TYPE_ANALOG_3CH: //standard dumb RGB @@ -538,7 +508,7 @@ void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { } //does no index check -uint32_t BusPwm::getPixelColor(uint16_t pix) const { +uint32_t BusPwm::getPixelColor(unsigned pix) const { if (!_valid) return 0; // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented) switch (_type) { @@ -560,50 +530,57 @@ uint32_t BusPwm::getPixelColor(uint16_t pix) const { void BusPwm::show() { if (!_valid) return; + const size_t numPins = getPins(); +#ifdef ESP8266 + const unsigned analogPeriod = F_CPU / _frequency; + const unsigned maxBri = analogPeriod; // compute to clock cycle accuracy + constexpr bool dithering = false; + constexpr unsigned bitShift = 8; // 256 clocks for dead time, ~3us at 80MHz +#else // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) - // https://github.com/Aircoookie/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1) + // https://github.com/wled/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1) const bool dithering = _needsRefresh; // avoid working with bitfield - const unsigned numPins = getPins(); - const unsigned maxBri = (1<<_depth); // possible values: 16384 (14), 8192 (13), 4096 (12), 2048 (11), 1024 (10), 512 (9) and 256 (8) - [[maybe_unused]] const unsigned bitShift = dithering * 4; // if dithering, _depth is 12 bit but LEDC channel is set to 8 bit (using 4 fractional bits) - - // use CIE brightness formula (cubic) to fit (or approximate linearity of) human eye perceived brightness - // the formula is based on 12 bit resolution as there is no need for greater precision + const unsigned maxBri = (1<<_depth); // possible values: 16384 (14), 8192 (13), 4096 (12), 2048 (11), 1024 (10), 512 (9) and 256 (8) + const unsigned bitShift = dithering * 4; // if dithering, _depth is 12 bit but LEDC channel is set to 8 bit (using 4 fractional bits) +#endif + // use CIE brightness formula (linear + cubic) to approximate human eye perceived brightness // see: https://en.wikipedia.org/wiki/Lightness - unsigned pwmBri = (unsigned)_bri * 100; // enlarge to use integer math for linear response - if (pwmBri < 2040) { - // linear response for values [0-20] - pwmBri = ((pwmBri << 12) + 115043) / 230087; //adding '0.5' before division for correct rounding - } else { - // cubic response for values [21-255] - pwmBri += 4080; - float temp = (float)pwmBri / 29580.0f; - temp = temp * temp * temp * (float)maxBri; - pwmBri = (unsigned)temp; // pwmBri is in range [0-maxBri] + unsigned pwmBri = _bri; + if (pwmBri < 21) { // linear response for values [0-20] + pwmBri = (pwmBri * maxBri + 2300 / 2) / 2300 ; // adding '0.5' before division for correct rounding, 2300 gives a good match to CIE curve + } else { // cubic response for values [21-255] + float temp = float(pwmBri + 41) / float(255 + 41); // 41 is to match offset & slope to linear part + temp = temp * temp * temp * (float)maxBri; + pwmBri = (unsigned)temp; // pwmBri is in range [0-maxBri] C } [[maybe_unused]] unsigned hPoint = 0; // phase shift (0 - maxBri) // we will be phase shifting every channel by previous pulse length (plus dead time if required) - // phase shifting is only mandatory when using H-bridge to drive reverse-polarity PWM CCT (2 wire) LED type + // phase shifting is only mandatory when using H-bridge to drive reverse-polarity PWM CCT (2 wire) LED type // CCT additive blending must be 0 (WW & CW will not overlap) otherwise signals *will* overlap // for all other cases it will just try to "spread" the load on PSU // Phase shifting requires that LEDC timers are synchronised (see setup()). For PWM CCT (and H-bridge) it is // also mandatory that both channels use the same timer (pinManager takes care of that). for (unsigned i = 0; i < numPins; i++) { - unsigned duty = (_data[i] * pwmBri) / 255; - #ifdef ESP8266 - if (_reversed) duty = maxBri - duty; - analogWrite(_pins[i], duty); - #else - int deadTime = 0; - if (_type == TYPE_ANALOG_2CH && Bus::getCCTBlend() == 0) { + unsigned duty = (_data[i] * pwmBri) / 255; + unsigned deadTime = 0; + + if (_type == TYPE_ANALOG_2CH && Bus::_cctBlend <= 0) { // add dead time between signals (when using dithering, two full 8bit pulses are required) deadTime = (1+dithering) << bitShift; // we only need to take care of shortening the signal at (almost) full brightness otherwise pulses may overlap - if (_bri >= 254 && duty >= maxBri / 2 && duty < maxBri) duty -= deadTime << 1; // shorten duty of larger signal except if full on - if (_reversed) deadTime = -deadTime; // need to invert dead time to make phaseshift go the opposite way so low signals dont overlap + if (_bri >= 254 && duty >= maxBri / 2 && duty < maxBri) { + duty -= deadTime << 1; // shorten duty of larger signal except if full on + } + } + if (_reversed) { + if (i) hPoint += duty; // align start at time zero + duty = maxBri - duty; } - if (_reversed) duty = maxBri - duty; + #ifdef ESP8266 + //stopWaveform(_pins[i]); // can cause the waveform to miss a cycle. instead we risk crossovers. + startWaveformClockCycles(_pins[i], duty, analogPeriod - duty, 0, i ? _pins[0] : -1, hPoint, false); + #else unsigned channel = _ledcStart + i; unsigned gr = channel/8; // high/low speed group unsigned ch = channel%8; // group channel @@ -612,20 +589,22 @@ void BusPwm::show() { LEDC.channel_group[gr].channel[ch].duty.duty = duty << ((!dithering)*4); // lowest 4 bits are used for dithering, shift by 4 bits if not using dithering LEDC.channel_group[gr].channel[ch].hpoint.hpoint = hPoint >> bitShift; // hPoint is at _depth resolution (needs shifting if dithering) ledc_update_duty((ledc_mode_t)gr, (ledc_channel_t)ch); - hPoint += duty + deadTime; // offset to cascade the signals - if (hPoint >= maxBri) hPoint = 0; // offset it out of bounds, reset #endif + + if (!_reversed) hPoint += duty; + hPoint += deadTime; // offset to cascade the signals + if (hPoint >= maxBri) hPoint -= maxBri; // offset is out of bounds, reset } } -uint8_t BusPwm::getPins(uint8_t* pinArray) const { +size_t BusPwm::getPins(uint8_t* pinArray) const { if (!_valid) return 0; unsigned numPins = numPWMPins(_type); if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusPwm::getLEDTypes() { return { {TYPE_ANALOG_1CH, "A", PSTR("PWM White")}, @@ -638,7 +617,7 @@ std::vector BusPwm::getLEDTypes() { } void BusPwm::deallocatePins() { - unsigned numPins = getPins(); + size_t numPins = getPins(); for (unsigned i = 0; i < numPins; i++) { PinManager::deallocatePin(_pins[i], PinOwner::BusPwm); if (!PinManager::isPinOk(_pins[i])) continue; @@ -654,9 +633,9 @@ void BusPwm::deallocatePins() { } -BusOnOff::BusOnOff(BusConfig &bc) +BusOnOff::BusOnOff(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) -, _onoffdata(0) +, _data(0) { if (!Bus::isOnOff(bc.type)) return; @@ -669,45 +648,39 @@ BusOnOff::BusOnOff(BusConfig &bc) _hasRgb = false; _hasWhite = false; _hasCCT = false; - _data = &_onoffdata; // avoid malloc() and use stack _valid = true; - DEBUG_PRINTF_P(PSTR("%successfully inited On/Off strip with pin %u\n"), _valid?"S":"Uns", _pin); + DEBUGBUS_PRINTF_P(PSTR("%successfully inited On/Off strip with pin %u\n"), _valid?"S":"Uns", _pin); } -void BusOnOff::setPixelColor(uint16_t pix, uint32_t c) { +void BusOnOff::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel - c = autoWhiteCalc(c); - uint8_t r = R(c); - 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; + _data = (c > 0) && bool(_bri) ? 0xFF : 0; // if any color channel is on and brightness is not zero, set to on } -uint32_t BusOnOff::getPixelColor(uint16_t pix) const { +uint32_t BusOnOff::getPixelColor(unsigned pix) const { if (!_valid) return 0; - return RGBW32(_data[0], _data[0], _data[0], _data[0]); + return RGBW32(_data, _data, _data, _data); } void BusOnOff::show() { if (!_valid) return; - digitalWrite(_pin, _reversed ? !(bool)_data[0] : (bool)_data[0]); + digitalWrite(_pin, _reversed ? !(bool)_data : (bool)_data); } -uint8_t BusOnOff::getPins(uint8_t* pinArray) const { +size_t BusOnOff::getPins(uint8_t* pinArray) const { if (!_valid) return 0; if (pinArray) pinArray[0] = _pin; return 1; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusOnOff::getLEDTypes() { return { {TYPE_ONOFF, "", PSTR("On/Off")}, }; } -BusNetwork::BusNetwork(BusConfig &bc) +BusNetwork::BusNetwork(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, bc.count) , _broadcastLock(false) { @@ -730,13 +703,19 @@ BusNetwork::BusNetwork(BusConfig &bc) _hasCCT = false; _UDPchannels = _hasWhite + 3; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); - _valid = (allocateData(_len * _UDPchannels) != nullptr); - DEBUG_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); + #ifdef ARDUINO_ARCH_ESP32 + _hostname = bc.text; + resolveHostname(); // resolve hostname to IP address if needed + #endif + _data = (uint8_t*)d_calloc(_len, _UDPchannels); + _valid = (_data != nullptr); + DEBUGBUS_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); } -void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { +void BusNetwork::setPixelColor(unsigned pix, uint32_t c) { if (!_valid || pix >= _len) return; - if (_hasWhite) c = autoWhiteCalc(c); + uint8_t ww, cw; // dummy, unused + if (_hasWhite) c = autoWhiteCalc(c, ww, cw); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT unsigned offset = pix * _UDPchannels; _data[offset] = R(c); @@ -745,7 +724,7 @@ void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { if (_hasWhite) _data[offset+3] = W(c); } -uint32_t BusNetwork::getPixelColor(uint16_t pix) const { +uint32_t BusNetwork::getPixelColor(unsigned pix) const { if (!_valid || pix >= _len) return 0; unsigned offset = pix * _UDPchannels; return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (hasWhite() ? _data[offset+3] : 0)); @@ -758,12 +737,25 @@ void BusNetwork::show() { _broadcastLock = false; } -uint8_t BusNetwork::getPins(uint8_t* pinArray) const { +size_t BusNetwork::getPins(uint8_t* pinArray) const { if (pinArray) for (unsigned i = 0; i < 4; i++) pinArray[i] = _client[i]; return 4; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +#ifdef ARDUINO_ARCH_ESP32 +void BusNetwork::resolveHostname() { + static unsigned long nextResolve = 0; + if (Network.isConnected() && millis() > nextResolve && _hostname.length() > 0) { + nextResolve = millis() + 600000; // resolve only every 10 minutes + IPAddress clnt; + if (strlen(cmDNS) > 0) clnt = MDNS.queryHost(_hostname); + else WiFi.hostByName(_hostname.c_str(), clnt); + if (clnt != IPAddress()) _client = clnt; + } +} +#endif + +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusNetwork::getLEDTypes() { return { {TYPE_NET_DDP_RGB, "N", PSTR("DDP RGB (network)")}, // should be "NNNN" to determine 4 "pin" fields @@ -774,55 +766,443 @@ std::vector BusNetwork::getLEDTypes() { //{TYPE_VIRTUAL_I2C_W, "V", PSTR("I2C White (virtual)")}, // allows setting I2C address in _pin[0] //{TYPE_VIRTUAL_I2C_CCT, "V", PSTR("I2C CCT (virtual)")}, // allows setting I2C address in _pin[0] //{TYPE_VIRTUAL_I2C_RGB, "VVV", PSTR("I2C RGB (virtual)")}, // allows setting I2C address in _pin[0] and 2 additional values in _pin[1] & _pin[2] - //{TYPE_USERMOD, "VVVVV", PSTR("Usermod (virtual)")}, // 5 data fields (see https://github.com/Aircoookie/WLED/pull/4123) + //{TYPE_USERMOD, "VVVVV", PSTR("Usermod (virtual)")}, // 5 data fields (see https://github.com/wled/WLED/pull/4123) }; } void BusNetwork::cleanup() { + DEBUGBUS_PRINTLN(F("Virtual Cleanup.")); + d_free(_data); + _data = nullptr; _type = I_NONE; _valid = false; - freeData(); } +// *************************************************************************** -//utility to get the approx. memory usage of a given BusConfig -uint32_t BusManager::memUsage(BusConfig &bc) { - if (Bus::isOnOff(bc.type) || Bus::isPWM(bc.type)) return OUTPUT_MAX_PINS; +#ifdef WLED_ENABLE_HUB75MATRIX +#warning "HUB75 driver enabled (experimental)" +#ifdef ESP8266 +#error ESP8266 does not support HUB75 +#endif - unsigned len = bc.count + bc.skipAmount; - unsigned channels = Bus::getNumberOfChannels(bc.type); - unsigned multiplier = 1; - if (Bus::isDigital(bc.type)) { // digital types - if (Bus::is16bit(bc.type)) len *= 2; // 16-bit LEDs - #ifdef ESP8266 - if (bc.pins[0] == 3) { //8266 DMA uses 5x the mem - multiplier = 5; +BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) { + size_t lastHeap = ESP.getFreeHeap(); + _valid = false; + _hasRgb = true; + _hasWhite = false; + virtualDisp = nullptr; // todo: this should be solved properly, can cause memory leak (if omitted here, nothing seems to work) + // aliases for easier reading + uint8_t panelWidth = bc.pins[0]; + uint8_t panelHeight = bc.pins[1]; + uint8_t chainLength = bc.pins[2]; + _rows = bc.pins[3]; + _cols = bc.pins[4]; + + mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer + + // mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver + // mxconfig.driver = HUB75_I2S_CFG::FM6124; // try this driver in case you panel stays dark, or when colors look too pastel + + // mxconfig.latch_blanking = 3; + // mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz + // mxconfig.min_refresh_rate = 90; + // mxconfig.min_refresh_rate = 120; + + mxconfig.clkphase = bc.reversed; + // allow chain length up to 4, limit to prevent bad data from preventing boot due to low memory + mxconfig.chain_length = max((uint8_t) 1, min(chainLength, (uint8_t) 4)); + + if (mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) { + DEBUGBUS_PRINTLN(F("WARNING, only single panel can be used of 64 pixel boards due to memory")); + mxconfig.chain_length = 1; + } + + if (bc.type == TYPE_HUB75MATRIX_HS) { + mxconfig.mx_width = min((uint8_t) 64, panelWidth); // TODO: UI limit is 128, this limits to 64 + mxconfig.mx_height = min((uint8_t) 64, panelHeight); + } else if (bc.type == TYPE_HUB75MATRIX_QS) { + _isVirtual = true; + mxconfig.mx_width = min((uint8_t) 64, panelWidth) * 2; + mxconfig.mx_height = min((uint8_t) 64, panelHeight) / 2; + } else { + DEBUGBUS_PRINTLN("Unknown type"); + return; + } + +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)// classic esp32, or esp32-s2: reduce bitdepth for large panels + if (mxconfig.mx_height >= 64) { + if (mxconfig.chain_length * mxconfig.mx_width > 192) mxconfig.setPixelColorDepthBits(3); + else if (mxconfig.chain_length * mxconfig.mx_width > 64) mxconfig.setPixelColorDepthBits(4); + else mxconfig.setPixelColorDepthBits(8); + } else mxconfig.setPixelColorDepthBits(8); +#endif + + + +// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN}; + +#if defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3 + + // https://www.adafruit.com/product/5778 + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Matrix Portal S3 config"); + mxconfig.gpio = { 42, 41, 40, 38, 39, 37, 45, 36, 48, 35, 21, 47, 14, 2 }; + +#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM + +#if defined(MOONHUB_S3_PINOUT) + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - T7 S3 with PSRAM, MOONHUB pinout"); + + // HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN}; + mxconfig.gpio = { 1, 5, 6, 7, 13, 9, 16, 48, 47, 21, 38, 8, 4, 18 }; + +#else + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - S3 with PSRAM"); + // HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN}; + mxconfig.gpio = {1, 2, 42, 41, 40, 39, 45, 48, 47, 21, 38, 8, 3, 18}; +#endif +#elif defined(ESP32_FORUM_PINOUT) // Common format for boards designed for SmartMatrix + + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - ESP32_FORUM_PINOUT"); +/* + ESP32 with SmartMatrix's default pinout - ESP32_FORUM_PINOUT + https://github.com/pixelmatix/SmartMatrix/blob/teensylc/src/MatrixHardware_ESP32_V0.h + Can use a board like https://github.com/rorosaurus/esp32-hub75-driver +*/ + + mxconfig.gpio = { 2, 15, 4, 16, 27, 17, 5, 18, 19, 21, 12, 26, 25, 22 }; + +#else + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Default pins"); + /* + https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA?tab=readme-ov-file + + Boards + + https://esp32trinity.com/ + https://www.electrodragon.com/product/rgb-matrix-panel-drive-interface-board-for-esp32-dma/ + + */ + mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 19, 5, 17, 18, 4, 15, 16 }; + +#endif + + int8_t pins[PIN_COUNT]; + memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio)); + if (!PinManager::allocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75, true)) { + DEBUGBUS_PRINTLN("Failed to allocate pins for HUB75"); + return; + } + + if (bc.colorOrder == COL_ORDER_RGB) { + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = Default color order (RGB)"); + } else if (bc.colorOrder == COL_ORDER_BGR) { + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = color order BGR"); + int8_t tmpPin; + tmpPin = mxconfig.gpio.r1; + mxconfig.gpio.r1 = mxconfig.gpio.b1; + mxconfig.gpio.b1 = tmpPin; + tmpPin = mxconfig.gpio.r2; + mxconfig.gpio.r2 = mxconfig.gpio.b2; + mxconfig.gpio.b2 = tmpPin; + } + else { + DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA = unsupported color order %u\n", bc.colorOrder); + } + + DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA config - %ux%u length: %u\n", mxconfig.mx_width, mxconfig.mx_height, mxconfig.chain_length); + DEBUGBUS_PRINTF("R1_PIN=%u, G1_PIN=%u, B1_PIN=%u, R2_PIN=%u, G2_PIN=%u, B2_PIN=%u, A_PIN=%u, B_PIN=%u, C_PIN=%u, D_PIN=%u, E_PIN=%u, LAT_PIN=%u, OE_PIN=%u, CLK_PIN=%u\n", + mxconfig.gpio.r1, mxconfig.gpio.g1, mxconfig.gpio.b1, mxconfig.gpio.r2, mxconfig.gpio.g2, mxconfig.gpio.b2, + mxconfig.gpio.a, mxconfig.gpio.b, mxconfig.gpio.c, mxconfig.gpio.d, mxconfig.gpio.e, mxconfig.gpio.lat, mxconfig.gpio.oe, mxconfig.gpio.clk); + + // OK, now we can create our matrix object + display = new MatrixPanel_I2S_DMA(mxconfig); + if (display == nullptr) { + DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! driver allocation failed ***********"); + DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); + return; + } + + this->_len = (display->width() * display->height()); // note: this returns correct number of pixels but incorrect dimensions if using virtual display (updated below) + + DEBUGBUS_PRINTF("Length: %u\n", _len); + if (this->_len >= MAX_LEDS) { + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA Too many LEDS - playing safe"); + return; + } + + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA created"); + + // as noted in HUB75_I2S_DMA library, some panels can show ghosting if set higher than 239, so let users override at compile time + #ifndef WLED_HUB75_MAX_BRIGHTNESS + #define WLED_HUB75_MAX_BRIGHTNESS 255 + #endif + // let's adjust default brightness (128), brightness scaling is handled by WLED + //display->setBrightness8(WLED_HUB75_MAX_BRIGHTNESS); // range is 0-255, 0 - 0%, 255 - 100% + + delay(24); // experimental + DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); + // Allocate memory and start DMA display + if (!display->begin() ) { + DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********"); + DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); + return; + } + else { + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA begin ok"); + DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); + delay(18); // experiment - give the driver a moment (~ one full frame @ 60hz) to settle + _valid = true; + display->clearScreen(); // initially clear the screen buffer + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA clear ok"); + + if (_ledBuffer) d_free(_ledBuffer); // should not happen + if (_ledsDirty) d_free(_ledsDirty); // should not happen + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory"); + _ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len)); // create LEDs dirty bits + DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory ok"); + + if (_ledsDirty == nullptr) { + display->stopDMAoutput(); + delete display; display = nullptr; + _valid = false; + DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA not started - not enough memory for dirty bits!")); + DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); + return; // fail is we cannot get memory for the buffer + } + setBitArray(_ledsDirty, _len, false); // reset dirty bits + + if (mxconfig.double_buff == false) { + // create LEDs buffer (initialized to BLACK), prefer DRAM if enough heap is available (faster in case global _pixels buffer is in PSRAM as not both will fit the cache) + _ledBuffer = static_cast(allocate_buffer(_len * sizeof(CRGB), BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR)); + } + } + + PANEL_CHAIN_TYPE chainType = CHAIN_NONE; // default for quarter-scan panels that do not use chaining + // chained panels with cols and rows define need the virtual display driver, so do quarter-scan panels + if (chainLength > 1 && (_rows > 1 || _cols > 1) || bc.type == TYPE_HUB75MATRIX_QS) { + _isVirtual = true; + chainType = CHAIN_BOTTOM_LEFT_UP; // TODO: is there any need to support other chaining types? + DEBUGBUS_PRINTF_P(PSTR("Using virtual matrix: %ux%u panels of %ux%u pixels\n"), _cols, _rows, mxconfig.mx_width, mxconfig.mx_height); + } + else { + _isVirtual = false; + } + + if (_isVirtual) { + virtualDisp = new VirtualMatrixPanel((*display), _rows, _cols, mxconfig.mx_width, mxconfig.mx_height, chainType); + virtualDisp->setRotation(0); + if (bc.type == TYPE_HUB75MATRIX_QS) { + switch(panelHeight) { + case 16: + virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH); + break; + case 32: + virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH); + break; + case 64: + virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH); + break; + default: + DEBUGBUS_PRINTLN("Unsupported height"); + cleanup(); + return; } - #else //ESP32 RMT uses double buffer, parallel I2S uses 8x buffer (3 times) - multiplier = PolyBus::isParallelI2S1Output() ? 24 : 2; - #endif + } + } + + if (_valid) { + _panelWidth = virtualDisp ? virtualDisp->width() : display->width(); // cache width - it will never change + } + + DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA ")); + DEBUGBUS_PRINTF_P(PSTR("%sstarted, width=%u, %u pixels.\n"), _valid? "":"not ", _panelWidth, _len); + + if (_ledBuffer != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS buffer enabled.")); + if (_ledsDirty != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled.")); + if ((_ledBuffer != nullptr) || (_ledsDirty != nullptr)) { + DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA LEDS buffer uses ")); + DEBUGBUS_PRINT((_ledBuffer? _len*sizeof(CRGB) :0) + (_ledsDirty? getBitArrayBytes(_len) :0)); + DEBUGBUS_PRINTLN(F(" bytes.")); } - return (len * multiplier + bc.doubleBuffer * (bc.count + bc.skipAmount)) * channels; } -uint32_t BusManager::memUsage(unsigned maxChannels, unsigned maxCount, unsigned minBuses) { - //ESP32 RMT uses double buffer, parallel I2S uses 8x buffer (3 times) - unsigned multiplier = PolyBus::isParallelI2S1Output() ? 3 : 2; - return (maxChannels * maxCount * minBuses * multiplier); +void IRAM_ATTR BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) { + if (!_valid) return; // note: no need to check pix >= _len as that is checked in containsPixel() + // if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT + + if (_ledBuffer) { + CRGB fastled_col = CRGB(c); + if (_ledBuffer[pix] != fastled_col) { + _ledBuffer[pix] = fastled_col; + setBitInArray(_ledsDirty, pix, true); // flag pixel as "dirty" + } + } + else { + if ((c == IS_BLACK) && (getBitFromArray(_ledsDirty, pix) == false)) return; // ignore black if pixel is already black + setBitInArray(_ledsDirty, pix, c != IS_BLACK); // dirty = true means "color is not BLACK" + + uint8_t r = R(c); + uint8_t g = G(c); + uint8_t b = B(c); + + if (virtualDisp != nullptr) { + int x = pix % _panelWidth; // TODO: check if using & and shift would be faster here, it limits to power-of-2 widths though + int y = pix / _panelWidth; + virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b); + } else { + int x = pix % _panelWidth; + int y = pix / _panelWidth; + display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b); + } + } +} + +uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const { + if (!_valid) return IS_BLACK; // note: no need to check pix >= _len as that is checked in containsPixel() + if (_ledBuffer) + return uint32_t(_ledBuffer[pix]); + else + return getBitFromArray(_ledsDirty, pix) ? IS_DARKGREY: IS_BLACK; // just a hack - we only know if the pixel is black or not +} + +void BusHub75Matrix::setBrightness(uint8_t b) { + _bri = b; + display->setBrightness(_bri); } -int BusManager::add(BusConfig &bc) { - if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; - if (Bus::isVirtual(bc.type)) { - busses[numBusses] = new BusNetwork(bc); +void BusHub75Matrix::show(void) { + if (!_valid) return; + if (_ledBuffer) { + // write out buffered LEDs + unsigned height = _isVirtual ? virtualDisp->height() : display->height(); + unsigned width = _panelWidth; + + //while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker. + size_t pix = 0; // running pixel index + for (int y=0; ydrawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b); + else display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b); + } + pix++; + } + setBitArray(_ledsDirty, _len, false); // buffer shown - reset all dirty bits + } +} + +void BusHub75Matrix::cleanup() { + if (display && _valid) display->stopDMAoutput(); // terminate DMA driver (display goes black) + _valid = false; + delay(30); // give some time to finish DMA + _panelWidth = 0; + deallocatePins(); + DEBUGBUS_PRINTLN(F("HUB75 output ended.")); + #ifndef CONFIG_IDF_TARGET_ESP32S3 // on ESP32-S3 deleting display/virtualDisp does not work and leads to crash (DMA issues), request reboot from user instead + if (virtualDisp != nullptr) delete virtualDisp; // note: in MM there is a warning to not do this but if using "NO_GFX" this is safe + if (display != nullptr) delete display; + display = nullptr; + virtualDisp = nullptr; // note: when not using "NO_GFX" this causes a memory leak + #endif + if (_ledBuffer != nullptr) d_free(_ledBuffer); _ledBuffer = nullptr; + if (_ledsDirty != nullptr) d_free(_ledsDirty); _ledsDirty = nullptr; +} + +void BusHub75Matrix::deallocatePins() { + uint8_t pins[PIN_COUNT]; + memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio)); + PinManager::deallocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75); +} + +std::vector BusHub75Matrix::getLEDTypes() { + return { + {TYPE_HUB75MATRIX_HS, "H", PSTR("HUB75 (Half Scan)")}, + {TYPE_HUB75MATRIX_QS, "H", PSTR("HUB75 (Quarter Scan)")}, + }; +} + +size_t BusHub75Matrix::getPins(uint8_t* pinArray) const { + if (pinArray) { + pinArray[0] = mxconfig.mx_width; + pinArray[1] = mxconfig.mx_height; + pinArray[2] = mxconfig.chain_length; + pinArray[3] = _rows; + pinArray[4] = _cols; + } + return 5; +} + +#endif +// *************************************************************************** + +BusPlaceholder::BusPlaceholder(const BusConfig &bc) +: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, bc.refreshReq) +, _colorOrder(bc.colorOrder) +, _skipAmount(bc.skipAmount) +, _driverType(bc.driverType) +, _frequency(bc.frequency) +, _milliAmpsPerLed(bc.milliAmpsPerLed) +, _milliAmpsMax(bc.milliAmpsMax) +, _text(bc.text) +{ + memcpy(_pins, bc.pins, sizeof(_pins)); +} + +size_t BusPlaceholder::getPins(uint8_t* pinArray) const { + size_t nPins = Bus::getNumberOfPins(_type); + if (pinArray) { + for (size_t i = 0; i < nPins; i++) pinArray[i] = _pins[i]; + } + return nPins; +} + +//utility to get the approx. memory usage of a given BusConfig inclduding segmentbuffer and global buffer (4 bytes per pixel) +size_t BusConfig::memUsage() const { + size_t mem = (count + skipAmount) * 8; // 8 bytes per pixel for segment + global buffer + if (Bus::isVirtual(type)) { + mem += sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type)); // note: getNumberOfChannels() includes CCT channel if applicable but virtual buses do not use CCT channel buffer + } else if (Bus::isDigital(type)) { + // if any of digital buses uses I2S, there is additional common I2S DMA buffer not accounted for here + mem += sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, iType); + } else if (Bus::isOnOff(type)) { + mem += sizeof(BusOnOff); + } else { + mem += sizeof(BusPwm); + } + return mem; +} + +int BusManager::add(const BusConfig &bc, bool placeholder) { + DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (p:%d v:%d)\n"), getNumBusses(), getNumVirtualBusses()); + unsigned digital = 0; + unsigned analog = 0; + unsigned twoPin = 0; + for (const auto &bus : busses) { + if (bus->isPWM()) analog += bus->getPins(); // number of analog channels used + if (bus->isDigital() && !bus->is2Pin()) digital++; + if (bus->is2Pin()) twoPin++; + } + digital += (Bus::isDigital(bc.type) && !Bus::is2Pin(bc.type)); + analog += (Bus::isPWM(bc.type) ? Bus::numPWMPins(bc.type) : 0); + if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true; // TODO: add errorFlag here + if (placeholder) { + busses.push_back(make_unique(bc)); + } else if (Bus::isVirtual(bc.type)) { + busses.push_back(make_unique(bc)); +#ifdef WLED_ENABLE_HUB75MATRIX + } else if (Bus::isHub75(bc.type)) { + busses.push_back(make_unique(bc)); +#endif } else if (Bus::isDigital(bc.type)) { - busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap); + busses.push_back(make_unique(bc)); } else if (Bus::isOnOff(bc.type)) { - busses[numBusses] = new BusOnOff(bc); + busses.push_back(make_unique(bc)); } else { - busses[numBusses] = new BusPwm(bc); + busses.push_back(make_unique(bc)); } - return numBusses++; + return busses.size(); } // credit @willmmiles @@ -838,7 +1218,7 @@ static String LEDTypesToJson(const std::vector& types) { return json; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 String BusManager::getLEDTypesJSONString() { String json = "["; json += LEDTypesToJson(BusDigital::getLEDTypes()); @@ -846,49 +1226,43 @@ String BusManager::getLEDTypesJSONString() { json += LEDTypesToJson(BusPwm::getLEDTypes()); json += LEDTypesToJson(BusNetwork::getLEDTypes()); //json += LEDTypesToJson(BusVirtual::getLEDTypes()); + #ifdef WLED_ENABLE_HUB75MATRIX + json += LEDTypesToJson(BusHub75Matrix::getLEDTypes()); + #endif + json.setCharAt(json.length()-1, ']'); // replace last comma with bracket return json; } -void BusManager::useParallelOutput() { - _parallelOutputs = 8; // hardcoded since we use NPB I2S x8 methods - PolyBus::setParallelI2S1Output(); +uint8_t BusManager::getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) { + return PolyBus::getI(busType, pins, driverPreference); } - //do not call this method from system context (network callback) void BusManager::removeAll() { - DEBUG_PRINTLN(F("Removing all.")); + DEBUGBUS_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); - for (unsigned i = 0; i < numBusses; i++) delete busses[i]; - numBusses = 0; - _parallelOutputs = 1; - PolyBus::setParallelI2S1Output(false); + busses.clear(); + #ifndef ESP8266 + // Reset channel tracking for fresh allocation + PolyBus::resetChannelTracking(); + #endif } #ifdef ESP32_DATA_IDLE_HIGH // #2478 // If enabled, RMT idle level is set to HIGH when off // to prevent leakage current when using an N-channel MOSFET to toggle LED power +// since I2S outputs are known only during config of buses, lets just assume RMT is used for digital buses +// unused RMT channels should have no effect void BusManager::esp32RMTInvertIdle() { bool idle_out; unsigned rmt = 0; - for (unsigned u = 0; u < numBusses(); u++) { - #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, only has 1 I2S but NPB does not support it ATM - if (u > 1) return; - rmt = u; - #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, only has 1 I2S bus, supported in NPB - if (u > 3) return; - rmt = u; - #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, has 2 I2S but NPB does not support them ATM - if (u > 3) return; - rmt = u; - #else - if (u < _parallelOutputs) continue; - if (u >= _parallelOutputs + 8) return; // only 8 RMT channels - rmt = u - _parallelOutputs; - #endif - if (busses[u]->getLength()==0 || !busses[u]->isDigital() || busses[u]->is2Pin()) continue; + unsigned u = 0; + for (auto &bus : busses) { + if (bus->getLength()==0 || !bus->isDigital() || bus->is2Pin()) continue; + if (static_cast(bus.get())->isI2S()) continue; + if (u >= WLED_MAX_RMT_CHANNELS) return; //assumes that bus number to rmt channel mapping stays 1:1 rmt_channel_t ch = static_cast(rmt); rmt_idle_level_t lvl; @@ -897,6 +1271,7 @@ void BusManager::esp32RMTInvertIdle() { else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH; else continue; rmt_set_idle_level(ch, idle_out, lvl); + u++; } } #endif @@ -905,17 +1280,24 @@ void BusManager::on() { #ifdef ESP8266 //Fix for turning off onboard LED breaking bus if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { - for (unsigned i = 0; i < numBusses; i++) { + for (auto &bus : busses) { uint8_t pins[2] = {255,255}; - if (busses[i]->isDigital() && busses[i]->getPins(pins)) { + if (bus->isDigital() && bus->getPins(pins) && bus->isOk()) { if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) { - BusDigital *bus = static_cast(busses[i]); - bus->begin(); + BusDigital &b = static_cast(*bus); + b.begin(); break; } } } } + #else + for (auto &bus : busses) if (bus->isVirtual()) { + // virtual/network bus should check for IP change if hostname is specified + // otherwise there are no endpoints to force DNS resolution + BusNetwork &b = static_cast(*bus); + b.resolveHostname(); + } #endif #ifdef ESP32_DATA_IDLE_HIGH esp32RMTInvertIdle(); @@ -927,7 +1309,7 @@ void BusManager::off() { // turn off built-in LED if strip is turned off // this will break digital bus so will need to be re-initialised on On if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { - for (unsigned i = 0; i < numBusses; i++) if (busses[i]->isOffRefreshRequired()) return; + for (const auto &bus : busses) if (bus->isOffRefreshRequired()) return; pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); } @@ -935,34 +1317,20 @@ void BusManager::off() { #ifdef ESP32_DATA_IDLE_HIGH esp32RMTInvertIdle(); #endif + _gMilliAmpsUsed = 0; // reset, assume no LED idle current if relay is off } void BusManager::show() { - _milliAmpsUsed = 0; - for (unsigned i = 0; i < numBusses; i++) { - busses[i]->show(); - _milliAmpsUsed += busses[i]->getUsedCurrent(); + applyABL(); // apply brightness limit, updates _gMilliAmpsUsed + for (auto &bus : busses) { + bus->show(); } - if (_milliAmpsUsed) _milliAmpsUsed += MA_FOR_ESP; } -void BusManager::setStatusPixel(uint32_t c) { - for (unsigned i = 0; i < numBusses; i++) { - busses[i]->setStatusPixel(c); - } -} - -void IRAM_ATTR BusManager::setPixelColor(uint16_t pix, uint32_t c) { - for (unsigned i = 0; i < numBusses; i++) { - unsigned bstart = busses[i]->getStart(); - if (pix < bstart || pix >= bstart + busses[i]->getLength()) continue; - busses[i]->setPixelColor(pix - bstart, c); - } -} - -void BusManager::setBrightness(uint8_t b) { - for (unsigned i = 0; i < numBusses; i++) { - busses[i]->setBrightness(b); +void IRAM_ATTR BusManager::setPixelColor(unsigned pix, uint32_t c) { + for (auto &bus : busses) { + if (!bus->containsPixel(pix)) continue; + bus->setPixelColor(pix - bus->getStart(), c); } } @@ -975,46 +1343,117 @@ void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { Bus::setCCT(cct); } -uint32_t BusManager::getPixelColor(uint16_t pix) { - for (unsigned i = 0; i < numBusses; i++) { - unsigned bstart = busses[i]->getStart(); - if (!busses[i]->containsPixel(pix)) continue; - return busses[i]->getPixelColor(pix - bstart); +uint32_t BusManager::getPixelColor(unsigned pix) { + for (auto &bus : busses) { + if (!bus->containsPixel(pix)) continue; + return bus->getPixelColor(pix - bus->getStart()); } return 0; } bool BusManager::canAllShow() { - for (unsigned i = 0; i < numBusses; i++) { - if (!busses[i]->canShow()) return false; - } + for (const auto &bus : busses) if (!bus->canShow()) return false; return true; } -Bus* BusManager::getBus(uint8_t busNr) { - if (busNr >= numBusses) return nullptr; - return busses[busNr]; +void BusManager::initializeABL() { + _useABL = false; // reset + if (_gMilliAmpsMax > 0) { + // check global brightness limit + for (auto &bus : busses) { + if (bus->isDigital() && bus->getLEDCurrent() > 0) { + _useABL = true; // at least one bus has valid LED current + return; + } + } + } else { + // check per bus brightness limit + unsigned numABLbuses = 0; + for (auto &bus : busses) { + if (bus->isDigital() && bus->getLEDCurrent() > 0 && bus->getMaxCurrent() > 0) + numABLbuses++; // count ABL enabled buses + } + if (numABLbuses > 0) { + _useABL = true; // at least one bus has ABL set + uint32_t ESPshare = MA_FOR_ESP / numABLbuses; // share of ESP current per ABL bus + for (auto &bus : busses) { + if (bus->isDigital() && bus->isOk()) { + BusDigital &busd = static_cast(*bus); + uint32_t busLength = busd.getLength(); + uint32_t busDemand = busLength * busd.getLEDCurrent(); + uint32_t busMax = busd.getMaxCurrent(); + if (busMax > ESPshare) busMax -= ESPshare; + if (busMax < busLength) busMax = busLength; // give each LED 1mA, ABL will dim down to minimum + if (busDemand == 0) busMax = 0; // no LED current set, disable ABL for this bus + busd.setCurrentLimit(busMax); + } + } + } + } } -//semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) -uint16_t BusManager::getTotalLength() { - unsigned len = 0; - for (unsigned i=0; igetLength(); - return len; +void BusManager::applyABL() { + if (_useABL) { + unsigned milliAmpsSum = 0; // use temporary variable to always return a valid _gMilliAmpsUsed to UI + unsigned totalLEDs = 0; + for (auto &bus : busses) { + if (bus->isDigital() && bus->isOk()) { + BusDigital &busd = static_cast(*bus); + busd.estimateCurrent(); // sets _milliAmpsTotal, current is estimated for all buses even if they have the limit set to 0 + if (_gMilliAmpsMax == 0) + busd.applyBriLimit(0); // apply per bus ABL limit, updates _milliAmpsTotal if limit reached + milliAmpsSum += busd.getUsedCurrent(); + totalLEDs += busd.getLength(); // sum total number of LEDs for global Limit + } + } + // check global current limit and apply global ABL limit, total current is summed above + if (_gMilliAmpsMax > 0) { + uint8_t newBri = 255; + uint32_t globalMax = _gMilliAmpsMax > MA_FOR_ESP ? _gMilliAmpsMax - MA_FOR_ESP : 1; // subtract ESP current consumption, fully limit if too low + if (globalMax > totalLEDs) { // check if budget is larger than standby current + if (milliAmpsSum > globalMax) { + newBri = globalMax * 255 / milliAmpsSum + 1; // scale brightness down to stay in current limit, +1 to avoid 0 brightness + milliAmpsSum = globalMax; // update total used current + } + } else { + newBri = 1; // limit too low, set brightness to minimum + milliAmpsSum = totalLEDs; // estimate total used current as minimum + } + + // apply brightness limit to each bus, if its 255 it will only reset _colorSum + for (auto &bus : busses) { + if (bus->isDigital() && bus->isOk()) { + BusDigital &busd = static_cast(*bus); + if (busd.getLEDCurrent() > 0) // skip buses with LED current set to 0 + busd.applyBriLimit(newBri); + } + } + } + _gMilliAmpsUsed = milliAmpsSum; + } + else + _gMilliAmpsUsed = 0; // reset, we have no current estimation without ABL } -bool PolyBus::useParallelI2S = false; +ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } +#ifndef ESP8266 +// PolyBus channel tracking for dynamic allocation +bool PolyBus::_useParallelI2S = false; +uint8_t PolyBus::_rmtChannelsAssigned = 0; // number of RMT channels assigned durig getI() check +uint8_t PolyBus::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create() +uint8_t PolyBus::_i2sChannelsAssigned = 0; // number of I2S channels assigned durig getI() check +uint8_t PolyBus::_parallelBusItype = 0; // type I_NONE +uint8_t PolyBus::_2PchannelsAssigned = 0; +#endif // Bus static member definition -int16_t Bus::_cct = -1; -uint8_t Bus::_cctBlend = 0; +int16_t Bus::_cct = -1; // -1 means use approximateKelvinFromRGB(), 0-255 is standard, >1900 use colorBalanceFromKelvin() +int8_t Bus::_cctBlend = 0; // -128 to +127 uint8_t Bus::_gAWM = 255; uint16_t BusDigital::_milliAmpsTotal = 0; -uint8_t BusManager::numBusses = 0; -Bus* BusManager::busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; -ColorOrderMap BusManager::colorOrderMap = {}; -uint16_t BusManager::_milliAmpsUsed = 0; -uint16_t BusManager::_milliAmpsMax = ABL_MILLIAMPS_DEFAULT; -uint8_t BusManager::_parallelOutputs = 1; +std::vector> BusManager::busses; +uint16_t BusManager::_gMilliAmpsUsed = 0; +uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; +bool BusManager::_useABL = false; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index ecebc120e8..d5ac31a670 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -1,12 +1,57 @@ +#pragma once #ifndef BusManager_h #define BusManager_h +#ifdef WLED_ENABLE_HUB75MATRIX + +#include +#include +#include + +#endif /* * Class for addressing various light types */ #include "const.h" +#include "pin_manager.h" #include +#include + +#if __cplusplus >= 201402L +using std::make_unique; +#else +// Really simple C++11 shim for non-array case; implementation from cppreference.com +template +std::unique_ptr +make_unique(Args&&... args) +{ + return std::unique_ptr(new T(std::forward(args)...)); +} +#endif + +// enable additional debug output +#if defined(WLED_DEBUG_HOST) + #include "net_debug.h" + #define DEBUGOUT NetDebug +#else + #define DEBUGOUT Serial +#endif + +#ifdef WLED_DEBUG_BUS + #ifndef ESP8266 + #include + #endif + #define DEBUGBUS_PRINT(x) DEBUGOUT.print(x) + #define DEBUGBUS_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUGBUS_PRINTF(x...) DEBUGOUT.printf(x) + #define DEBUGBUS_PRINTF_P(x...) DEBUGOUT.printf_P(x) +#else + #define DEBUGBUS_PRINT(x) + #define DEBUGBUS_PRINTLN(x) + #define DEBUGBUS_PRINTF(x...) + #define DEBUGBUS_PRINTF_P(x...) +#endif //colors.cpp uint16_t approximateKelvinFromRGB(uint32_t rgb); @@ -67,60 +112,64 @@ class Bus { Bus(uint8_t type, uint16_t start, uint8_t aw, uint16_t len = 1, bool reversed = false, bool refresh = false) : _type(type) , _bri(255) + , _NPBbri(255) , _start(start) - , _len(len) + , _len(std::max(len,(uint16_t)1)) , _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 begin() {}; - virtual void show() = 0; - virtual bool canShow() const { return true; } - virtual void setStatusPixel(uint32_t c) {} - virtual void setPixelColor(uint16_t pix, uint32_t c) = 0; - virtual void setBrightness(uint8_t b) { _bri = b; }; - virtual void setColorOrder(uint8_t co) {} - virtual uint32_t getPixelColor(uint16_t pix) const { return 0; } - virtual uint8_t getPins(uint8_t* pinArray = nullptr) const { return 0; } - virtual uint16_t getLength() const { return isOk() ? _len : 0; } - virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } - virtual uint8_t skippedLeds() const { return 0; } - virtual uint16_t getFrequency() const { return 0U; } - virtual uint16_t getLEDCurrent() const { return 0; } - virtual uint16_t getUsedCurrent() const { return 0; } - virtual uint16_t getMaxCurrent() const { return 0; } - - inline bool hasRGB() const { return _hasRgb; } - inline bool hasWhite() const { return _hasWhite; } - inline bool hasCCT() const { return _hasCCT; } - inline bool isDigital() const { return isDigital(_type); } - inline bool is2Pin() const { return is2Pin(_type); } - inline bool isOnOff() const { return isOnOff(_type); } - inline bool isPWM() const { return isPWM(_type); } - inline bool isVirtual() const { return isVirtual(_type); } - inline bool is16bit() const { return is16bit(_type); } - inline bool mustRefresh() const { return mustRefresh(_type); } - inline void setReversed(bool reversed) { _reversed = reversed; } - inline void setStart(uint16_t start) { _start = start; } - inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } - inline uint8_t getAutoWhiteMode() const { return _autoWhiteMode; } - inline uint8_t getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } - inline uint16_t getStart() const { return _start; } - inline uint8_t getType() const { return _type; } - inline bool isOk() const { return _valid; } - inline bool isReversed() const { return _reversed; } - inline bool isOffRefreshRequired() const { return _needsRefresh; } - inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } - - static inline std::vector getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes - static constexpr uint8_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK - static constexpr uint8_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } + virtual void begin() {}; + virtual void show() = 0; + virtual bool canShow() const { return true; } + virtual void setStatusPixel(uint32_t c) {} + virtual void setPixelColor(unsigned pix, uint32_t c) = 0; + virtual void setBrightness(uint8_t b) { _bri = b; }; + virtual void setColorOrder(uint8_t co) {} + virtual uint32_t getPixelColor(unsigned pix) const { return 0; } + virtual size_t getPins(uint8_t* pinArray = nullptr) const { return 0; } + virtual uint16_t getLength() const { return _len; } + virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } + virtual unsigned skippedLeds() const { return 0; } + virtual uint16_t getFrequency() const { return 0U; } + virtual uint16_t getLEDCurrent() const { return 0; } + virtual uint16_t getUsedCurrent() const { return 0; } + virtual uint16_t getMaxCurrent() const { return 0; } + virtual uint8_t getDriverType() const { return 0; } // Default to RMT (0) for non-digital buses + virtual size_t getBusSize() const { return sizeof(Bus); } // currently unused + virtual const String getCustomText() const { return String(); } + + inline bool hasRGB() const { return _hasRgb; } + inline bool hasWhite() const { return _hasWhite; } + inline bool hasCCT() const { return _hasCCT; } + inline bool isDigital() const { return isDigital(_type); } + inline bool is2Pin() const { return is2Pin(_type); } + inline bool isOnOff() const { return isOnOff(_type); } + inline bool isPWM() const { return isPWM(_type); } + inline bool isVirtual() const { return isVirtual(_type); } + inline bool is16bit() const { return is16bit(_type); } + virtual bool isPlaceholder() const { return false; } + inline bool mustRefresh() const { return mustRefresh(_type); } + inline void setReversed(bool reversed) { _reversed = reversed; } + inline void setStart(uint16_t start) { _start = start; } + inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } + inline uint8_t getAutoWhiteMode() const { return _autoWhiteMode; } + inline size_t getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } + inline uint16_t getStart() const { return _start; } + inline uint8_t getType() const { return _type; } + inline bool isOk() const { return _valid; } + inline bool isReversed() const { return _reversed; } + inline bool isOffRefreshRequired() const { return _needsRefresh; } + inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } + + static inline std::vector getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes + static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK + static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } static constexpr bool hasRGB(uint8_t type) { return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); } @@ -143,6 +192,7 @@ class Bus { static constexpr bool isOnOff(uint8_t type) { return (type == TYPE_ONOFF); } static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); } static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); } + static constexpr bool isHub75(uint8_t type) { return (type >= TYPE_HUB75MATRIX_MIN && type <= TYPE_HUB75MATRIX_MAX); } static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814; } static constexpr int numPWMPins(uint8_t type) { return (type - 40); } @@ -151,9 +201,9 @@ class Bus { static inline void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } static inline uint8_t getGlobalAWMode() { return _gAWM; } static inline void setCCT(int16_t cct) { _cct = cct; } - static inline uint8_t getCCTBlend() { return _cctBlend; } - static inline void setCCTBlend(uint8_t b) { - _cctBlend = (std::min((int)b,100) * 127) / 100; + static inline int8_t getCCTBlend() { return (_cctBlend * 100 + (_cctBlend >= 0 ? 64 : -64)) / 127; } // returns -100 to +100, +/-100% = +/-127. +/-64 for rounding + static inline void setCCTBlend(int8_t b) { // input is -100 to +100 + _cctBlend = (std::max(-100, std::min(100, (int)b)) * 127 + (b >= 0 ? 50 : -50)) / 100; // +/-50 for rounding, b=+/-100% -> +/-127 //compile-time limiter for hardware that can't power both white channels at max #ifdef WLED_MAX_CCT_BLEND if (_cctBlend > WLED_MAX_CCT_BLEND) _cctBlend = WLED_MAX_CCT_BLEND; @@ -163,7 +213,9 @@ class Bus { protected: uint8_t _type; - uint8_t _bri; + uint8_t _bri; // bus brightness + uint8_t _NPBbri; // total brightness applied to colors in NPB buffer (_bri + ABL) + uint8_t _autoWhiteMode; // global Auto White Calculation override uint16_t _start; uint16_t _len; //struct { //using bitfield struct adds abour 250 bytes to binary size @@ -174,61 +226,64 @@ class Bus { bool _hasWhite;// : 1; bool _hasCCT;// : 1; //} __attribute__ ((packed)); - uint8_t _autoWhiteMode; - uint8_t *_data; - // global Auto White Calculation override static uint8_t _gAWM; - // _cct has the following menaings (see calculateCCT() & BusManager::setSegmentCCT()): + // _cct has the following meanings (see calculateCCT() & BusManager::setSegmentCCT()): // -1 means to extract approximate CCT value in K from RGB (in calcualteCCT()) // [0,255] is the exact CCT value where 0 means warm and 255 cold // [1900,10060] only for color correction expressed in K (colorBalanceFromKelvin()) static int16_t _cct; - // _cctBlend determines WW/CW blending: + // _cctBlend determines WW/CW blending, see calculateCCT() + // < 0 - linear blending in center, single white at both ends, single white zone extends with decreased value (-127 min) // 0 - linear (CCT 127 => 50% warm, 50% cold) // 63 - semi additive/nonlinear (CCT 127 => 66% warm, 66% cold) // 127 - additive CCT blending (CCT 127 => 100% warm, 100% cold) - static uint8_t _cctBlend; + static int8_t _cctBlend; - uint32_t autoWhiteCalc(uint32_t c) const; - uint8_t *allocateData(size_t size = 1); - void freeData() { if (_data != nullptr) free(_data); _data = nullptr; } + uint32_t autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const; }; class BusDigital : public Bus { public: - BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com); + BusDigital(const BusConfig &bc); ~BusDigital() { cleanup(); } void show() override; bool canShow() const override; - void setBrightness(uint8_t b) override; void setStatusPixel(uint32_t c) override; - [[gnu::hot]] void setPixelColor(uint16_t pix, uint32_t c) override; + [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; - [[gnu::hot]] uint32_t getPixelColor(uint16_t pix) const override; + [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; uint8_t getColorOrder() const override { return _colorOrder; } - uint8_t getPins(uint8_t* pinArray = nullptr) const override; - uint8_t skippedLeds() const override { return _skip; } + size_t getPins(uint8_t* pinArray = nullptr) const override; + unsigned skippedLeds() const override { return _skip; } uint16_t getFrequency() const override { return _frequencykHz; } uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } uint16_t getUsedCurrent() const override { return _milliAmpsTotal; } uint16_t getMaxCurrent() const override { return _milliAmpsMax; } + uint8_t getDriverType() const override { return _driverType; } + void setCurrentLimit(uint16_t milliAmps) { _milliAmpsLimit = milliAmps; } + void estimateCurrent(); // estimate used current from summed colors + void applyBriLimit(uint8_t newBri); + size_t getBusSize() const override; + bool isI2S(); // true if this bus uses I2S driver void begin() override; void cleanup(); static std::vector getLEDTypes(); private: - uint8_t _skip; - uint8_t _colorOrder; - uint8_t _pins[2]; - uint8_t _iType; + uint8_t _skip; + uint8_t _colorOrder; + uint8_t _pins[2]; + uint8_t _iType; + uint8_t _driverType; // 0=RMT (default), 1=I2S uint16_t _frequencykHz; - uint8_t _milliAmpsPerLed; uint16_t _milliAmpsMax; - void * _busPtr; - const ColorOrderMap &_colorOrderMap; + uint8_t _milliAmpsPerLed; + uint16_t _milliAmpsLimit; + uint32_t _colorSum; // total color value for the bus, updated in setPixelColor(), used to estimate current + void *_busPtr; static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() @@ -242,28 +297,27 @@ class BusDigital : public Bus { } return c; } - - uint8_t estimateCurrentAndLimitBri(); }; class BusPwm : public Bus { public: - BusPwm(BusConfig &bc); + BusPwm(const BusConfig &bc); ~BusPwm() { cleanup(); } - void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) const override; //does no index check - uint8_t getPins(uint8_t* pinArray = nullptr) const override; + void setPixelColor(unsigned pix, uint32_t c) override; + uint32_t getPixelColor(unsigned pix) const override; //does no index check + size_t getPins(uint8_t* pinArray = nullptr) const override; uint16_t getFrequency() const override { return _frequency; } + size_t getBusSize() const override { return sizeof(BusPwm); } void show() override; - void cleanup() { deallocatePins(); } + inline void cleanup() { deallocatePins(); } static std::vector getLEDTypes(); private: uint8_t _pins[OUTPUT_MAX_PINS]; - uint8_t _pwmdata[OUTPUT_MAX_PINS]; + uint8_t _data[OUTPUT_MAX_PINS]; #ifdef ARDUINO_ARCH_ESP32 uint8_t _ledcStart; #endif @@ -276,34 +330,40 @@ class BusPwm : public Bus { class BusOnOff : public Bus { public: - BusOnOff(BusConfig &bc); + BusOnOff(const BusConfig &bc); ~BusOnOff() { cleanup(); } - void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) const override; - uint8_t getPins(uint8_t* pinArray) const override; + void setPixelColor(unsigned pix, uint32_t c) override; + uint32_t getPixelColor(unsigned pix) const override; + size_t getPins(uint8_t* pinArray) const override; + size_t getBusSize() const override { return sizeof(BusOnOff); } void show() override; - void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } + inline void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } static std::vector getLEDTypes(); private: uint8_t _pin; - uint8_t _onoffdata; + uint8_t _data; }; class BusNetwork : public Bus { public: - BusNetwork(BusConfig &bc); + BusNetwork(const BusConfig &bc); ~BusNetwork() { cleanup(); } bool canShow() const override { 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) override; - uint32_t getPixelColor(uint16_t pix) const override; - uint8_t getPins(uint8_t* pinArray = nullptr) const override; - void show() override; - void cleanup(); + [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; + [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; + size_t getPins(uint8_t* pinArray = nullptr) const override; + size_t getBusSize() const override { return sizeof(BusNetwork) + (isOk() ? _len * _UDPchannels : 0); } + void show() override; + void cleanup(); + #ifdef ARDUINO_ARCH_ESP32 + void resolveHostname(); + const String getCustomText() const override { return _hostname; } + #endif static std::vector getLEDTypes(); @@ -312,8 +372,81 @@ class BusNetwork : public Bus { uint8_t _UDPtype; uint8_t _UDPchannels; bool _broadcastLock; + uint8_t *_data; + #ifdef ARDUINO_ARCH_ESP32 + String _hostname; + #endif }; +// Placeholder for buses that we can't construct due to resource limitations +// This preserves the configuration so it can be read back to the settings pages +// Function calls "mimic" the replaced bus, isPlaceholder() can be used to identify a placeholder +class BusPlaceholder : public Bus { + public: + BusPlaceholder(const BusConfig &bc); + + // Actual calls are stubbed out + void setPixelColor(unsigned pix, uint32_t c) override {}; + void show() override {}; + + // Accessors + uint8_t getColorOrder() const override { return _colorOrder; } + size_t getPins(uint8_t* pinArray) const override; + unsigned skippedLeds() const override { return _skipAmount; } + uint16_t getFrequency() const override { return _frequency; } + uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } + uint16_t getMaxCurrent() const override { return _milliAmpsMax; } + uint8_t getDriverType() const override { return _driverType; } + const String getCustomText() const override { return _text; } + bool isPlaceholder() const override { return true; } + + size_t getBusSize() const override { return sizeof(BusPlaceholder); } + + private: + uint8_t _colorOrder; + uint8_t _skipAmount; + uint8_t _pins[OUTPUT_MAX_PINS]; + uint8_t _driverType; + uint16_t _frequency; + uint8_t _milliAmpsPerLed; + uint16_t _milliAmpsMax; + String _text; +}; + +#ifdef WLED_ENABLE_HUB75MATRIX +class BusHub75Matrix : public Bus { + public: + BusHub75Matrix(const BusConfig &bc); + [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; + [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; + void show() override; + void setBrightness(uint8_t b) override; + size_t getPins(uint8_t* pinArray = nullptr) const override; + void deallocatePins(); + void cleanup(); + + ~BusHub75Matrix() { + cleanup(); + } + + static std::vector getLEDTypes(void); + + private: + MatrixPanel_I2S_DMA *display = nullptr; + VirtualMatrixPanel *virtualDisp = nullptr; + HUB75_I2S_CFG mxconfig; + unsigned _panelWidth = 0; + uint8_t _rows = 1; // panels per row + uint8_t _cols = 1; // panels per column + bool _isVirtual = false; // note: this is not strictly needed but there are padding bytes here anyway + CRGB *_ledBuffer = nullptr; // note: using uint32_t buffer is only 2% faster and not worth the extra RAM + byte *_ledsDirty = nullptr; + // workaround for missing constants on include path for non-MM + static constexpr uint32_t IS_BLACK = 0x000000u; + static constexpr uint32_t IS_DARKGREY = 0x333333u; + static constexpr int PIN_COUNT = 14; +}; +#endif //temporary struct for passing bus configuration to bus struct BusConfig { @@ -325,34 +458,49 @@ struct BusConfig { uint8_t skipAmount; bool refreshReq; uint8_t autoWhite; - uint8_t pins[5] = {255, 255, 255, 255, 255}; + uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; uint16_t frequency; - bool doubleBuffer; uint8_t milliAmpsPerLed; uint16_t milliAmpsMax; + uint8_t driverType; // 0=RMT (default), 1=I2S + uint8_t iType; // internal bus type (I_*) determined during memory estimation, used for bus creation + String text; - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, bool dblBfr=false, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) - : count(len) + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "") + : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) , reversed(rev) , skipAmount(skip) , autoWhite(aw) , frequency(clock_kHz) - , doubleBuffer(dblBfr) , milliAmpsPerLed(maPerLed) , milliAmpsMax(maMax) + , driverType(driver) + , iType(0) // default to I_NONE + , text(sometext) { refreshReq = (bool) GET_BIT(busType,7); type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) size_t nPins = Bus::getNumberOfPins(type); for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; + DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d, driver:%s)\n"), + (int)start, (int)(start+len), + (int)type, + (int)colorOrder, + (int)reversed, + (int)skipAmount, + (int)autoWhite, + (int)frequency, + (int)milliAmpsPerLed, (int)milliAmpsMax, + driverType == 0 ? "RMT" : "I2S" + ); } //validates start and length and extends total if needed bool adjustBounds(uint16_t& total) { if (!count) count = 1; - if (count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS; + if (!Bus::isVirtual(type) && count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS; if (start >= MAX_LEDS) return false; //limit length of strip if it would exceed total permissible LEDs if (start + count > MAX_LEDS) count = MAX_LEDS - start; @@ -360,64 +508,74 @@ struct BusConfig { if (start + count > total) total = start + count; return true; } -}; - - -class BusManager { - public: - BusManager() {}; - - //utility to get the approx. memory usage of a given BusConfig - static uint32_t memUsage(BusConfig &bc); - static uint32_t memUsage(unsigned channels, unsigned count, unsigned buses = 1); - static uint16_t currentMilliamps() { return _milliAmpsUsed; } - static uint16_t ablMilliampsMax() { return _milliAmpsMax; } - - static int add(BusConfig &bc); - static void useParallelOutput(); // workaround for inaccessible PolyBus - - //do not call this method from system context (network callback) - static void removeAll(); - - static void on(); - static void off(); - static void show(); - static bool canAllShow(); - static void setStatusPixel(uint32_t c); - [[gnu::hot]] static void setPixelColor(uint16_t pix, uint32_t c); - static void setBrightness(uint8_t b); - // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K - // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() - static void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); - static inline void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} - static uint32_t getPixelColor(uint16_t pix); - static inline int16_t getSegmentCCT() { return Bus::getCCT(); } + size_t memUsage() const; +}; - static Bus* getBus(uint8_t busNr); - //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) - static uint16_t getTotalLength(); - static inline uint8_t getNumBusses() { return numBusses; } - static String getLEDTypesJSONString(); +// milliamps used by ESP (for power estimation) +// you can set it to 0 if the ESP is powered by USB and the LEDs by external +#ifndef MA_FOR_ESP + #ifdef ESP8266 + #define MA_FOR_ESP 80 //how much mA does the ESP use (Wemos D1 about 80mA) + #else + #define MA_FOR_ESP 120 //how much mA does the ESP use (ESP32 about 120mA) + #endif +#endif - static inline ColorOrderMap& getColorOrderMap() { return colorOrderMap; } +namespace BusManager { + + extern std::vector> busses; + //extern std::vector busses; + extern uint16_t _gMilliAmpsUsed; + extern uint16_t _gMilliAmpsMax; + extern bool _useABL; + + #ifdef ESP32_DATA_IDLE_HIGH + void esp32RMTInvertIdle() ; + #endif + inline size_t getNumVirtualBusses() { + size_t j = 0; + for (const auto &bus : busses) j += bus->isVirtual(); + return j; + } - private: - static uint8_t numBusses; - static Bus* busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; - static ColorOrderMap colorOrderMap; - static uint16_t _milliAmpsUsed; - static uint16_t _milliAmpsMax; - static uint8_t _parallelOutputs; - - #ifdef ESP32_DATA_IDLE_HIGH - static void esp32RMTInvertIdle() ; - #endif - static uint8_t getNumVirtualBusses() { - int j = 0; - for (int i=0; iisVirtual()) j++; - return j; - } + inline uint16_t currentMilliamps() { return _gMilliAmpsUsed + MA_FOR_ESP; } + //inline uint16_t ablMilliampsMax() { unsigned sum = 0; for (auto &bus : busses) sum += bus->getMaxCurrent(); return sum; } + inline uint16_t ablMilliampsMax() { return _gMilliAmpsMax; } // used for compatibility reasons (and enabling virtual global ABL) + inline void setMilliampsMax(uint16_t max) { _gMilliAmpsMax = max;} + void initializeABL(); // setup automatic brightness limiter parameters, call once after buses are initialized + void applyABL(); // apply automatic brightness limiter, global or per bus + + uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference); // workaround for access to PolyBus function from FX_fcn.cpp + + //do not call this method from system context (network callback) + void removeAll(); + int add(const BusConfig &bc, bool placeholder); + + void on(); + void off(); + + [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c); + [[gnu::hot]] uint32_t getPixelColor(unsigned pix); + void show(); + bool canAllShow(); + inline void setStatusPixel(uint32_t c) { for (auto &bus : busses) bus->setStatusPixel(c);} + inline void setBrightness(uint8_t b) { for (auto &bus : busses) bus->setBrightness(b); } + // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K + // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() + void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); + inline int16_t getSegmentCCT() { return Bus::getCCT(); } + inline Bus* getBus(size_t busNr) { return busNr < busses.size() ? busses[busNr].get() : nullptr; } + inline size_t getNumBusses() { return busses.size(); } + + //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) + inline uint16_t getTotalLength(bool onlyPhysical = false) { + unsigned len = 0; + for (const auto &bus : busses) if (!(bus->isVirtual() && onlyPhysical)) len += bus->getLength(); + return len; + } + String getLEDTypesJSONString(); + ColorOrderMap& getColorOrderMap(); }; #endif diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index d2a18c9d87..0ecd4f986d 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -1,23 +1,9 @@ +#pragma once #ifndef BusWrapper_h #define BusWrapper_h -#include "NeoPixelBusLg.h" -#include "bus_manager.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 +//#define NPB_CONF_4STEP_CADENCE +#include "NeoPixelBus.h" //Hardware SPI Pins #define P_8266_HS_MOSI 13 @@ -55,110 +41,98 @@ #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 +#define I_8266_U0_UCS_3 21 +#define I_8266_U1_UCS_3 22 +#define I_8266_DM_UCS_3 23 +#define I_8266_BB_UCS_3 24 //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 +#define I_8266_U0_UCS_4 25 +#define I_8266_U1_UCS_4 26 +#define I_8266_DM_UCS_4 27 +#define I_8266_BB_UCS_4 28 //FW1906 GRBCW -#define I_8266_U0_FW6_5 66 -#define I_8266_U1_FW6_5 67 -#define I_8266_DM_FW6_5 68 -#define I_8266_BB_FW6_5 69 +#define I_8266_U0_FW6_5 29 +#define I_8266_U1_FW6_5 30 +#define I_8266_DM_FW6_5 31 +#define I_8266_BB_FW6_5 32 //ESP8266 APA106 -#define I_8266_U0_APA106_3 81 -#define I_8266_U1_APA106_3 82 -#define I_8266_DM_APA106_3 83 -#define I_8266_BB_APA106_3 84 +#define I_8266_U0_APA106_3 33 +#define I_8266_U1_APA106_3 34 +#define I_8266_DM_APA106_3 35 +#define I_8266_BB_APA106_3 36 //WS2805 (RGBCW) -#define I_8266_U0_2805_5 89 -#define I_8266_U1_2805_5 90 -#define I_8266_DM_2805_5 91 -#define I_8266_BB_2805_5 92 +#define I_8266_U0_2805_5 37 +#define I_8266_U1_2805_5 38 +#define I_8266_DM_2805_5 39 +#define I_8266_BB_2805_5 40 //TM1914 (RGB) -#define I_8266_U0_TM1914_3 99 -#define I_8266_U1_TM1914_3 100 -#define I_8266_DM_TM1914_3 101 -#define I_8266_BB_TM1914_3 102 +#define I_8266_U0_TM1914_3 41 +#define I_8266_U1_TM1914_3 42 +#define I_8266_DM_TM1914_3 43 +#define I_8266_BB_TM1914_3 44 //SM16825 (RGBCW) -#define I_8266_U0_SM16825_5 103 -#define I_8266_U1_SM16825_5 104 -#define I_8266_DM_SM16825_5 105 -#define I_8266_BB_SM16825_5 106 +#define I_8266_U0_SM16825_5 45 +#define I_8266_U1_SM16825_5 46 +#define I_8266_DM_SM16825_5 47 +#define I_8266_BB_SM16825_5 48 /*** 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_RN_NEO_3 1 +#define I_32_I2_NEO_3 2 //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_RN_NEO_4 5 +#define I_32_I2_NEO_4 6 //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_RN_400_3 9 +#define I_32_I2_400_3 10 //TM1814 (RGBW) -#define I_32_RN_TM1_4 33 -#define I_32_I0_TM1_4 34 -#define I_32_I1_TM1_4 35 +#define I_32_RN_TM1_4 13 +#define I_32_I2_TM1_4 14 //TM1829 (RGB) -#define I_32_RN_TM2_3 36 -#define I_32_I0_TM2_3 37 -#define I_32_I1_TM2_3 38 +#define I_32_RN_TM2_3 17 +#define I_32_I2_TM2_3 18 //UCS8903 (RGB) -#define I_32_RN_UCS_3 57 -#define I_32_I0_UCS_3 58 -#define I_32_I1_UCS_3 59 +#define I_32_RN_UCS_3 21 +#define I_32_I2_UCS_3 22 //UCS8904 (RGBW) -#define I_32_RN_UCS_4 60 -#define I_32_I0_UCS_4 61 -#define I_32_I1_UCS_4 62 +#define I_32_RN_UCS_4 25 +#define I_32_I2_UCS_4 26 //FW1906 GRBCW -#define I_32_RN_FW6_5 63 -#define I_32_I0_FW6_5 64 -#define I_32_I1_FW6_5 65 +#define I_32_RN_FW6_5 29 +#define I_32_I2_FW6_5 30 //APA106 -#define I_32_RN_APA106_3 85 -#define I_32_I0_APA106_3 86 -#define I_32_I1_APA106_3 87 +#define I_32_RN_APA106_3 33 +#define I_32_I2_APA106_3 34 //WS2805 (RGBCW) -#define I_32_RN_2805_5 93 -#define I_32_I0_2805_5 94 -#define I_32_I1_2805_5 95 +#define I_32_RN_2805_5 37 +#define I_32_I2_2805_5 38 //TM1914 (RGB) -#define I_32_RN_TM1914_3 96 -#define I_32_I0_TM1914_3 97 -#define I_32_I1_TM1914_3 98 +#define I_32_RN_TM1914_3 41 +#define I_32_I2_TM1914_3 42 //SM16825 (RGBCW) -#define I_32_RN_SM16825_5 107 -#define I_32_I0_SM16825_5 108 -#define I_32_I1_SM16825_5 109 +#define I_32_RN_SM16825_5 45 +#define I_32_I2_SM16825_5 46 //APA102 -#define I_HS_DOT_3 39 //hardware SPI -#define I_SS_DOT_3 40 //soft SPI +#define I_HS_DOT_3 101 //hardware SPI +#define I_SS_DOT_3 102 //soft SPI //LPD8806 -#define I_HS_LPD_3 41 -#define I_SS_LPD_3 42 +#define I_HS_LPD_3 103 +#define I_SS_LPD_3 104 //WS2801 -#define I_HS_WS1_3 43 -#define I_SS_WS1_3 44 +#define I_HS_WS1_3 105 +#define I_SS_WS1_3 106 //P9813 -#define I_HS_P98_3 45 -#define I_SS_P98_3 46 +#define I_HS_P98_3 107 +#define I_SS_P98_3 108 //LPD6803 -#define I_HS_LPO_3 47 -#define I_SS_LPO_3 48 +#define I_HS_LPO_3 109 +#define I_SS_LPO_3 110 // In the following NeoGammaNullMethod can be replaced with NeoGammaWLEDMethod to perform Gamma correction implicitly @@ -167,159 +141,196 @@ /*** 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) +#define B_8266_U0_NEO_3 NeoPixelBus //3 chan, esp8266, gpio1 +#define B_8266_U1_NEO_3 NeoPixelBus //3 chan, esp8266, gpio2 +#define B_8266_DM_NEO_3 NeoPixelBus //3 chan, esp8266, gpio3 +#define B_8266_BB_NEO_3 NeoPixelBus //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) +#define B_8266_U0_NEO_4 NeoPixelBus //4 chan, esp8266, gpio1 +#define B_8266_U1_NEO_4 NeoPixelBus //4 chan, esp8266, gpio2 +#define B_8266_DM_NEO_4 NeoPixelBus //4 chan, esp8266, gpio3 +#define B_8266_BB_NEO_4 NeoPixelBus //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) +#define B_8266_U0_400_3 NeoPixelBus //3 chan, esp8266, gpio1 +#define B_8266_U1_400_3 NeoPixelBus //3 chan, esp8266, gpio2 +#define B_8266_DM_400_3 NeoPixelBus //3 chan, esp8266, gpio3 +#define B_8266_BB_400_3 NeoPixelBus //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 +#define B_8266_U0_TM1_4 NeoPixelBus +#define B_8266_U1_TM1_4 NeoPixelBus +#define B_8266_DM_TM1_4 NeoPixelBus +#define B_8266_BB_TM1_4 NeoPixelBus //TM1829 (RGB) -#define B_8266_U0_TM2_3 NeoPixelBusLg -#define B_8266_U1_TM2_3 NeoPixelBusLg -#define B_8266_DM_TM2_3 NeoPixelBusLg -#define B_8266_BB_TM2_3 NeoPixelBusLg +#define B_8266_U0_TM2_3 NeoPixelBus +#define B_8266_U1_TM2_3 NeoPixelBus +#define B_8266_DM_TM2_3 NeoPixelBus +#define B_8266_BB_TM2_3 NeoPixelBus //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) +#define B_8266_U0_UCS_3 NeoPixelBus //3 chan, esp8266, gpio1 +#define B_8266_U1_UCS_3 NeoPixelBus //3 chan, esp8266, gpio2 +#define B_8266_DM_UCS_3 NeoPixelBus //3 chan, esp8266, gpio3 +#define B_8266_BB_UCS_3 NeoPixelBus //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) +#define B_8266_U0_UCS_4 NeoPixelBus //4 chan, esp8266, gpio1 +#define B_8266_U1_UCS_4 NeoPixelBus //4 chan, esp8266, gpio2 +#define B_8266_DM_UCS_4 NeoPixelBus //4 chan, esp8266, gpio3 +#define B_8266_BB_UCS_4 NeoPixelBus //4 chan, esp8266, bb (any pin) //APA106 -#define B_8266_U0_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio1 -#define B_8266_U1_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio2 -#define B_8266_DM_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio3 -#define B_8266_BB_APA106_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin but 16) +#define B_8266_U0_APA106_3 NeoPixelBus //3 chan, esp8266, gpio1 +#define B_8266_U1_APA106_3 NeoPixelBus //3 chan, esp8266, gpio2 +#define B_8266_DM_APA106_3 NeoPixelBus //3 chan, esp8266, gpio3 +#define B_8266_BB_APA106_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) //FW1906 GRBCW -#define B_8266_U0_FW6_5 NeoPixelBusLg //esp8266, gpio1 -#define B_8266_U1_FW6_5 NeoPixelBusLg //esp8266, gpio2 -#define B_8266_DM_FW6_5 NeoPixelBusLg //esp8266, gpio3 -#define B_8266_BB_FW6_5 NeoPixelBusLg //esp8266, bb +#define B_8266_U0_FW6_5 NeoPixelBus //esp8266, gpio1 +#define B_8266_U1_FW6_5 NeoPixelBus //esp8266, gpio2 +#define B_8266_DM_FW6_5 NeoPixelBus //esp8266, gpio3 +#define B_8266_BB_FW6_5 NeoPixelBus //esp8266, bb //WS2805 GRBCW -#define B_8266_U0_2805_5 NeoPixelBusLg //esp8266, gpio1 -#define B_8266_U1_2805_5 NeoPixelBusLg //esp8266, gpio2 -#define B_8266_DM_2805_5 NeoPixelBusLg //esp8266, gpio3 -#define B_8266_BB_2805_5 NeoPixelBusLg //esp8266, bb +#define B_8266_U0_2805_5 NeoPixelBus //esp8266, gpio1 +#define B_8266_U1_2805_5 NeoPixelBus //esp8266, gpio2 +#define B_8266_DM_2805_5 NeoPixelBus //esp8266, gpio3 +#define B_8266_BB_2805_5 NeoPixelBus //esp8266, bb //TM1914 (RGB) -#define B_8266_U0_TM1914_3 NeoPixelBusLg -#define B_8266_U1_TM1914_3 NeoPixelBusLg -#define B_8266_DM_TM1914_3 NeoPixelBusLg -#define B_8266_BB_TM1914_3 NeoPixelBusLg +#define B_8266_U0_TM1914_3 NeoPixelBus +#define B_8266_U1_TM1914_3 NeoPixelBus +#define B_8266_DM_TM1914_3 NeoPixelBus +#define B_8266_BB_TM1914_3 NeoPixelBus //Sm16825 (RGBWC) -#define B_8266_U0_SM16825_5 NeoPixelBusLg -#define B_8266_U1_SM16825_5 NeoPixelBusLg -#define B_8266_DM_SM16825_5 NeoPixelBusLg -#define B_8266_BB_SM16825_5 NeoPixelBusLg +#define B_8266_U0_SM16825_5 NeoPixelBus +#define B_8266_U1_SM16825_5 NeoPixelBus +#define B_8266_DM_SM16825_5 NeoPixelBus +#define B_8266_BB_SM16825_5 NeoPixelBus #endif /*** ESP32 Neopixel methods ***/ #ifdef ARDUINO_ARCH_ESP32 +// C3: I2S0 and I2S1 methods not supported (has one I2S bus) +// S2: I2S0 methods supported (single & parallel), I2S1 methods not supported (has one I2S bus) +// S3: I2S0 methods not supported, I2S1 supports LCD parallel methods (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(CONFIG_IDF_TARGET_ESP32S3) + // S3 will always use LCD parallel output + typedef X8Ws2812xMethod X1Ws2812xMethod; + typedef X8Sk6812Method X1Sk6812Method; + typedef X8400KbpsMethod X1400KbpsMethod; + typedef X8800KbpsMethod X1800KbpsMethod; + typedef X8Tm1814Method X1Tm1814Method; + typedef X8Tm1829Method X1Tm1829Method; + typedef X8Apa106Method X1Apa106Method; + typedef X8Ws2805Method X1Ws2805Method; + typedef X8Tm1914Method X1Tm1914Method; +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + // S2 will use I2S0 + typedef NeoEsp32I2s0Ws2812xMethod X1Ws2812xMethod; + typedef NeoEsp32I2s0Sk6812Method X1Sk6812Method; + typedef NeoEsp32I2s0400KbpsMethod X1400KbpsMethod; + typedef NeoEsp32I2s0800KbpsMethod X1800KbpsMethod; + typedef NeoEsp32I2s0Tm1814Method X1Tm1814Method; + typedef NeoEsp32I2s0Tm1829Method X1Tm1829Method; + typedef NeoEsp32I2s0Apa106Method X1Apa106Method; + typedef NeoEsp32I2s0Ws2805Method X1Ws2805Method; + typedef NeoEsp32I2s0Tm1914Method X1Tm1914Method; +#elif !defined(CONFIG_IDF_TARGET_ESP32C3) + // regular ESP32 will use I2S1 + typedef NeoEsp32I2s1Ws2812xMethod X1Ws2812xMethod; + typedef NeoEsp32I2s1Sk6812Method X1Sk6812Method; + typedef NeoEsp32I2s1400KbpsMethod X1400KbpsMethod; + typedef NeoEsp32I2s1800KbpsMethod X1800KbpsMethod; + typedef NeoEsp32I2s1Tm1814Method X1Tm1814Method; + typedef NeoEsp32I2s1Tm1829Method X1Tm1829Method; + typedef NeoEsp32I2s1Apa106Method X1Apa106Method; + typedef NeoEsp32I2s1Ws2805Method X1Ws2805Method; + typedef NeoEsp32I2s1Tm1914Method X1Tm1914Method; +#endif + +// RMT driver selection +#if !defined(WLED_USE_SHARED_RMT) && !defined(__riscv) +#include +#define NeoEsp32RmtMethod(x) NeoEsp32RmtHIN ## x ## Method +#else +#define NeoEsp32RmtMethod(x) NeoEsp32RmtN ## x ## Method +#endif + //RGB -#define B_32_RN_NEO_3 NeoPixelBusLg -#define B_32_I0_NEO_3 NeoPixelBusLg -#define B_32_I1_NEO_3 NeoPixelBusLg -#define B_32_I1_NEO_3P NeoPixelBusLg // parallel I2S +#define B_32_RN_NEO_3 NeoPixelBus // ESP32, S2, S3, C3 +//#define B_32_IN_NEO_3 NeoPixelBus // ESP32 (dynamic I2S selection) +#define B_32_I2_NEO_3 NeoPixelBus // ESP32, S2, S3 (automatic I2S selection, see typedef above) +#define B_32_IP_NEO_3 NeoPixelBus // parallel I2S (ESP32, S2, S3) //RGBW -#define B_32_RN_NEO_4 NeoPixelBusLg -#define B_32_I0_NEO_4 NeoPixelBusLg -#define B_32_I1_NEO_4 NeoPixelBusLg -#define B_32_I1_NEO_4P NeoPixelBusLg // parallel I2S +#define B_32_RN_NEO_4 NeoPixelBus +#define B_32_I2_NEO_4 NeoPixelBus +#define B_32_IP_NEO_4 NeoPixelBus // parallel I2S //400Kbps -#define B_32_RN_400_3 NeoPixelBusLg -#define B_32_I0_400_3 NeoPixelBusLg -#define B_32_I1_400_3 NeoPixelBusLg -#define B_32_I1_400_3P NeoPixelBusLg // parallel I2S +#define B_32_RN_400_3 NeoPixelBus +#define B_32_I2_400_3 NeoPixelBus +#define B_32_IP_400_3 NeoPixelBus // parallel I2S //TM1814 (RGBW) -#define B_32_RN_TM1_4 NeoPixelBusLg -#define B_32_I0_TM1_4 NeoPixelBusLg -#define B_32_I1_TM1_4 NeoPixelBusLg -#define B_32_I1_TM1_4P NeoPixelBusLg // parallel I2S +#define B_32_RN_TM1_4 NeoPixelBus +#define B_32_I2_TM1_4 NeoPixelBus +#define B_32_IP_TM1_4 NeoPixelBus // parallel I2S //TM1829 (RGB) -#define B_32_RN_TM2_3 NeoPixelBusLg -#define B_32_I0_TM2_3 NeoPixelBusLg -#define B_32_I1_TM2_3 NeoPixelBusLg -#define B_32_I1_TM2_3P NeoPixelBusLg // parallel I2S +#define B_32_RN_TM2_3 NeoPixelBus +#define B_32_I2_TM2_3 NeoPixelBus +#define B_32_IP_TM2_3 NeoPixelBus // parallel I2S //UCS8903 -#define B_32_RN_UCS_3 NeoPixelBusLg -#define B_32_I0_UCS_3 NeoPixelBusLg -#define B_32_I1_UCS_3 NeoPixelBusLg -#define B_32_I1_UCS_3P NeoPixelBusLg // parallel I2S +#define B_32_RN_UCS_3 NeoPixelBus +#define B_32_I2_UCS_3 NeoPixelBus +#define B_32_IP_UCS_3 NeoPixelBus // parallel I2S //UCS8904 -#define B_32_RN_UCS_4 NeoPixelBusLg -#define B_32_I0_UCS_4 NeoPixelBusLg -#define B_32_I1_UCS_4 NeoPixelBusLg -#define B_32_I1_UCS_4P NeoPixelBusLg// parallel I2S +#define B_32_RN_UCS_4 NeoPixelBus +#define B_32_I2_UCS_4 NeoPixelBus +#define B_32_IP_UCS_4 NeoPixelBus// parallel I2S //APA106 -#define B_32_RN_APA106_3 NeoPixelBusLg -#define B_32_I0_APA106_3 NeoPixelBusLg -#define B_32_I1_APA106_3 NeoPixelBusLg -#define B_32_I1_APA106_3P NeoPixelBusLg // parallel I2S +#define B_32_RN_APA106_3 NeoPixelBus +#define B_32_I2_APA106_3 NeoPixelBus +#define B_32_IP_APA106_3 NeoPixelBus // parallel I2S //FW1906 GRBCW -#define B_32_RN_FW6_5 NeoPixelBusLg -#define B_32_I0_FW6_5 NeoPixelBusLg -#define B_32_I1_FW6_5 NeoPixelBusLg -#define B_32_I1_FW6_5P NeoPixelBusLg // parallel I2S +#define B_32_RN_FW6_5 NeoPixelBus +#define B_32_I2_FW6_5 NeoPixelBus +#define B_32_IP_FW6_5 NeoPixelBus // parallel I2S //WS2805 RGBWC -#define B_32_RN_2805_5 NeoPixelBusLg -#define B_32_I0_2805_5 NeoPixelBusLg -#define B_32_I1_2805_5 NeoPixelBusLg -#define B_32_I1_2805_5P NeoPixelBusLg // parallel I2S +#define B_32_RN_2805_5 NeoPixelBus +#define B_32_I2_2805_5 NeoPixelBus +#define B_32_IP_2805_5 NeoPixelBus // parallel I2S //TM1914 (RGB) -#define B_32_RN_TM1914_3 NeoPixelBusLg -#define B_32_I0_TM1914_3 NeoPixelBusLg -#define B_32_I1_TM1914_3 NeoPixelBusLg -#define B_32_I1_TM1914_3P NeoPixelBusLg // parallel I2S +#define B_32_RN_TM1914_3 NeoPixelBus +#define B_32_I2_TM1914_3 NeoPixelBus +#define B_32_IP_TM1914_3 NeoPixelBus // parallel I2S //Sm16825 (RGBWC) -#define B_32_RN_SM16825_5 NeoPixelBusLg -#define B_32_I0_SM16825_5 NeoPixelBusLg -#define B_32_I1_SM16825_5 NeoPixelBusLg -#define B_32_I1_SM16825_5P NeoPixelBusLg // parallel I2S +#define B_32_RN_SM16825_5 NeoPixelBus +#define B_32_I2_SM16825_5 NeoPixelBus +#define B_32_IP_SM16825_5 NeoPixelBus // parallel 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) +#define B_HS_DOT_3 NeoPixelBus //hardware HSPI (was DotStarEsp32DmaHspi5MhzMethod in NPB @ 2.6.9) #else -#define B_HS_DOT_3 NeoPixelBusLg //hardware VSPI +#define B_HS_DOT_3 NeoPixelBus //hardware VSPI #endif -#define B_SS_DOT_3 NeoPixelBusLg //soft SPI +#define B_SS_DOT_3 NeoPixelBus //soft SPI //LPD8806 -#define B_HS_LPD_3 NeoPixelBusLg -#define B_SS_LPD_3 NeoPixelBusLg +#define B_HS_LPD_3 NeoPixelBus +#define B_SS_LPD_3 NeoPixelBus //LPD6803 -#define B_HS_LPO_3 NeoPixelBusLg -#define B_SS_LPO_3 NeoPixelBusLg +#define B_HS_LPO_3 NeoPixelBus +#define B_SS_LPO_3 NeoPixelBus //WS2801 #ifdef WLED_USE_ETHERNET -#define B_HS_WS1_3 NeoPixelBusLg>, NeoGammaNullMethod> +#define B_HS_WS1_3 NeoPixelBus>> #else -#define B_HS_WS1_3 NeoPixelBusLg +#define B_HS_WS1_3 NeoPixelBus #endif -#define B_SS_WS1_3 NeoPixelBusLg +#define B_SS_WS1_3 NeoPixelBus //P9813 -#define B_HS_P98_3 NeoPixelBusLg -#define B_SS_P98_3 NeoPixelBusLg +#define B_HS_P98_3 NeoPixelBus +#define B_SS_P98_3 NeoPixelBus // 48bit & 64bit to 24bit & 32bit RGB(W) conversion #define toRGBW32(c) (RGBW32((c>>40)&0xFF, (c>>24)&0xFF, (c>>8)&0xFF, (c>>56)&0xFF)) @@ -328,11 +339,17 @@ //handles pointer type conversion for all possible bus types class PolyBus { private: - static bool useParallelI2S; - + #ifndef ESP8266 + static bool _useParallelI2S; // use parallel I2S/LCD (8 channels) + static uint8_t _rmtChannelsAssigned; // RMT channel tracking for dynamic allocation + static uint8_t _rmtChannel; // physical RMT channel to use during bus creation + static uint8_t _i2sChannelsAssigned; // I2S channel tracking for dynamic allocation + static uint8_t _parallelBusItype; // parallel output does not allow mixed LED types, track I_Type + static uint8_t _2PchannelsAssigned; // 2-Pin (SPI) channel assigned: first one gets the hardware SPI, others use bit-banged SPI + // note on 2-Pin Types: all supported types except WS2801 use start/stop or latch frames, speed is not critical. WS2801 uses a 500us timeout and is prone to flickering if bit-banged too slow. + // TODO: according to #4863 using more than one bit-banged output can cause glitches even in APA102. This needs investigation as from a hardware perspective all but WS2801 should be immune to timing issues. + #endif public: - static inline void setParallelI2S1Output(bool b = true) { useParallelI2S = b; } - static inline bool isParallelI2S1Output(void) { return useParallelI2S; } // initialize SPI bus speed for DotStar methods template @@ -436,34 +453,19 @@ class PolyBus { case I_32_RN_TM1914_3: beginTM1914(busPtr); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->Begin(); break; // I2S1 bus or parellel buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_TM1_4: if (useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_TM1914_3: if (useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_TM1_4: beginTM1814(busPtr); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_2805_5: (static_cast(busPtr))->Begin(); break; - case I_32_I0_TM1914_3: beginTM1914(busPtr); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->Begin(); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_TM1_4: if (_useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; + case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; #endif // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; @@ -480,13 +482,7 @@ class PolyBus { } } - static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel) { - #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) - // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation - // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation - if (useParallelI2S && channel > 7) channel -= 8; // accommodate parallel I2S1 which is used 1st on classic ESP32 - else if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 - #endif + static void* create(uint8_t busType, uint8_t* pins, uint16_t len) { void* busPtr = nullptr; switch (busType) { case I_NONE: break; @@ -542,47 +538,32 @@ class PolyBus { #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses - case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; + case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) busPtr = new B_32_I1_NEO_3P(len, pins[0]); else busPtr = new B_32_I1_NEO_3(len, pins[0]); break; - case I_32_I1_NEO_4: if (useParallelI2S) busPtr = new B_32_I1_NEO_4P(len, pins[0]); else busPtr = new B_32_I1_NEO_4(len, pins[0]); break; - case I_32_I1_400_3: if (useParallelI2S) busPtr = new B_32_I1_400_3P(len, pins[0]); else busPtr = new B_32_I1_400_3(len, pins[0]); break; - case I_32_I1_TM1_4: if (useParallelI2S) busPtr = new B_32_I1_TM1_4P(len, pins[0]); else busPtr = new B_32_I1_TM1_4(len, pins[0]); break; - case I_32_I1_TM2_3: if (useParallelI2S) busPtr = new B_32_I1_TM2_3P(len, pins[0]); else busPtr = new B_32_I1_TM2_3(len, pins[0]); break; - case I_32_I1_UCS_3: if (useParallelI2S) busPtr = new B_32_I1_UCS_3P(len, pins[0]); else busPtr = new B_32_I1_UCS_3(len, pins[0]); break; - case I_32_I1_UCS_4: if (useParallelI2S) busPtr = new B_32_I1_UCS_4P(len, pins[0]); else busPtr = new B_32_I1_UCS_4(len, pins[0]); break; - case I_32_I1_APA106_3: if (useParallelI2S) busPtr = new B_32_I1_APA106_3P(len, pins[0]); else busPtr = new B_32_I1_APA106_3(len, pins[0]); break; - case I_32_I1_FW6_5: if (useParallelI2S) busPtr = new B_32_I1_FW6_5P(len, pins[0]); else busPtr = new B_32_I1_FW6_5(len, pins[0]); break; - case I_32_I1_2805_5: if (useParallelI2S) busPtr = new B_32_I1_2805_5P(len, pins[0]); else busPtr = new B_32_I1_2805_5(len, pins[0]); break; - case I_32_I1_TM1914_3: if (useParallelI2S) busPtr = new B_32_I1_TM1914_3P(len, pins[0]); else busPtr = new B_32_I1_TM1914_3(len, pins[0]); break; - case I_32_I1_SM16825_5: if (useParallelI2S) busPtr = new B_32_I1_SM16825_5P(len, pins[0]); else busPtr = new B_32_I1_SM16825_5(len, pins[0]); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; - case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; - case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; - 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; - case I_32_I0_UCS_3: busPtr = new B_32_I0_UCS_3(len, pins[0]); break; - case I_32_I0_UCS_4: busPtr = new B_32_I0_UCS_4(len, pins[0]); break; - case I_32_I0_APA106_3: busPtr = new B_32_I0_APA106_3(len, pins[0]); break; - case I_32_I0_FW6_5: busPtr = new B_32_I0_FW6_5(len, pins[0]); break; - case I_32_I0_2805_5: busPtr = new B_32_I0_2805_5(len, pins[0]); break; - case I_32_I0_TM1914_3: busPtr = new B_32_I0_TM1914_3(len, pins[0]); break; - case I_32_I0_SM16825_5: busPtr = new B_32_I0_SM16825_5(len, pins[0]); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) busPtr = new B_32_IP_NEO_3(len, pins[0]); else busPtr = new B_32_I2_NEO_3(len, pins[0]); break; + case I_32_I2_NEO_4: if (_useParallelI2S) busPtr = new B_32_IP_NEO_4(len, pins[0]); else busPtr = new B_32_I2_NEO_4(len, pins[0]); break; + case I_32_I2_400_3: if (_useParallelI2S) busPtr = new B_32_IP_400_3(len, pins[0]); else busPtr = new B_32_I2_400_3(len, pins[0]); break; + case I_32_I2_TM1_4: if (_useParallelI2S) busPtr = new B_32_IP_TM1_4(len, pins[0]); else busPtr = new B_32_I2_TM1_4(len, pins[0]); break; + case I_32_I2_TM2_3: if (_useParallelI2S) busPtr = new B_32_IP_TM2_3(len, pins[0]); else busPtr = new B_32_I2_TM2_3(len, pins[0]); break; + case I_32_I2_UCS_3: if (_useParallelI2S) busPtr = new B_32_IP_UCS_3(len, pins[0]); else busPtr = new B_32_I2_UCS_3(len, pins[0]); break; + case I_32_I2_UCS_4: if (_useParallelI2S) busPtr = new B_32_IP_UCS_4(len, pins[0]); else busPtr = new B_32_I2_UCS_4(len, pins[0]); break; + case I_32_I2_APA106_3: if (_useParallelI2S) busPtr = new B_32_IP_APA106_3(len, pins[0]); else busPtr = new B_32_I2_APA106_3(len, pins[0]); break; + case I_32_I2_FW6_5: if (_useParallelI2S) busPtr = new B_32_IP_FW6_5(len, pins[0]); else busPtr = new B_32_I2_FW6_5(len, pins[0]); break; + case I_32_I2_2805_5: if (_useParallelI2S) busPtr = new B_32_IP_2805_5(len, pins[0]); else busPtr = new B_32_I2_2805_5(len, pins[0]); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) busPtr = new B_32_IP_TM1914_3(len, pins[0]); else busPtr = new B_32_I2_TM1914_3(len, pins[0]); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) busPtr = new B_32_IP_SM16825_5(len, pins[0]); else busPtr = new B_32_I2_SM16825_5(len, pins[0]); break; #endif #endif // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) @@ -669,34 +650,19 @@ class PolyBus { case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->Show(consistent); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; @@ -743,6 +709,7 @@ class PolyBus { 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; + case I_8266_BB_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_APA106_3: return (static_cast(busPtr))->CanShow(); break; @@ -779,34 +746,19 @@ class PolyBus { case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_SM16825_5: return (static_cast(busPtr))->CanShow(); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_NEO_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_400_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM1_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM2_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_UCS_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_UCS_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_APA106_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_FW6_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_2805_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM1914_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_SM16825_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_NEO_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_400_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_TM1_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_TM2_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_UCS_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_UCS_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_APA106_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_FW6_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_2805_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; #endif #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; @@ -823,7 +775,7 @@ class PolyBus { return true; } - static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { + [[gnu::hot]] static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { uint8_t r = c >> 16; uint8_t g = c >> 8; uint8_t b = c >> 0; @@ -916,34 +868,19 @@ class PolyBus { case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - 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; - case I_32_I0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -959,118 +896,7 @@ class PolyBus { } } - 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; - case I_8266_U0_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_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; - case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #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) { + [[gnu::hot]] 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; @@ -1139,34 +965,19 @@ class PolyBus { case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_NEO_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_400_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_TM1_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_TM2_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_UCS_3: { Rgb48Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; - case I_32_I1_UCS_4: { Rgbw64Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; - case I_32_I1_APA106_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_FW6_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I1_2805_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I1_TM1914_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_SM16825_5: { Rgbww80Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - 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; - case I_32_I0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; - case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; - case I_32_I0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_NEO_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_400_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_TM1_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_TM2_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_UCS_3: { Rgb48Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; + case I_32_I2_UCS_4: { Rgbw64Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; + case I_32_I2_APA106_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_FW6_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I2_2805_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I2_TM1914_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_SM16825_5: { Rgbww80Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W #endif #endif case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1269,34 +1080,19 @@ class PolyBus { case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; case I_32_RN_SM16825_5: delete (static_cast(busPtr)); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_NEO_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_400_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_TM1_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_TM2_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_UCS_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_UCS_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_APA106_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_FW6_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_2805_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_TM1914_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_SM16825_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: delete (static_cast(busPtr)); break; - case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; - case I_32_I0_400_3: delete (static_cast(busPtr)); break; - case I_32_I0_TM1_4: delete (static_cast(busPtr)); break; - case I_32_I0_TM2_3: delete (static_cast(busPtr)); break; - case I_32_I0_UCS_3: delete (static_cast(busPtr)); break; - case I_32_I0_UCS_4: delete (static_cast(busPtr)); break; - case I_32_I0_APA106_3: delete (static_cast(busPtr)); break; - case I_32_I0_FW6_5: delete (static_cast(busPtr)); break; - case I_32_I0_2805_5: delete (static_cast(busPtr)); break; - case I_32_I0_TM1914_3: delete (static_cast(busPtr)); break; - case I_32_I0_SM16825_5: delete (static_cast(busPtr)); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_NEO_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_400_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_TM1_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_TM2_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_UCS_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_UCS_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_APA106_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_FW6_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_2805_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; #endif #endif case I_HS_DOT_3: delete (static_cast(busPtr)); break; @@ -1312,29 +1108,207 @@ class PolyBus { } } - //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) { + static unsigned getDataSize(void* busPtr, uint8_t busType) { + unsigned size = 0; + #ifdef ARDUINO_ARCH_ESP32 + size = 100; // ~100bytes for NPB internal structures (measured for both I2S and RMT, much smaller and more variable on ESP8266) + #endif + switch (busType) { + case I_NONE: break; + #ifdef ESP8266 + case I_8266_U0_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_NEO_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_NEO_4: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_400_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_400_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_400_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_400_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_TM1_4: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_TM2_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_UCS_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_UCS_4: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_APA106_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_FW6_5: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_2805_5: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U0_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_U1_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; + case I_8266_DM_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; + #endif + #ifdef ARDUINO_ARCH_ESP32 + // RMT buses (front + back + small system managed RMT) + case I_32_RN_NEO_3: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_NEO_4: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_400_3: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_TM1_4: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_TM2_3: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_UCS_3: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_UCS_4: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_APA106_3: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_FW6_5: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_2805_5: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_TM1914_3: size += (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_SM16825_5: size += (static_cast(busPtr))->PixelsSize()*2; break; + // I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes) not: for parallel I2S only the largest bus counts for DMA memory, this is not done correctly here, also assumes 3-step cadence + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_NEO_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_400_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_TM1_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_TM2_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_UCS_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_UCS_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_APA106_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_FW6_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_2805_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_TM1914_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_SM16825_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + #endif + #endif + case I_HS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_HS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_HS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_HS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_HS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + } + return size; + } + + static unsigned memUsage(unsigned count, unsigned busType) { + unsigned size = count*3; // let's assume 3 channels, we will add count or 2*count below for 4 channels or 5 channels + switch (busType) { + case I_NONE: size = 0; break; + #ifdef ESP8266 + // UART methods have front + back buffers + small UART + case I_8266_U0_NEO_4 : // fallthrough + case I_8266_U1_NEO_4 : // fallthrough + case I_8266_BB_NEO_4 : // fallthrough + case I_8266_U0_TM1_4 : // fallthrough + case I_8266_U1_TM1_4 : // fallthrough + case I_8266_BB_TM1_4 : size = (size + count); break; // 4 channels + case I_8266_U0_UCS_3 : // fallthrough + case I_8266_U1_UCS_3 : // fallthrough + case I_8266_BB_UCS_3 : size *= 2; break; // 16 bit + case I_8266_U0_UCS_4 : // fallthrough + case I_8266_U1_UCS_4 : // fallthrough + case I_8266_BB_UCS_4 : size = (size + count)*2; break; // 16 bit 4 channels + case I_8266_U0_FW6_5 : // fallthrough + case I_8266_U1_FW6_5 : // fallthrough + case I_8266_BB_FW6_5 : // fallthrough + case I_8266_U0_2805_5 : // fallthrough + case I_8266_U1_2805_5 : // fallthrough + case I_8266_BB_2805_5 : size = (size + 2*count); break; // 5 channels + case I_8266_U0_SM16825_5: // fallthrough + case I_8266_U1_SM16825_5: // fallthrough + case I_8266_BB_SM16825_5: size = (size + 2*count)*2; break; // 16 bit 5 channels + // DMA methods have front + DMA buffer = ((1+(3+1)) * channels; exact value is a bit of mistery - needs a dig into NPB) + case I_8266_DM_NEO_3 : // fallthrough + case I_8266_DM_400_3 : // fallthrough + case I_8266_DM_TM2_3 : // fallthrough + case I_8266_DM_APA106_3 : // fallthrough + case I_8266_DM_TM1914_3 : size *= 5; break; + case I_8266_DM_NEO_4 : // fallthrough + case I_8266_DM_TM1_4 : size = (size + count)*5; break; + case I_8266_DM_UCS_3 : size *= 2*5; break; + case I_8266_DM_UCS_4 : size = (size + count)*2*5; break; + case I_8266_DM_FW6_5 : // fallthrough + case I_8266_DM_2805_5 : size = (size + 2*count)*5; break; + case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break; + #else + // note: RMT and I2S buses use ~100 bytes of internal NPB memory each, not included here for simplicity + // RMT buses (1x front and 1x back buffer, does not include small RMT buffer) + case I_32_RN_NEO_4 : // fallthrough + case I_32_RN_TM1_4 : size = (size + count)*2; break; // 4 channels + case I_32_RN_UCS_3 : size *= 2*2; break; // 16bit + case I_32_RN_UCS_4 : size = (size + count)*2*2; break; // 16bit, 4 channels + case I_32_RN_FW6_5 : // fallthrough + case I_32_RN_2805_5 : size = (size + 2*count)*2; break; // 5 channels + case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; // 16bit, 5 channels + // I2S bus or paralell I2S buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD) + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3 : // fallthrough + case I_32_I2_400_3 : // fallthrough + case I_32_I2_TM2_3 : // fallthrough + case I_32_I2_APA106_3 : break; // do nothing, I2S uses single buffer + DMA buffer + case I_32_I2_NEO_4 : // fallthrough + case I_32_I2_TM1_4 : size = (size + count); break; // 4 channels + case I_32_I2_UCS_3 : size *= 2; break; // 16 bit + case I_32_I2_UCS_4 : size = (size + count)*2; break; // 16 bit, 4 channels + case I_32_I2_FW6_5 : // fallthrough + case I_32_I2_2805_5 : size = (size + 2*count); break; // 5 channels + case I_32_I2_SM16825_5: size = (size + 2*count)*2; break; // 16 bit, 5 channels + #endif + default : size *= 2; break; // everything else uses 2 buffers + #endif + } + return size; + } +#ifndef ESP8266 + // Reset channel tracking (call before adding buses) + static void resetChannelTracking() { + _useParallelI2S = false; + _rmtChannelsAssigned = 0; + _rmtChannel = 0; + _i2sChannelsAssigned = 0; + _parallelBusItype = I_NONE; + _2PchannelsAssigned = 0; + } +#endif + // reserves and gives back the internal type index (I_XX_XXX_X above) for the input based on bus type and pins + static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) { if (!Bus::isDigital(busType)) return I_NONE; + uint8_t t = I_NONE; if (Bus::is2Pin(busType)) { //SPI LED chips bool isHSPI = false; #ifdef ESP8266 if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true; #else - // temporary hack to limit use of hardware SPI to a single SPI peripheral (HSPI): only allow ESP32 hardware serial on segment 0 - // SPI global variable is normally linked to VSPI on ESP32 (or FSPI C3, S3) - if (!num) isHSPI = true; + if (_2PchannelsAssigned == 0) isHSPI = true; // first 2-pin channel uses hardware SPI + _2PchannelsAssigned++; #endif - uint8_t t = I_NONE; switch (busType) { case TYPE_APA102: t = I_SS_DOT_3; break; case TYPE_LPD8806: t = I_SS_LPD_3; break; case TYPE_LPD6803: t = I_SS_LPO_3; break; case TYPE_WS2801: t = I_SS_WS1_3; break; case TYPE_P9813: t = I_SS_P98_3; break; - default: t=I_NONE; } if (t > I_NONE && isHSPI) t--; //hardware SPI has one smaller ID than software - return t; } else { #ifdef ESP8266 uint8_t offset = pins[0] -1; //for driver: 0 = uart0, 1 = uart1, 2 = dma, 3 = bitbang @@ -1344,88 +1318,87 @@ class PolyBus { case TYPE_WS2812_2CH_X3: case TYPE_WS2812_RGB: case TYPE_WS2812_WWA: - return I_8266_U0_NEO_3 + offset; + t = I_8266_U0_NEO_3 + offset; break; case TYPE_SK6812_RGBW: - return I_8266_U0_NEO_4 + offset; + t = I_8266_U0_NEO_4 + offset; break; case TYPE_WS2811_400KHZ: - return I_8266_U0_400_3 + offset; + t = I_8266_U0_400_3 + offset; break; case TYPE_TM1814: - return I_8266_U0_TM1_4 + offset; + t = I_8266_U0_TM1_4 + offset; break; case TYPE_TM1829: - return I_8266_U0_TM2_3 + offset; + t = I_8266_U0_TM2_3 + offset; break; case TYPE_UCS8903: - return I_8266_U0_UCS_3 + offset; + t = I_8266_U0_UCS_3 + offset; break; case TYPE_UCS8904: - return I_8266_U0_UCS_4 + offset; + t = I_8266_U0_UCS_4 + offset; break; case TYPE_APA106: - return I_8266_U0_APA106_3 + offset; + t = I_8266_U0_APA106_3 + offset; break; case TYPE_FW1906: - return I_8266_U0_FW6_5 + offset; + t = I_8266_U0_FW6_5 + offset; break; case TYPE_WS2805: - return I_8266_U0_2805_5 + offset; + t = I_8266_U0_2805_5 + offset; break; case TYPE_TM1914: - return I_8266_U0_TM1914_3 + offset; + t = I_8266_U0_TM1914_3 + offset; break; case TYPE_SM16825: - return I_8266_U0_SM16825_5 + offset; + t = I_8266_U0_SM16825_5 + offset; break; } #else //ESP32 - uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S0 (used by Audioreactive), 2 = 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 (use last to allow Audioreactive) - #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 (useParallelI2S) { - if (num > 16) return I_NONE; - if (num < 8) offset = 2; // prefer 8 parallel I2S1 channels - if (num == 16) offset = 1; + // dynamic channel allocation based on driver preference + // determine which driver to use based on preference and availability. First I2S bus locks the I2S type, all subsequent I2S buses are assigned the same type (hardware restriction) + uint8_t offset = 0; // 0 = RMT, 1 = I2S/LCD + if (driverPreference == 0 && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) { + _rmtChannelsAssigned++; + } else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) { + offset = 1; // I2S requested or RMT full + _i2sChannelsAssigned++; } else { - if (num > 9) return I_NONE; - if (num > 8) offset = 1; - if (num == 0) offset = 2; // prefer I2S1 for 1st bus (less flickering but more RAM needed) + return I_NONE; // No channels available } - #endif + + // Now determine actual bus type with the chosen offset switch (busType) { case TYPE_WS2812_1CH_X3: case TYPE_WS2812_2CH_X3: case TYPE_WS2812_RGB: case TYPE_WS2812_WWA: - return I_32_RN_NEO_3 + offset; + t = I_32_RN_NEO_3 + offset; break; case TYPE_SK6812_RGBW: - return I_32_RN_NEO_4 + offset; + t = I_32_RN_NEO_4 + offset; break; case TYPE_WS2811_400KHZ: - return I_32_RN_400_3 + offset; + t = I_32_RN_400_3 + offset; break; case TYPE_TM1814: - return I_32_RN_TM1_4 + offset; + t = I_32_RN_TM1_4 + offset; break; case TYPE_TM1829: - return I_32_RN_TM2_3 + offset; + t = I_32_RN_TM2_3 + offset; break; case TYPE_UCS8903: - return I_32_RN_UCS_3 + offset; + t = I_32_RN_UCS_3 + offset; break; case TYPE_UCS8904: - return I_32_RN_UCS_4 + offset; + t = I_32_RN_UCS_4 + offset; break; case TYPE_APA106: - return I_32_RN_APA106_3 + offset; + t = I_32_RN_APA106_3 + offset; break; case TYPE_FW1906: - return I_32_RN_FW6_5 + offset; + t = I_32_RN_FW6_5 + offset; break; case TYPE_WS2805: - return I_32_RN_2805_5 + offset; + t = I_32_RN_2805_5 + offset; break; case TYPE_TM1914: - return I_32_RN_TM1914_3 + offset; + t = I_32_RN_TM1914_3 + offset; break; case TYPE_SM16825: - return I_32_RN_SM16825_5 + offset; + t = I_32_RN_SM16825_5 + offset; break; + } + // If using parallel I2S, set the type accordingly + if (_i2sChannelsAssigned == 1 && offset == 1) { // first I2S channel request, lock the type + _parallelBusItype = t; + #ifdef CONFIG_IDF_TARGET_ESP32S3 + _useParallelI2S = true; // ESP32-S3 always uses parallel I2S (LCD method) + #endif + } + else if (offset == 1) { // not first I2S channel, use locked type and enable parallel flag + _useParallelI2S = true; + t = _parallelBusItype; } #endif } - return I_NONE; + return t; } }; #endif diff --git a/wled00/button.cpp b/wled00/button.cpp index 4d6f954f60..d544dd73ab 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -17,19 +17,19 @@ static bool buttonBriDirection = false; // true: increase brightness, false: dec void shortPressAction(uint8_t b) { - if (!macroButton[b]) { + if (!buttons[b].macroButton) { switch (b) { case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break; case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break; } } else { - applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); + applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET); } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "short"); } @@ -38,9 +38,9 @@ void shortPressAction(uint8_t b) void longPressAction(uint8_t b) { - if (!macroLongPress[b]) { + if (!buttons[b].macroLongPress) { switch (b) { - case 0: setRandomColor(col); colorUpdated(CALL_MODE_BUTTON); break; + case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break; case 1: if(buttonBriDirection) { if (bri == 255) break; // avoid unnecessary updates to brightness @@ -52,17 +52,17 @@ void longPressAction(uint8_t b) else bri -= WLED_LONG_BRI_STEPS; } stateUpdated(CALL_MODE_BUTTON); - buttonPressedTime[b] = millis(); + buttons[b].pressedTime = millis(); break; // repeatable action } } else { - applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); + applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET); } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "long"); } @@ -71,31 +71,31 @@ void longPressAction(uint8_t b) void doublePressAction(uint8_t b) { - if (!macroDoublePress[b]) { + if (!buttons[b].macroDoublePress) { switch (b) { //case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set - case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; + case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; } } else { - applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET); + applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET); } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "double"); } #endif } -bool isButtonPressed(uint8_t i) +bool isButtonPressed(uint8_t b) { - if (btnPin[i]<0) return false; - unsigned pin = btnPin[i]; + if (buttons[b].pin < 0) return false; + unsigned pin = buttons[b].pin; - switch (buttonType[i]) { + switch (buttons[b].type) { case BTN_TYPE_NONE: case BTN_TYPE_RESERVED: break; @@ -113,7 +113,7 @@ bool isButtonPressed(uint8_t i) #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt) if (touchInterruptGetLastStatus(pin)) return true; #else - if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) return true; + if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true; #endif #endif break; @@ -124,25 +124,25 @@ bool isButtonPressed(uint8_t i) void handleSwitch(uint8_t b) { // isButtonPressed() handles inverted/noninverted logic - if (buttonPressedBefore[b] != isButtonPressed(b)) { + if (buttons[b].pressedBefore != isButtonPressed(b)) { DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b); - buttonPressedTime[b] = millis(); - buttonPressedBefore[b] = !buttonPressedBefore[b]; + buttons[b].pressedTime = millis(); + buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state } - if (buttonLongPressed[b] == buttonPressedBefore[b]) return; + if (buttons[b].longPressed == buttons[b].pressedBefore) return; - if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) + if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b); - if (!buttonPressedBefore[b]) { // on -> off + if (!buttons[b].pressedBefore) { // on -> off DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b); - if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); + if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET); else { //turn on if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);} } } else { // off -> on DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b); - if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); + if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET); else { //turn off if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);} } @@ -151,14 +151,14 @@ void handleSwitch(uint8_t b) #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); + char subuf[MQTT_MAX_TOPIC_LEN + 32]; + if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b); else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); - mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on"); + mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? "off" : "on"); } #endif - buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state + buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state } } @@ -178,17 +178,17 @@ void handleAnalog(uint8_t b) #ifdef ESP8266 rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit #else - if ((btnPin[b] < 0) /*|| (digitalPinToAnalogChannel(btnPin[b]) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise - rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution + if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise + rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution #endif yield(); // keep WiFi task running - analog read may take several millis on ESP8266 filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255] unsigned aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit - if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used - if(aRead >= 255-POT_SENSITIVITY) aRead = 255; + if (aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used + if (aRead >= 255-POT_SENSITIVITY) aRead = 255; - if (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead; + if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead; // remove noise & reduce frequency of UI updates if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading @@ -206,10 +206,10 @@ void handleAnalog(uint8_t b) oldRead[b] = aRead; // if no macro for "short press" and "long press" is defined use brightness control - if (!macroButton[b] && !macroLongPress[b]) { - DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), macroDoublePress[b]); + if (!buttons[b].macroButton && !buttons[b].macroLongPress) { + DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), buttons[b].macroDoublePress); // if "double press" macro defines which option to change - if (macroDoublePress[b] >= 250) { + if (buttons[b].macroDoublePress >= 250) { // global brightness if (aRead == 0) { briLast = bri; @@ -218,27 +218,30 @@ void handleAnalog(uint8_t b) if (bri == 0) strip.restartRuntime(); bri = aRead; } - } else if (macroDoublePress[b] == 249) { + } else if (buttons[b].macroDoublePress == 249) { // effect speed effectSpeed = aRead; - } else if (macroDoublePress[b] == 248) { + } else if (buttons[b].macroDoublePress == 248) { // effect intensity effectIntensity = aRead; - } else if (macroDoublePress[b] == 247) { + } else if (buttons[b].macroDoublePress == 247) { // selected palette - effectPalette = map(aRead, 0, 252, 0, 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) { + effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1); + effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result + } else if (buttons[b].macroDoublePress == 200) { // primary color, hue, full saturation - colorHStoRGB(aRead*256,255,col); + colorHStoRGB(aRead*256, 255, colPri); } else { // otherwise use "double press" for segment selection - Segment& seg = strip.getSegment(macroDoublePress[b]); + Segment& seg = strip.getSegment(buttons[b].macroDoublePress); if (aRead == 0) { - seg.setOption(SEG_OPTION_ON, false); // off (use transition) + seg.on = false; // do not use transition + //seg.setOption(SEG_OPTION_ON, false); // off (use transition) } else { - seg.setOpacity(aRead); - seg.setOption(SEG_OPTION_ON, true); // on (use transition) + seg.opacity = aRead; // set brightness (opacity) of segment + seg.on = true; + //seg.setOpacity(aRead); + //seg.setOption(SEG_OPTION_ON, true); // on (use transition) } // this will notify clients of update (websockets,mqtt,etc) updateInterfaces(CALL_MODE_BUTTON); @@ -261,16 +264,16 @@ void handleButton() if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips) lastRun = now; - for (unsigned b=0; b ANALOG_BTN_READ_CYCLE) { handleAnalog(b); } @@ -278,7 +281,7 @@ void handleButton() } // button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0) - if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_TOUCH_SWITCH || buttonType[b] == BTN_TYPE_PIR_SENSOR) { + if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) { handleSwitch(b); continue; } @@ -287,70 +290,66 @@ void handleButton() if (isButtonPressed(b)) { // pressed // if all macros are the same, fire action immediately on rising edge - if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) { - if (!buttonPressedBefore[b]) - shortPressAction(b); - buttonPressedBefore[b] = true; - buttonPressedTime[b] = now; // continually update (for debouncing to work in release handler) + if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) { + if (!buttons[b].pressedBefore) shortPressAction(b); + buttons[b].pressedBefore = true; + buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler) continue; } - if (!buttonPressedBefore[b]) buttonPressedTime[b] = now; - buttonPressedBefore[b] = true; + if (!buttons[b].pressedBefore) buttons[b].pressedTime = now; + buttons[b].pressedBefore = true; - if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press - if (!buttonLongPressed[b]) { + if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press + if (!buttons[b].longPressed) { buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press longPressAction(b); } else if (b) { //repeatable action (~5 times per s) on button > 0 longPressAction(b); - buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms + buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms } - buttonLongPressed[b] = true; + buttons[b].longPressed = true; } - } else if (buttonPressedBefore[b]) { //released - long dur = now - buttonPressedTime[b]; + } else if (buttons[b].pressedBefore) { //released + long dur = now - buttons[b].pressedTime; // released after rising-edge short press action - if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) { - if (dur > WLED_DEBOUNCE_THRESHOLD) buttonPressedBefore[b] = false; // debounce, blocks button for 50 ms once it has been released + if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) { + if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released continue; } - if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} // too short "press", debounce - bool doublePress = buttonWaitTime[b]; //did we have a short press before? - buttonWaitTime[b] = 0; + if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce + bool doublePress = buttons[b].waitTime; //did we have a short press before? + buttons[b].waitTime = 0; if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released) if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds WLED_FS.format(); - #ifdef WLED_ADD_EEPROM_SUPPORT - clearEEPROM(); - #endif doReboot = true; } else { WLED::instance().initAP(true); } - } else if (!buttonLongPressed[b]) { //short press + } else if (!buttons[b].longPressed) { //short press //NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling - if (b != 1 && !macroDoublePress[b]) { //don't wait for double press on buttons without a default action if no double press macro set + if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set shortPressAction(b); } else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0) if (doublePress) { doublePressAction(b); } else { - buttonWaitTime[b] = now; + buttons[b].waitTime = now; } } } - buttonPressedBefore[b] = false; - buttonLongPressed[b] = false; + buttons[b].pressedBefore = false; + buttons[b].longPressed = false; } //if 350ms elapsed since last short press release it is a short press - if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) { - buttonWaitTime[b] = 0; + if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) { + buttons[b].waitTime = 0; shortPressAction(b); } } @@ -368,23 +367,29 @@ void handleIO() // if we want to control on-board LED (ESP8266) or relay we have to do it here as the final show() may not happen until // next loop() cycle - if (strip.getBrightness()) { + handleOnOff(); +} + +void handleOnOff(bool forceOff) +{ + if (strip.getBrightness() && !forceOff) { lastOnTime = millis(); if (offMode) { BusManager::on(); if (rlyPin>=0) { - pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); - digitalWrite(rlyPin, rlyMde); + // note: pinMode is set in first call to handleOnOff(true) in beginStrip() + digitalWrite(rlyPin, rlyMde); // set to on state + delay(RELAY_DELAY); // let power stabilize before sending LED data (#346 #812 #3581 #3955) } offMode = false; } - } else if (millis() - lastOnTime > 600 && !strip.needsUpdate()) { + } else if ((millis() - lastOnTime > 600 && !strip.needsUpdate()) || forceOff) { // for turning LED or relay off we need to wait until strip no longer needs updates (strip.trigger()) if (!offMode) { BusManager::off(); if (rlyPin>=0) { + digitalWrite(rlyPin, !rlyMde); // set output before disabling high-z state to avoid output glitches pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); - digitalWrite(rlyPin, !rlyMde); } offMode = true; } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 3f6cfbacb6..4f131cab96 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -6,10 +6,39 @@ * The structure of the JSON is not to be considered an official API and may change without notice. */ +#ifndef PIXEL_COUNTS + #define PIXEL_COUNTS DEFAULT_LED_COUNT +#endif + +#ifndef DATA_PINS + #define DATA_PINS DEFAULT_LED_PIN +#endif + +#ifndef LED_TYPES + #define LED_TYPES DEFAULT_LED_TYPE +#endif + +#ifndef DEFAULT_LED_COLOR_ORDER + #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB +#endif + +static constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) { + return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0; +} + +static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) { + // Pins provided < pins required -> always invalid + // Pins provided = pins required -> always valid + // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated + return (sumPinsRequired(types, numTypes) > numPins) ? false : + (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; +} + + //simple macro for ArduinoJSON's or syntax #define CJSON(a,b) a = b | a -void getStringFromJson(char* dest, const char* src, size_t len) { +static inline void getStringFromJson(char* dest, const char* src, size_t len) { if (src != nullptr) strlcpy(dest, src, len); } @@ -20,13 +49,6 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { //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); @@ -38,8 +60,24 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject nw = doc["nw"]; #ifndef WLED_DISABLE_ESPNOW CJSON(enableESPNow, nw[F("espnow")]); - getStringFromJson(linked_remote, nw[F("linked_remote")], 13); - linked_remote[12] = '\0'; + linked_remotes.clear(); + JsonVariant lrem = nw[F("linked_remote")]; + if (!lrem.isNull()) { + if (lrem.is()) { + for (size_t i = 0; i < lrem.size(); i++) { + std::array entry{}; + getStringFromJson(entry.data(), lrem[i], 13); + entry[12] = '\0'; + linked_remotes.emplace_back(entry); + } + } + else { // legacy support for single MAC address in config + std::array entry{}; + getStringFromJson(entry.data(), lrem, 13); + entry[12] = '\0'; + linked_remotes.emplace_back(entry); + } + } #endif size_t n = 0; @@ -53,9 +91,11 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray sn = wifi["sn"]; char ssid[33] = ""; char pass[65] = ""; + char bssid[13] = ""; IPAddress nIP = (uint32_t)0U, nGW = (uint32_t)0U, nSN = (uint32_t)0x00FFFFFF; // little endian getStringFromJson(ssid, wifi[F("ssid")], 33); getStringFromJson(pass, wifi["psk"], 65); // password is not normally present but if it is, use it + getStringFromJson(bssid, wifi[F("bssid")], 13); for (size_t i = 0; i < 4; i++) { CJSON(nIP[i], ip[i]); CJSON(nGW[i], gw[i]); @@ -63,9 +103,21 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } if (strlen(ssid) > 0) strlcpy(multiWiFi[n].clientSSID, ssid, 33); // this will keep old SSID intact if not present in JSON if (strlen(pass) > 0) strlcpy(multiWiFi[n].clientPass, pass, 65); // this will keep old password intact if not present in JSON + if (strlen(bssid) > 0) fillStr2MAC(multiWiFi[n].bssid, bssid); multiWiFi[n].staticIP = nIP; multiWiFi[n].staticGW = nGW; multiWiFi[n].staticSN = nSN; +#ifdef WLED_ENABLE_WPA_ENTERPRISE + byte encType = WIFI_ENCRYPTION_TYPE_PSK; + char anonIdent[65] = ""; + char ident[65] = ""; + CJSON(encType, wifi[F("enc_type")]); + getStringFromJson(anonIdent, wifi["e_anon_ident"], 65); + getStringFromJson(ident, wifi["e_ident"], 65); + multiWiFi[n].encryptionType = encType; + strlcpy(multiWiFi[n].enterpriseAnonIdentity, anonIdent, 65); + strlcpy(multiWiFi[n].enterpriseIdentity, ident, 65); +#endif if (++n >= WLED_MAX_WIFI_COUNT) break; } } @@ -77,6 +129,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } } + // https://github.com/wled/WLED/issues/5247 +#ifdef WLED_USE_ETHERNET + JsonObject ethernet = doc[F("eth")]; + CJSON(ethernetType, ethernet["type"]); + // NOTE: Ethernet configuration takes priority over other use of pins + initEthernet(); +#endif + JsonObject ap = doc["ap"]; getStringFromJson(apSSID, ap[F("ssid")], 33); getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security @@ -114,22 +174,22 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(strip.correctWB, hw_led["cct"]); CJSON(strip.cctFromRgb, hw_led[F("cr")]); CJSON(cctICused, hw_led[F("ic")]); - CJSON(strip.cctBlending, hw_led[F("cb")]); - Bus::setCCTBlend(strip.cctBlending); + uint8_t cctBlending = hw_led[F("cb")] | Bus::getCCTBlend(); + Bus::setCCTBlend(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")]); + unsigned numPanels = matrix[F("mpc")] | 1; + numPanels = constrain(numPanels, 1, WLED_MAX_PANELS); strip.panel.clear(); JsonArray panels = matrix[F("panels")]; - int s = 0; + unsigned 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 + strip.panel.reserve(numPanels); // pre-allocate default 8x8 panels for (JsonObject pnl : panels) { WS2812FX::Panel p; CJSON(p.bottomStart, pnl["b"]); @@ -141,59 +201,22 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { 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 + if (++s >= numPanels) 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 + strip.panel.shrink_to_fit(); // release unused memory (just in case) + // cannot call strip.deserializeLedmap()/strip.setUpMatrix() here due to already locked JSON buffer + //if (!fromFS) doInit2D = true; // if called at boot (fromFS==true), WLED::beginStrip() will take care of setting up matrix } #endif + DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize()); JsonArray ins = hw_led["ins"]; - - if (fromFS || !ins.isNull()) { - DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), ESP.getFreeHeap()); + if (!ins.isNull()) { int s = 0; // bus iterator - if (fromFS) BusManager::removeAll(); // can't safely manipulate busses directly in network callback - unsigned mem = 0; - - // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) - bool useParallel = false; - #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) - unsigned digitalCount = 0; - unsigned maxLedsOnBus = 0; - unsigned maxChannels = 0; - for (JsonObject elm : ins) { - unsigned type = elm["type"] | TYPE_WS2812_RGB; - unsigned len = elm["len"] | DEFAULT_LED_COUNT; - if (!Bus::isDigital(type)) continue; - if (!Bus::is2Pin(type)) { - digitalCount++; - unsigned channels = Bus::getNumberOfChannels(type); - if (len > maxLedsOnBus) maxLedsOnBus = len; - if (channels > maxChannels) maxChannels = channels; - } - } - DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); - // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 - if (maxLedsOnBus <= 300 && digitalCount > 5) { - DEBUG_PRINTLN(F("Switching to parallel I2S.")); - useParallel = true; - BusManager::useParallelOutput(); - mem = BusManager::memUsage(maxChannels, maxLedsOnBus, 8); // use alternate memory calculation - } - #endif - for (JsonObject elm : ins) { - if (s >= WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES) break; - uint8_t pins[5] = {255, 255, 255, 255, 255}; + if (s >= WLED_MAX_BUSSES) break; // only counts physical buses + uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; JsonArray pinArr = elm["pin"]; if (pinArr.size() == 0) continue; //pins[0] = pinArr[0]; @@ -220,26 +243,97 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { maMax = 0; } ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh - if (fromFS) { - BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); - if (useParallel && s < 8) { - // if for some unexplained reason the above pre-calculation was wrong, update - unsigned memT = BusManager::memUsage(bc); // includes x8 memory allocation for parallel I2S - if (memT > mem) mem = memT; // if we have unequal LED count use the largest - } else - mem += BusManager::memUsage(bc); // includes global buffer - if (mem <= MAX_LED_MEMORY) if (BusManager::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, maPerLed, maMax); - doInitBusses = true; // finalization done in beginStrip() + uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S note: polybus may override this if driver is not available + + String host = elm[F("text")] | String(); + busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host); + doInitBusses = true; // finalization done in beginStrip() + if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want + } + } else if (fromFS) { + //if busses failed to load, add default (fresh install, FS issue, ...) + BusManager::removeAll(); + busConfigs.clear(); + + DEBUG_PRINTLN(F("No busses, init default")); + constexpr unsigned defDataTypes[] = {LED_TYPES}; + constexpr unsigned defDataPins[] = {DATA_PINS}; + constexpr unsigned defCounts[] = {PIXEL_COUNTS}; + constexpr unsigned defNumTypes = (sizeof(defDataTypes) / sizeof(defDataTypes[0])); + constexpr unsigned defNumPins = (sizeof(defDataPins) / sizeof(defDataPins[0])); + constexpr unsigned defNumCounts = (sizeof(defCounts) / sizeof(defCounts[0])); + + static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins), + "The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES"); + + unsigned pinsIndex = 0; + for (unsigned i = 0; i < WLED_MAX_BUSSES; i++) { + uint8_t defPin[OUTPUT_MAX_PINS]; + // if we have less types than requested outputs and they do not align, use last known type to set current type + unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1]; + unsigned busPins = Bus::getNumberOfPins(dataType); + + // if we need more pins than available all outputs have been configured + if (pinsIndex + busPins > defNumPins) break; + + // Assign all pins first so we can check for conflicts on this bus + for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j]; + + for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) { + bool validPin = true; + // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware + // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. + // Pin should not be already allocated, read/only or defined for current bus + while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { + if (validPin) { + DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); + defPin[j] = 1; // start with GPIO1 and work upwards + validPin = false; + } else if (defPin[j] < WLED_NUM_PINS) { + defPin[j]++; + } else { + DEBUG_PRINTLN(F("No available pins left! Can't configure output.")); + break; + } + // is the newly assigned pin already defined or used previously? + // try next in line until there are no clashes or we run out of pins + bool clash; + do { + clash = false; + // check for conflicts on current bus + for (const auto &pin : defPin) { + if (&pin != &defPin[j] && pin == defPin[j]) { + clash = true; + break; + } + } + // We already have a clash on current bus, no point checking next buses + if (!clash) { + // check for conflicts in defined pins + for (const auto &pin : defDataPins) { + if (pin == defPin[j]) { + clash = true; + break; + } + } + } + if (clash) defPin[j]++; + if (defPin[j] >= WLED_NUM_PINS) break; + } while (clash); + } } - s++; + pinsIndex += busPins; + + // if we have less counts than pins and they do not align, use last known count to set current count + unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; + unsigned start = 0; + // analog always has length 1 + if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; + busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, LED_MILLIAMPS_DEFAULT, ABL_MILLIAMPS_DEFAULT, 0); // driver=0 (RMT default) + doInitBusses = true; // finalization done in beginStrip() } - DEBUG_PRINTF_P(PSTR("LED buffer size: %uB\n"), mem); - DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); } - if (hw_led["rev"]) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus + if (hw_led["rev"] && BusManager::getNumBusses()) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus // read color order map configuration JsonArray hw_com = hw[F("com")]; @@ -261,99 +355,91 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray hw_btn_ins = btn_obj["ins"]; if (!hw_btn_ins.isNull()) { // deallocate existing button pins - for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) PinManager::deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button + for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button + buttons.clear(); // clear existing buttons unsigned s = 0; for (JsonObject btn : hw_btn_ins) { - CJSON(buttonType[s], btn["type"]); - int8_t pin = btn["pin"][0] | -1; + uint8_t type = btn["type"] | BTN_TYPE_NONE; + int8_t pin = btn["pin"][0] | -1; if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) { - btnPin[s] = pin; - #ifdef ARDUINO_ARCH_ESP32 + #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that analog button pin is a valid ADC gpio - if ((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) { - if (digitalPinToAnalogChannel(btnPin[s]) < 0) { + if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) { + if (digitalPinToAnalogChannel(pin) < 0) { // not an ADC analog pin - DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s); - btnPin[s] = -1; - PinManager::deallocatePin(pin,PinOwner::Button); + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), pin, s); + PinManager::deallocatePin(pin, PinOwner::Button); + pin = -1; + continue; } else { analogReadResolution(12); // see #4040 } - } - else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH)) - { - if (digitalPinToTouchChannel(btnPin[s]) < 0) { + } else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) { + if (digitalPinToTouchChannel(pin) < 0) { // not a touch pin - DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), btnPin[s], s); - btnPin[s] = -1; - PinManager::deallocatePin(pin,PinOwner::Button); + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), pin, s); + PinManager::deallocatePin(pin, PinOwner::Button); + pin = -1; + continue; } //if touch pin, enable the touch interrupt on ESP32 S2 & S3 #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so - else - { - touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) - } + else touchAttachInterrupt(pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) #endif - } - else - #endif + } else + #endif { + // regular buttons and switches if (disablePullUp) { - pinMode(btnPin[s], INPUT); + pinMode(pin, INPUT); } else { #ifdef ESP32 - pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); + pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); #else - pinMode(btnPin[s], INPUT_PULLUP); + pinMode(pin, INPUT_PULLUP); #endif } } - } else { - btnPin[s] = -1; + JsonArray hw_btn_ins_0_macros = btn["macros"]; + uint8_t press = hw_btn_ins_0_macros[0] | 0; + uint8_t longPress = hw_btn_ins_0_macros[1] | 0; + uint8_t doublePress = hw_btn_ins_0_macros[2] | 0; + buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector } - JsonArray hw_btn_ins_0_macros = btn["macros"]; - CJSON(macroButton[s], hw_btn_ins_0_macros[0]); - CJSON(macroLongPress[s],hw_btn_ins_0_macros[1]); - CJSON(macroDoublePress[s], hw_btn_ins_0_macros[2]); if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached } - // clear remaining buttons - for (; s= 0) { - if (disablePullUp) { - pinMode(btnPin[s], INPUT); - } else { - #ifdef ESP32 - pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); - #else - pinMode(btnPin[s], INPUT_PULLUP); - #endif - } - } - macroButton[s] = 0; - macroLongPress[s] = 0; - macroDoublePress[s] = 0; + // relies upon only being called once with fromFS == true, which is currently true. + constexpr uint8_t defTypes[] = {BTNTYPE}; + constexpr int8_t defPins[] = {BTNPIN}; + constexpr unsigned numTypes = (sizeof(defTypes) / sizeof(defTypes[0])); + constexpr unsigned numPins = (sizeof(defPins) / sizeof(defPins[0])); + // check if the number of pins and types are valid; count of pins must be greater than or equal to types + static_assert(numTypes <= numPins, "The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE"); + + uint8_t type = BTN_TYPE_NONE; + buttons.clear(); // clear existing buttons (just in case) + for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) { + type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins + if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) { + if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined) + continue; // pin not available or invalid, skip configuring this GPIO } + if (disablePullUp) { + pinMode(defPins[s], INPUT); + } else { + #ifdef ESP32 + pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); + #else + pinMode(defPins[s], INPUT_PULLUP); + #endif + } + buttons.emplace_back(defPins[s], type); // add button to vector } } - CJSON(buttonPublishMqtt,btn_obj["mqtt"]); + CJSON(buttonPublishMqtt, btn_obj["mqtt"]); #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 @@ -426,31 +512,27 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject light = doc[F("light")]; CJSON(briMultiplier, light[F("scale-bri")]); - CJSON(strip.paletteBlend, light[F("pal-mode")]); + CJSON(paletteBlend, light[F("pal-mode")]); CJSON(strip.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 { + CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.2 + float light_gc_bri = light["gc"]["bri"] | 1.0f; // default to 1.0 (false) + float light_gc_col = light["gc"]["col"] | gammaCorrectVal; // default to gammaCorrectVal (true) + if (light_gc_bri != 1.0f) gammaCorrectBri = true; + else gammaCorrectBri = false; + if (light_gc_col != 1.0f) gammaCorrectCol = true; + else gammaCorrectCol = false; + if (gammaCorrectVal < 0.1f || gammaCorrectVal > 3) { gammaCorrectVal = 1.0f; // no gamma correction gammaCorrectBri = false; gammaCorrectCol = false; } + NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables JsonObject light_tr = light["tr"]; - CJSON(fadeTransition, light_tr["mode"]); - CJSON(modeBlending, light_tr["fx"]); int tdd = light_tr["dur"] | -1; if (tdd >= 0) transitionDelay = transitionDelayDefault = tdd * 100; - strip.setTransition(fadeTransition ? transitionDelayDefault : 0); - CJSON(strip.paletteFade, light_tr["pal"]); + strip.setTransition(transitionDelayDefault); CJSON(randomPaletteChangeTime, light_tr[F("rpc")]); CJSON(useHarmonicRandomPalette, light_tr[F("hrp")]); @@ -523,6 +605,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { tdd = if_live[F("timeout")] | -1; if (tdd >= 0) realtimeTimeoutMs = tdd * 100; + + #ifdef WLED_ENABLE_DMX_INPUT + CJSON(dmxInputTransmitPin, if_live_dmx[F("inputRxPin")]); + CJSON(dmxInputReceivePin, if_live_dmx[F("inputTxPin")]); + CJSON(dmxInputEnablePin, if_live_dmx[F("inputEnablePin")]); + CJSON(dmxInputPort, if_live_dmx[F("dmxInputPort")]); + #endif + CJSON(arlsForceMaxBri, if_live[F("maxbri")]); CJSON(arlsDisableGammaCorrection, if_live[F("no-gc")]); // false CJSON(arlsOffset, if_live[F("offset")]); // 0 @@ -641,8 +731,11 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { 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")]); + #ifndef WLED_DISABLE_OTA CJSON(aOtaEnabled, ota[F("aota")]); + #endif getStringFromJson(otaPass, pwd, 33); //normally not present due to security + CJSON(otaSameSubnet, ota[F("same-subnet")]); } #ifdef WLED_ENABLE_DMX @@ -674,40 +767,44 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { return (doc["sv"] | true); } - static const char s_cfg_json[] PROGMEM = "/cfg.json"; -void deserializeConfigFromFS() { - bool success = deserializeConfigSec(); - #ifdef WLED_ADD_EEPROM_SUPPORT - if (!success) { //if file does not exist, try reading from EEPROM - deEEPSettings(); - return; +bool backupConfig() { + return backupFile(s_cfg_json); +} + +bool restoreConfig() { + return restoreFile(s_cfg_json); +} + +bool verifyConfig() { + return validateJsonFile(s_cfg_json); +} + +bool configBackupExists() { + return checkBackupExists(s_cfg_json); +} + +// rename config file and reboot +// if the cfg file doesn't exist, such as after a reset, do nothing +void resetConfig() { + if (WLED_FS.exists(s_cfg_json)) { + DEBUG_PRINTLN(F("Reset config")); + char backupname[32]; + snprintf_P(backupname, sizeof(backupname), PSTR("/rst.%s"), &s_cfg_json[1]); + WLED_FS.rename(s_cfg_json, backupname); + doReboot = true; } - #endif +} + +bool deserializeConfigFromFS() { + [[maybe_unused]] bool success = deserializeConfigSec(); - if (!requestJSONBufferLock(1)) return; + if (!requestJSONBufferLock(JSON_LOCK_CFG_DES)) return false; DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); success = readObjectFromFile(s_cfg_json, nullptr, pDoc); - 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(); - UsermodManager::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 @@ -715,18 +812,30 @@ void deserializeConfigFromFS() { bool needsSave = deserializeConfig(root, true); releaseJSONBufferLock(); - if (needsSave) serializeConfig(); // usermods required new parameters + return needsSave; } -void serializeConfig() { +void serializeConfigToFS() { serializeConfigSec(); + backupConfig(); // backup before writing new config DEBUG_PRINTLN(F("Writing settings to /cfg.json...")); - if (!requestJSONBufferLock(2)) return; + if (!requestJSONBufferLock(JSON_LOCK_CFG_SER)) return; JsonObject root = pDoc->to(); + serializeConfig(root); + + File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); + if (f) serializeJson(root, f); + f.close(); + releaseJSONBufferLock(); + + configNeedsWrite = false; +} + +void serializeConfig(JsonObject root) { JsonArray rev = root.createNestedArray("rev"); rev.add(1); //major settings revision rev.add(0); //minor settings revision @@ -744,7 +853,10 @@ void serializeConfig() { JsonObject nw = root.createNestedObject("nw"); #ifndef WLED_DISABLE_ESPNOW nw[F("espnow")] = enableESPNow; - nw[F("linked_remote")] = linked_remote; + JsonArray lrem = nw.createNestedArray(F("linked_remote")); + for (size_t i = 0; i < linked_remotes.size(); i++) { + lrem.add(linked_remotes[i].data()); + } #endif JsonArray nw_ins = nw.createNestedArray("ins"); @@ -752,6 +864,9 @@ void serializeConfig() { JsonObject wifi = nw_ins.createNestedObject(); wifi[F("ssid")] = multiWiFi[n].clientSSID; wifi[F("pskl")] = strlen(multiWiFi[n].clientPass); + char bssid[13]; + fillMAC2Str(bssid, multiWiFi[n].bssid); + wifi[F("bssid")] = bssid; JsonArray wifi_ip = wifi.createNestedArray("ip"); JsonArray wifi_gw = wifi.createNestedArray("gw"); JsonArray wifi_sn = wifi.createNestedArray("sn"); @@ -760,6 +875,13 @@ void serializeConfig() { wifi_gw.add(multiWiFi[n].staticGW[i]); wifi_sn.add(multiWiFi[n].staticSN[i]); } +#ifdef WLED_ENABLE_WPA_ENTERPRISE + wifi[F("enc_type")] = multiWiFi[n].encryptionType; + if (multiWiFi[n].encryptionType == WIFI_ENCRYPTION_TYPE_ENTERPRISE) { + wifi[F("e_anon_ident")] = multiWiFi[n].enterpriseAnonIdentity; + wifi[F("e_ident")] = multiWiFi[n].enterpriseIdentity; + } +#endif } JsonArray dns = nw.createNestedArray(F("dns")); @@ -787,7 +909,7 @@ void serializeConfig() { wifi[F("txpwr")] = txPower; #endif -#ifdef WLED_USE_ETHERNET +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) JsonObject ethernet = root.createNestedObject("eth"); ethernet["type"] = ethernetType; if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { @@ -816,20 +938,19 @@ void serializeConfig() { JsonObject hw_led = hw.createNestedObject("led"); hw_led[F("total")] = strip.getLengthTotal(); //provided for compatibility on downgrade and per-output ABL hw_led[F("maxpwr")] = BusManager::ablMilliampsMax(); - hw_led[F("ledma")] = 0; // no longer used +// hw_led[F("ledma")] = 0; // no longer used hw_led["cct"] = strip.correctWB; hw_led[F("cr")] = strip.cctFromRgb; hw_led[F("ic")] = cctICused; - hw_led[F("cb")] = strip.cctBlending; + hw_led[F("cb")] = Bus::getCCTBlend(); 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; + matrix[F("mpc")] = strip.panel.size(); JsonArray panels = matrix.createNestedArray(F("panels")); for (size_t i = 0; i < strip.panel.size(); i++) { JsonObject pnl = panels.createNestedObject(); @@ -848,32 +969,44 @@ void serializeConfig() { JsonArray hw_led_ins = hw_led.createNestedArray("ins"); for (size_t s = 0; s < BusManager::getNumBusses(); s++) { - Bus *bus = BusManager::getBus(s); - if (!bus || bus->getLength()==0) break; + DEBUG_PRINTF_P(PSTR("Cfg: Saving bus #%u\n"), s); + const Bus *bus = BusManager::getBus(s); + if (!bus) break; // Memory corruption, iterator invalid + DEBUG_PRINTF_P(PSTR(" (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"), + (int)bus->getStart(), (int)(bus->getStart()+bus->getLength()), + (int)(bus->getType() & 0x7F), + (int)bus->getColorOrder(), + (int)bus->isReversed(), + (int)bus->skippedLeds(), + (int)bus->getAutoWhiteMode(), + (int)bus->getFrequency(), + (int)bus->getLEDCurrent(), (int)bus->getMaxCurrent() + ); JsonObject ins = hw_led_ins.createNestedObject(); ins["start"] = bus->getStart(); - ins["len"] = bus->getLength(); + ins["len"] = bus->getLength(); JsonArray ins_pin = ins.createNestedArray("pin"); uint8_t pins[5]; uint8_t nPins = bus->getPins(pins); for (int 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(); + 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(); ins[F("maxpwr")] = bus->getMaxCurrent(); - ins[F("ledma")] = bus->getLEDCurrent(); + ins[F("ledma")] = bus->getLEDCurrent(); + ins[F("drv")] = bus->getDriverType(); + ins[F("text")] = bus->getCustomText(); } JsonArray hw_com = hw.createNestedArray(F("com")); const ColorOrderMap& com = BusManager::getColorOrderMap(); for (size_t s = 0; s < com.count(); s++) { const ColorOrderMapEntry *entry = com.get(s); - if (!entry) break; - + if (!entry || !entry->len) break; JsonObject co = hw_com.createNestedObject(); co["start"] = entry->start; co["len"] = entry->len; @@ -887,15 +1020,15 @@ void serializeConfig() { JsonArray hw_btn_ins = hw_btn.createNestedArray("ins"); // configuration for all buttons - for (int i = 0; i < WLED_MAX_BUTTONS; i++) { + for (const auto &button : buttons) { JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject(); - hw_btn_ins_0["type"] = buttonType[i]; + hw_btn_ins_0["type"] = button.type; JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin"); - hw_btn_ins_0_pin.add(btnPin[i]); + hw_btn_ins_0_pin.add(button.pin); JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros"); - hw_btn_ins_0_macros.add(macroButton[i]); - hw_btn_ins_0_macros.add(macroLongPress[i]); - hw_btn_ins_0_macros.add(macroDoublePress[i]); + hw_btn_ins_0_macros.add(button.macroButton); + hw_btn_ins_0_macros.add(button.macroLongPress); + hw_btn_ins_0_macros.add(button.macroDoublePress); } hw_btn[F("tt")] = touchThreshold; @@ -929,7 +1062,7 @@ void serializeConfig() { JsonObject light = root.createNestedObject(F("light")); light[F("scale-bri")] = briMultiplier; - light[F("pal-mode")] = strip.paletteBlend; + light[F("pal-mode")] = paletteBlend; light[F("aseg")] = strip.autoSegments; JsonObject light_gc = light.createNestedObject("gc"); @@ -938,10 +1071,7 @@ void serializeConfig() { light_gc["val"] = gammaCorrectVal; JsonObject light_tr = light.createNestedObject("tr"); - light_tr["mode"] = fadeTransition; - light_tr["fx"] = modeBlending; light_tr["dur"] = transitionDelayDefault / 100; - light_tr["pal"] = strip.paletteFade; light_tr[F("rpc")] = randomPaletteChangeTime; light_tr[F("hrp")] = useHarmonicRandomPalette; @@ -1002,6 +1132,12 @@ void serializeConfig() { if_live_dmx[F("addr")] = DMXAddress; if_live_dmx[F("dss")] = DMXSegmentSpacing; if_live_dmx["mode"] = DMXMode; + #ifdef WLED_ENABLE_DMX_INPUT + if_live_dmx[F("inputRxPin")] = dmxInputTransmitPin; + if_live_dmx[F("inputTxPin")] = dmxInputReceivePin; + if_live_dmx[F("inputEnablePin")] = dmxInputEnablePin; + if_live_dmx[F("dmxInputPort")] = dmxInputPort; + #endif if_live[F("timeout")] = realtimeTimeoutMs / 100; if_live[F("maxbri")] = arlsForceMaxBri; @@ -1103,7 +1239,10 @@ void serializeConfig() { ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; ota[F("pskl")] = strlen(otaPass); + #ifndef WLED_DISABLE_OTA ota[F("aota")] = aOtaEnabled; + #endif + ota[F("same-subnet")] = otaSameSubnet; #ifdef WLED_ENABLE_DMX JsonObject dmx = root.createNestedObject("dmx"); @@ -1122,13 +1261,6 @@ void serializeConfig() { JsonObject usermods_settings = root.createNestedObject("um"); UsermodManager::addToConfig(usermods_settings); - - File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); - if (f) serializeJson(root, f); - f.close(); - releaseJSONBufferLock(); - - doSerializeConfig = false; } @@ -1138,7 +1270,7 @@ static const char s_wsec_json[] PROGMEM = "/wsec.json"; bool deserializeConfigSec() { DEBUG_PRINTLN(F("Reading settings from /wsec.json...")); - if (!requestJSONBufferLock(3)) return false; + if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_DES)) return false; bool success = readObjectFromFile(s_wsec_json, nullptr, pDoc); if (!success) { @@ -1181,7 +1313,9 @@ bool deserializeConfigSec() { getStringFromJson(otaPass, ota[F("pwd")], 33); CJSON(otaLock, ota[F("lock")]); CJSON(wifiLock, ota[F("lock-wifi")]); + #ifndef WLED_DISABLE_OTA CJSON(aOtaEnabled, ota[F("aota")]); + #endif releaseJSONBufferLock(); return true; @@ -1190,7 +1324,7 @@ bool deserializeConfigSec() { void serializeConfigSec() { DEBUG_PRINTLN(F("Writing settings to /wsec.json...")); - if (!requestJSONBufferLock(4)) return; + if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_SER)) return; JsonObject root = pDoc->to(); @@ -1221,7 +1355,9 @@ void serializeConfigSec() { ota[F("pwd")] = otaPass; ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; + #ifndef WLED_DISABLE_OTA ota[F("aota")] = aOtaEnabled; + #endif File f = WLED_FS.open(FPSTR(s_wsec_json), "w"); if (f) serializeJson(root, f); diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 478a0a277d..8ecde0820b 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -5,90 +5,129 @@ */ /* - * color blend function + * color blend function, based on FastLED blend function + * the calculation for each color is: result = (A*(amountOfA) + A + B*(amountOfB) + B) / 256 with amountOfA = 255 - amountOfB */ -uint32_t color_blend(uint32_t color1, uint32_t color2, uint16_t blend, bool b16) { - if (blend == 0) return color1; - unsigned blendmax = b16 ? 0xFFFF : 0xFF; - if (blend == blendmax) return color2; - unsigned 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); +uint32_t WLED_O2_ATTR IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend) { + // min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance + const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated (poorman's SIMD; https://github.com/wled/WLED/pull/4568#discussion_r1986587221) + uint32_t rb1 = color1 & TWO_CHANNEL_MASK; // extract R & B channels from color1 + uint32_t wg1 = (color1 >> 8) & TWO_CHANNEL_MASK; // extract W & G channels from color1 (shifted for multiplication later) + uint32_t rb2 = color2 & TWO_CHANNEL_MASK; // extract R & B channels from color2 + uint32_t wg2 = (color2 >> 8) & TWO_CHANNEL_MASK; // extract W & G channels from color2 (shifted for multiplication later) + uint32_t rb3 = ((((rb1 << 8) | rb2) + (rb2 * blend) - (rb1 * blend)) >> 8) & TWO_CHANNEL_MASK; // blend red and blue + uint32_t wg3 = ((((wg1 << 8) | wg2) + (wg2 * blend) - (wg1 * blend))) & ~TWO_CHANNEL_MASK; // negated mask for white and green + return rb3 | wg3; } /* * color add function that preserves ratio - * idea: https://github.com/Aircoookie/WLED/pull/2465 by https://github.com/Proto-molecule + * original idea: https://github.com/wled-dev/WLED/pull/2465 by https://github.com/Proto-molecule + * speed optimisations by @dedehai */ -uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) +uint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR) //1212558 | 1212598 | 1212576 | 1212530 { if (c1 == BLACK) return c2; if (c2 == BLACK) return c1; - if (fast) { - uint8_t r = R(c1); - uint8_t g = G(c1); - uint8_t b = B(c1); - uint8_t w = W(c1); - r = qadd8(r, R(c2)); - g = qadd8(g, G(c2)); - b = qadd8(b, B(c2)); - w = qadd8(w, W(c2)); - return RGBW32(r,g,b,w); + const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated + uint32_t rb = ( c1 & TWO_CHANNEL_MASK) + ( c2 & TWO_CHANNEL_MASK); // mask and add two colors at once + uint32_t wg = ((c1>>8) & TWO_CHANNEL_MASK) + ((c2>>8) & TWO_CHANNEL_MASK); + + if (preserveCR) { // preserve color ratios + uint32_t overflow = (rb | wg) & 0x01000100; // detect overflow by checking 9th bit + if (overflow) { + uint32_t r = rb >> 16; // extract single color values + uint32_t b = rb & 0xFFFF; + uint32_t w = wg >> 16; + uint32_t g = wg & 0xFFFF; + uint32_t max = std::max(r,g); + max = std::max(max,b); + max = std::max(max,w); + const uint32_t scale = (uint32_t(255)<<8) / max; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead + rb = ((rb * scale) >> 8) & TWO_CHANNEL_MASK; + wg = (wg * scale) & ~TWO_CHANNEL_MASK; + } else wg <<= 8; //shift white and green back to correct position } else { - uint32_t r = R(c1) + R(c2); - uint32_t g = G(c1) + G(c2); - uint32_t b = B(c1) + B(c2); - uint32_t w = W(c1) + W(c2); - unsigned 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); + // branchless per-channel saturation to 255 (extract 9th bit, subtract 1 if it is set, mask with 0xFF, input is 0xFF+0xFF=0x1EF max) + // example with overflow: input: 0x01EF01EF -> (0x0100100 - 0x00010001) = 0x00FF00FF -> input|0x00FF00FF = 0x00FF00FF (saturate) + // example without overflow: input: 0x007F007F -> (0x00000000 - 0x00000000) = 0x00000000 -> input|0x00000000 = input (no change) + rb |= ((rb & 0x01000100) - ((rb >> 8) & 0x00010001)) & 0x00FF00FF; + wg |= ((wg & 0x01000100) - ((wg >> 8) & 0x00010001)) & 0x00FF00FF; + wg <<= 8; // restore WG position } + return rb | wg; } /* * fades color toward black * if using "video" method the resulting color will never become black unless it is already black */ +uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) { + if (c1 == BLACK || amount == 0) return 0; // black or full fade + if (amount == 255) return c1; // no change + uint32_t addRemains = 0; + + if (!video) amount++; // add one for correct scaling using bitshifts + else { + // video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue + uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels + uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation + addRemains = r && (r<<5) > maxc ? 0x00010000 : 0; // note: setting color preservation threshold too high results in flickering and + addRemains |= g && (g<<5) > maxc ? 0x00000100 : 0; // jumping colors in low brightness gradients. Multiplying the color preserves + addRemains |= b && (b<<5) > maxc ? 0x00000001 : 0; // better accuracy than dividing the maxc. Shifting by 5 is a good compromise + addRemains |= w ? 0x01000000 : 0; // i.e. remove color channel if <13% of max + } + const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; + uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue + uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK; // scale white and green + return (rb | wg) + addRemains; +} -uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) -{ - if (c1 == BLACK || amount + video == 0) return BLACK; - uint32_t scaledcolor; // color order is: W R G B from MSB to LSB - uint32_t r = R(c1); - uint32_t g = G(c1); - uint32_t b = B(c1); - uint32_t w = W(c1); - uint32_t scale = amount; // 32bit for faster calculation - if (video) { - scaledcolor = (((r * scale) >> 8) + ((r && scale) ? 1 : 0)) << 16; - scaledcolor |= (((g * scale) >> 8) + ((g && scale) ? 1 : 0)) << 8; - scaledcolor |= ((b * scale) >> 8) + ((b && scale) ? 1 : 0); - scaledcolor |= (((w * scale) >> 8) + ((w && scale) ? 1 : 0)) << 24; - } else { - scaledcolor = ((r * scale) >> 8) << 16; - scaledcolor |= ((g * scale) >> 8) << 8; - scaledcolor |= (b * scale) >> 8; - scaledcolor |= ((w * scale) >> 8) << 24; +/* + * color adjustment in HSV color space (converts RGB to HSV and back), color conversions are not 100% accurate! + shifts hue, increase brightness, decreases saturation (if not black) + note: inputs are 32bit to speed up the function, useful input value ranges are 0-255 + */ +uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten) { + if (rgb == 0 || hueShift + lighten + brighten == 0) return rgb; // black or no change + CHSV32 hsv; + rgb2hsv(rgb, hsv); //convert to HSV + hsv.h += (hueShift << 8); // shift hue (hue is 16 bits) + hsv.s = max((int32_t)0, (int32_t)hsv.s - (int32_t)lighten); // desaturate + hsv.v = min((uint32_t)255, (uint32_t)hsv.v + brighten); // increase brightness + uint32_t rgb_adjusted; + hsv2rgb(hsv, rgb_adjusted); // convert back to RGB TODO: make this into 16 bit conversion + return rgb_adjusted; +} + +// 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes) +uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) { + if (blendType == LINEARBLEND_NOWRAP) { + index = (index * 0xF0) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping + } + unsigned hi4 = byte(index) >> 4; + unsigned lo4 = (index & 0x0F); + const CRGB* entry = (CRGB*)&(pal[0]) + hi4; + unsigned red1 = entry->r; + unsigned green1 = entry->g; + unsigned blue1 = entry->b; + if (lo4 && blendType != NOBLEND) { + if (hi4 == 15) entry = &(pal[0]); + else ++entry; + unsigned f2 = (lo4 << 4); + unsigned f1 = 256 - f2; + red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8; // note: using color_blend() is slower + green1 = (green1 * f1 + (unsigned)entry->g * f2) >> 8; + blue1 = (blue1 * f1 + (unsigned)entry->b * f2) >> 8; } - return scaledcolor; + if (brightness < 255) { // note: zero checking could be done to return black but that is hardly ever used so it is omitted + // actually same as color_fade(), using color_fade() is slower + uint32_t scale = brightness + 1; // adjust for rounding (bitshift) + red1 = (red1 * scale) >> 8; + green1 = (green1 * scale) >> 8; + blue1 = (blue1 * scale) >> 8; + } + return RGBW32(red1,green1,blue1,0); } void setRandomColor(byte* rgb) @@ -101,95 +140,95 @@ void setRandomColor(byte* rgb) * generates a random palette based on harmonic color theory * takes a base palette as the input, it will choose one color of the base palette and keep it */ -CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette) +CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette) { - CHSV palettecolors[4]; //array of colors for the new palette - uint8_t keepcolorposition = random8(4); //color position of current random palette to keep - palettecolors[keepcolorposition] = rgb2hsv_approximate(basepalette.entries[keepcolorposition*5]); //read one of the base colors of the current palette - palettecolors[keepcolorposition].hue += random8(10)-5; // +/- 5 randomness of base color - //generate 4 saturation and brightness value numbers - //only one saturation is allowed to be below 200 creating mostly vibrant colors - //only one brightness value number is allowed below 200, creating mostly bright palettes - - for (int i = 0; i < 3; i++) { //generate three high values - palettecolors[i].saturation = random8(200,255); - palettecolors[i].value = random8(220,255); + CHSV palettecolors[4]; // array of colors for the new palette + uint8_t keepcolorposition = hw_random8(4); // color position of current random palette to keep + palettecolors[keepcolorposition] = rgb2hsv(basepalette.entries[keepcolorposition*5]); // read one of the base colors of the current palette + palettecolors[keepcolorposition].hue += hw_random8(10)-5; // +/- 5 randomness of base color + // generate 4 saturation and brightness value numbers + // only one saturation is allowed to be below 200 creating mostly vibrant colors + // only one brightness value number is allowed below 200, creating mostly bright palettes + + for (int i = 0; i < 3; i++) { // generate three high values + palettecolors[i].saturation = hw_random8(200,255); + palettecolors[i].value = hw_random8(220,255); } - //allow one to be lower - palettecolors[3].saturation = random8(20,255); - palettecolors[3].value = random8(80,255); + // allow one to be lower + palettecolors[3].saturation = hw_random8(20,255); + palettecolors[3].value = hw_random8(80,255); - //shuffle the arrays + // shuffle the arrays for (int i = 3; i > 0; i--) { - std::swap(palettecolors[i].saturation, palettecolors[random8(i + 1)].saturation); - std::swap(palettecolors[i].value, palettecolors[random8(i + 1)].value); + std::swap(palettecolors[i].saturation, palettecolors[hw_random8(i + 1)].saturation); + std::swap(palettecolors[i].value, palettecolors[hw_random8(i + 1)].value); } - //now generate three new hues based off of the hue of the chosen current color + // now generate three new hues based off of the hue of the chosen current color uint8_t basehue = palettecolors[keepcolorposition].hue; - uint8_t harmonics[3]; //hues that are harmonic but still a little random - uint8_t type = random8(5); //choose a harmony type + uint8_t harmonics[3]; // hues that are harmonic but still a little random + uint8_t type = hw_random8(5); // choose a harmony type switch (type) { case 0: // analogous - harmonics[0] = basehue + random8(30, 50); - harmonics[1] = basehue + random8(10, 30); - harmonics[2] = basehue - random8(10, 30); + harmonics[0] = basehue + hw_random8(30, 50); + harmonics[1] = basehue + hw_random8(10, 30); + harmonics[2] = basehue - hw_random8(10, 30); break; case 1: // triadic - harmonics[0] = basehue + 113 + random8(15); - harmonics[1] = basehue + 233 + random8(15); - harmonics[2] = basehue - 7 + random8(15); + harmonics[0] = basehue + 113 + hw_random8(15); + harmonics[1] = basehue + 233 + hw_random8(15); + harmonics[2] = basehue - 7 + hw_random8(15); break; case 2: // split-complementary - harmonics[0] = basehue + 145 + random8(10); - harmonics[1] = basehue + 205 + random8(10); - harmonics[2] = basehue - 5 + random8(10); + harmonics[0] = basehue + 145 + hw_random8(10); + harmonics[1] = basehue + 205 + hw_random8(10); + harmonics[2] = basehue - 5 + hw_random8(10); break; - + case 3: // square - harmonics[0] = basehue + 85 + random8(10); - harmonics[1] = basehue + 175 + random8(10); - harmonics[2] = basehue + 265 + random8(10); + harmonics[0] = basehue + 85 + hw_random8(10); + harmonics[1] = basehue + 175 + hw_random8(10); + harmonics[2] = basehue + 265 + hw_random8(10); break; case 4: // tetradic - harmonics[0] = basehue + 80 + random8(20); - harmonics[1] = basehue + 170 + random8(20); - harmonics[2] = basehue - 15 + random8(30); + harmonics[0] = basehue + 80 + hw_random8(20); + harmonics[1] = basehue + 170 + hw_random8(20); + harmonics[2] = basehue - 15 + hw_random8(30); break; } - if (random8() < 128) { - //50:50 chance of shuffling hues or keep the color order + if (hw_random8() < 128) { + // 50:50 chance of shuffling hues or keep the color order for (int i = 2; i > 0; i--) { - std::swap(harmonics[i], harmonics[random8(i + 1)]); + std::swap(harmonics[i], harmonics[hw_random8(i + 1)]); } } - //now set the hues + // now set the hues int j = 0; for (int i = 0; i < 4; i++) { - if (i==keepcolorposition) continue; //skip the base color + if (i==keepcolorposition) continue; // skip the base color palettecolors[i].hue = harmonics[j]; j++; } bool makepastelpalette = false; - if (random8() < 25) { //~10% chance of desaturated 'pastel' colors + if (hw_random8() < 25) { // ~10% chance of desaturated 'pastel' colors makepastelpalette = true; } - //apply saturation & gamma correction + // apply saturation CRGB RGBpalettecolors[4]; for (int i = 0; i < 4; i++) { - if (makepastelpalette && palettecolors[i].saturation > 180) { + if (makepastelpalette && palettecolors[i].saturation > 180) { palettecolors[i].saturation -= 160; //desaturate all four colors - } + } RGBpalettecolors[i] = (CRGB)palettecolors[i]; //convert to RGB - RGBpalettecolors[i] = gamma32(((uint32_t)RGBpalettecolors[i]) & 0x00FFFFFFU); //strip alpha from CRGB + RGBpalettecolors[i] = ((uint32_t)RGBpalettecolors[i]) & 0x00FFFFFFU; //strip alpha from CRGB } return CRGBPalette16(RGBpalettecolors[0], @@ -198,36 +237,124 @@ CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette) RGBpalettecolors[3]); } -CRGBPalette16 generateRandomPalette() //generate fully random palette +CRGBPalette16 generateRandomPalette() // generate fully random palette { - return 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))); + return CRGBPalette16(CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), + CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), + CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), + CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255))); } -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) //hue, sat to rgb +void loadCustomPalettes() { + byte tcp[72]; //support gradient palettes with up to 18 entries + CRGBPalette16 targetPalette; + customPalettes.clear(); // start fresh + StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers -> TODO: current format uses 214 bytes max per palette, why is this buffer so large? + unsigned emptyPaletteGap = 0; // count gaps in palette files to stop looking for more (each exists() call takes ~5ms) + for (int index = 0; index < WLED_MAX_CUSTOM_PALETTES; index++) { + char fileName[32]; + sprintf_P(fileName, PSTR("/palette%d.json"), index); + if (WLED_FS.exists(fileName)) { + emptyPaletteGap = 0; // reset gap counter if file exists + DEBUGFX_PRINTF_P(PSTR("Reading palette from %s\n"), fileName); + if (readObjectFromFile(fileName, nullptr, &pDoc)) { + JsonArray pal = pDoc[F("palette")]; + if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) + memset(tcp, 255, sizeof(tcp)); + 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) { + uint8_t rgbw[] = {0,0,0,0}; + if (colorFromHexString(rgbw, pal[i+1].as())) { // will catch non-string entires + tcp[ j ] = (uint8_t) pal[ i ].as(); // index + for (size_t c=0; c<3; c++) tcp[j+1+c] = rgbw[c]; // only use RGB component + DEBUGFX_PRINTF_P(PSTR("%2u -> %3d [%3d,%3d,%3d]\n"), i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); + j += 4; + } + } + } 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 + for (size_t c=0; c<3; c++) tcp[i+1+c] = (uint8_t) pal[i+1+c].as(); + DEBUGFX_PRINTF_P(PSTR("%2u -> %3d [%3d,%3d,%3d]\n"), i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); + } + } + customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); + } else { + DEBUGFX_PRINTLN(F("Wrong palette format.")); + } + } + } else { + emptyPaletteGap++; + if (emptyPaletteGap > WLED_MAX_CUSTOM_PALETTE_GAP) break; // stop looking for more palettes + } + } +} + +void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0) { - float h = ((float)hue)/10922.5f; // hue*6/65535 - float s = ((float)sat)/255.0f; - int i = int(h); - float f = h - i; - int p = int(255.0f * (1.0f-s)); - int q = int(255.0f * (1.0f-s*f)); - int t = int(255.0f * (1.0f-s*(1.0f-f))); - 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; break; + unsigned int remainder, region, p, q, t; + unsigned int h = hsv.h; + unsigned int s = hsv.s; + unsigned int v = hsv.v; + if (s == 0) { + rgb = v << 16 | v << 8 | v; + return; + } + region = h / 10923; // 65536 / 6 = 10923 + remainder = (h - (region * 10923)) * 6; + p = (v * (255 - s)) >> 8; + q = (v * (255 - ((s * remainder) >> 16))) >> 8; + t = (v * (255 - ((s * (65535 - remainder)) >> 16))) >> 8; + switch (region) { + case 0: + rgb = v << 16 | t << 8 | p; break; + case 1: + rgb = q << 16 | v << 8 | p; break; + case 2: + rgb = p << 16 | v << 8 | t; break; + case 3: + rgb = p << 16 | q << 8 | v; break; + case 4: + rgb = t << 16 | p << 8 | v; break; + default: + rgb = v << 16 | p << 8 | q; break; } } +void rgb2hsv(const uint32_t rgb, CHSV32& hsv) // convert RGB to HSV (16bit hue), much more accurate and faster than fastled version +{ + hsv.raw = 0; + int32_t r = (rgb>>16)&0xFF; + int32_t g = (rgb>>8)&0xFF; + int32_t b = rgb&0xFF; + int32_t minval, maxval, delta; + minval = min(r, g); + minval = min(minval, b); + maxval = max(r, g); + maxval = max(maxval, b); + if (maxval == 0) return; // black + hsv.v = maxval; + delta = maxval - minval; + hsv.s = (255 * delta) / maxval; + if (hsv.s == 0) return; // gray value + if (maxval == r) hsv.h = (10923 * (g - b)) / delta; + else if (maxval == g) hsv.h = 21845 + (10923 * (b - r)) / delta; + else hsv.h = 43690 + (10923 * (r - g)) / delta; +} + +void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) { //hue, sat to rgb + uint32_t crgb; + hsv2rgb(CHSV32(hue, sat, 255), crgb); + rgb[0] = byte((crgb) >> 16); + rgb[1] = byte((crgb) >> 8); + rgb[2] = byte(crgb); +} + //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 { @@ -332,7 +459,7 @@ void colorXYtoRGB(float x, float y, byte* rgb) //coordinates to rgb (https://www 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) +void colorRGBtoXY(const byte* rgb, float* xy) //rgb to coordinates (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy) { float X = rgb[0] * 0.664511f + rgb[1] * 0.154324f + rgb[2] * 0.162028f; float Y = rgb[0] * 0.283881f + rgb[1] * 0.668433f + rgb[2] * 0.047685f; @@ -343,7 +470,7 @@ 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) +void colorFromDecOrHexString(byte* rgb, const char* in) { if (in[0] == 0) return; char first = in[0]; @@ -452,50 +579,39 @@ uint16_t approximateKelvinFromRGB(uint32_t rgb) { } } -//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 +// gamma lookup tables used for color correction (filled on 1st use (cfg.cpp & set.cpp)) +uint8_t NeoGammaWLEDMethod::gammaT[256]; +uint8_t NeoGammaWLEDMethod::gammaT_inv[256]; + +// re-calculates & fills gamma tables void NeoGammaWLEDMethod::calcGammaTable(float gamma) { - for (size_t i = 0; i < 256; i++) { + float gamma_inv = 1.0f / gamma; // inverse gamma + for (size_t i = 1; i < 256; i++) { gammaT[i] = (int)(powf((float)i / 255.0f, gamma) * 255.0f + 0.5f); + gammaT_inv[i] = (int)(powf(((float)i - 0.5f) / 255.0f, gamma_inv) * 255.0f + 0.5f); + //DEBUG_PRINTF_P(PSTR("gammaT[%d] = %d gammaT_inv[%d] = %d\n"), i, gammaT[i], i, gammaT_inv[i]); } + gammaT[0] = 0; + gammaT_inv[0] = 0; } -uint8_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct(uint8_t value) +uint8_t NeoGammaWLEDMethod::Correct(uint8_t value) { if (!gammaCorrectCol) return value; return gammaT[value]; } -// used for color gamma correction -uint32_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct32(uint32_t color) +uint32_t NeoGammaWLEDMethod::inverseGamma32(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]; + w = gammaT_inv[w]; + r = gammaT_inv[r]; + g = gammaT_inv[g]; + b = gammaT_inv[b]; return RGBW32(r, g, b, w); } diff --git a/wled00/colors.h b/wled00/colors.h new file mode 100644 index 0000000000..eb5767de7e --- /dev/null +++ b/wled00/colors.h @@ -0,0 +1,155 @@ +#pragma once +#ifndef WLED_COLORS_H +#define WLED_COLORS_H + +/* + * Color structs and color utility functions + */ +#include +#include "FastLED.h" + +#define ColorFromPalette ColorFromPaletteWLED // override fastled version + +// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color +// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts +// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB +struct CRGBW { + union { + uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB) + struct { + uint8_t b; + uint8_t g; + uint8_t r; + uint8_t w; + }; + uint8_t raw[4]; // Access as an array in the order B, G, R, W + }; + + // Default constructor + inline CRGBW() __attribute__((always_inline)) = default; + + // Constructor from a 32-bit color (0xWWRRGGBB) + constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {} + + // Constructor with r, g, b, w values + constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {} + + // Constructor from CRGB + constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {} + + // Access as an array + inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; } + + // Assignment from 32-bit color + inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; } + + // Assignment from r, g, b, w + inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; } + + // Conversion operator to uint32_t + inline operator uint32_t() const __attribute__((always_inline)) { + return color32; + } + /* + // Conversion operator to CRGB + inline operator CRGB() const __attribute__((always_inline)) { + return CRGB(r, g, b); + } + + CRGBW& scale32 (uint8_t scaledown) // 32bit math + { + if (color32 == 0) return *this; // 2 extra instructions, worth it if called a lot on black (which probably is true) adding check if scaledown is zero adds much more overhead as its 8bit + uint32_t scale = scaledown + 1; + uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue + uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green + color32 = rb | wg; + return *this; + }*/ + +}; + +struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions + union { + struct { + uint16_t h; // hue + uint8_t s; // saturation + uint8_t v; // value + }; + uint32_t raw; // 32bit access + }; + inline CHSV32() __attribute__((always_inline)) = default; // default constructor + + /// Allow construction from hue, saturation, and value + /// @param ih input hue + /// @param is input saturation + /// @param iv input value + inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v + : h(ih), s(is), v(iv) {} + inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v + : h((uint16_t)ih << 8), s(is), v(iv) {} + inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV + : h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {} + inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV +}; +extern bool gammaCorrectCol; +// similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) +class NeoGammaWLEDMethod { + public: + [[gnu::hot]] static uint8_t Correct(uint8_t value); // apply Gamma to single channel + [[gnu::hot]] static uint32_t inverseGamma32(uint32_t color); // apply inverse Gamma to RGBW32 color + static void calcGammaTable(float gamma); // re-calculates & fills gamma tables + static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; } // get value from Gamma table (WLED specific, not used by NPB) + static inline uint8_t rawInverseGamma8(uint8_t val) { return gammaT_inv[val]; } // get value from inverse Gamma table (WLED specific, not used by NPB) + static inline uint32_t Correct32(uint32_t color) { // apply Gamma to RGBW32 color (WLED specific, not used by NPB) + if (!gammaCorrectCol) return color; // no gamma correction + uint8_t w = byte(color>>24), r = byte(color>>16), g = byte(color>>8), b = byte(color); // extract r, g, b, w channels + w = gammaT[w]; r = gammaT[r]; g = gammaT[g]; b = gammaT[b]; + return (uint32_t(w) << 24) | (uint32_t(r) << 16) | (uint32_t(g) << 8) | uint32_t(b); + } + private: + static uint8_t gammaT[]; + static uint8_t gammaT_inv[]; +}; +#define gamma32(c) NeoGammaWLEDMethod::Correct32(c) +#define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) +#define gamma32inv(c) NeoGammaWLEDMethod::inverseGamma32(c) +#define gamma8inv(c) NeoGammaWLEDMethod::rawInverseGamma8(c) +[[gnu::hot, gnu::pure]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend); +inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); }; +[[gnu::hot, gnu::pure]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false); +[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false); +[[gnu::hot, gnu::pure]] uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten); +[[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); +CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); +CRGBPalette16 generateRandomPalette(); +void loadCustomPalettes(); +extern std::vector customPalettes; +inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); } +inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } +void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); +void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); +void rgb2hsv(const uint32_t rgb, CHSV32& hsv); +inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 16) | (byte(c.g) << 8) | (byte(c.b)))), hsv); return CHSV(hsv); } // CRGB to hsv +void colorKtoRGB(uint16_t kelvin, byte* rgb); +void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb +void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO +void colorRGBtoXY(const byte* rgb, float* xy); // only defined if huesync disabled TODO +void colorFromDecOrHexString(byte* rgb, const char* in); +bool colorFromHexString(byte* rgb, const char* in); +uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); +uint16_t approximateKelvinFromRGB(uint32_t rgb); +void setRandomColor(byte* rgb); + +// fast scaling function for colors, performs color*scale/256 for all four channels, speed over accuracy +// note: inlining uses less code than actual function calls +static inline uint32_t fast_color_scale(const uint32_t c, const uint8_t scale) { + uint32_t rb = (((c & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; + uint32_t wg = (((c>>8) & 0x00FF00FF) * scale) & ~0x00FF00FF; + return rb | wg; +} + +// palettes +extern const TProgmemRGBPalette16* const fastledPalettes[]; +extern const uint8_t* const gGradientPalettes[]; +#endif + diff --git a/wled00/const.h b/wled00/const.h index 07873deca1..95e69d855b 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -1,3 +1,4 @@ +#pragma once #ifndef WLED_CONST_H #define WLED_CONST_H @@ -5,7 +6,16 @@ * Readability defines and their associated numerical values + compile-time constants */ -#define GRADIENT_PALETTE_COUNT 58 +constexpr size_t FASTLED_PALETTE_COUNT = 7; // 6-12 = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]); +constexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]); +constexpr size_t DYNAMIC_PALETTE_COUNT = 6; // 0- 5 = dynamic palettes (0=default(virtual),1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black) +constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes +#ifndef ESP8266 + #define WLED_MAX_CUSTOM_PALETTES (255 - FIXED_PALETTE_COUNT) // allow up to 255 total palettes, user is warned about stability issues when adding more than 10 +#else + #define WLED_MAX_CUSTOM_PALETTES 10 // ESP8266: limit custom palettes to 10 +#endif +#define WLED_MAX_CUSTOM_PALETTE_GAP 20 // max number of empty palette files in a row before stopping to look for more (20 takes 100ms) // You can define custom product info from build flags. // This is useful to allow API consumer to identify what type of WLED version @@ -37,76 +47,70 @@ #endif #ifndef WLED_MAX_USERMODS - #ifdef ESP8266 + #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_USERMODS 4 #else #define WLED_MAX_USERMODS 6 #endif #endif -#ifndef WLED_MAX_BUSSES - #ifdef ESP8266 - #define WLED_MAX_DIGITAL_CHANNELS 3 - #define WLED_MAX_ANALOG_CHANNELS 5 - #define WLED_MAX_BUSSES 4 // will allow 3 digital & 1 analog RGB - #define WLED_MIN_VIRTUAL_BUSSES 2 - #else - #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) - #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM - #define WLED_MAX_BUSSES 6 // will allow 2 digital & 2 analog RGB or 6 PWM white - #define WLED_MAX_DIGITAL_CHANNELS 2 - //#define WLED_MAX_ANALOG_CHANNELS 6 - #define WLED_MIN_VIRTUAL_BUSSES 3 - #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB - // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) - #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog RGB - #define WLED_MAX_DIGITAL_CHANNELS 5 - //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_MIN_VIRTUAL_BUSSES 3 - #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 RGB - #define WLED_MAX_DIGITAL_CHANNELS 4 - //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_MIN_VIRTUAL_BUSSES 4 - #else - // the last digital bus (I2S0) will prevent Audioreactive usermod from functioning - #define WLED_MAX_BUSSES 20 // will allow 17 digital & 3 analog RGB - #define WLED_MAX_DIGITAL_CHANNELS 17 - //#define WLED_MAX_ANALOG_CHANNELS 16 - #define WLED_MIN_VIRTUAL_BUSSES 4 - #endif - #endif +#ifdef ESP8266 + #define WLED_MAX_DIGITAL_CHANNELS 3 + #define WLED_MAX_RMT_CHANNELS 0 // ESP8266 does not have RMT nor I2S + #define WLED_MAX_I2S_CHANNELS 0 + #define WLED_MAX_ANALOG_CHANNELS 5 + #define WLED_PLATFORM_ID 0 // used in UI to distinguish ESP types, needs a proper fix! #else - #ifdef ESP8266 - #if WLED_MAX_BUSSES > 5 - #error Maximum number of buses is 5. - #endif - #ifndef WLED_MAX_ANALOG_CHANNELS - #error You must also define WLED_MAX_ANALOG_CHANNELS. - #endif - #ifndef WLED_MAX_DIGITAL_CHANNELS - #error You must also define WLED_MAX_DIGITAL_CHANNELS. - #endif - #define WLED_MIN_VIRTUAL_BUSSES (5-WLED_MAX_BUSSES) + #if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX) + #include "driver/ledc.h" // needed for analog/LEDC channel counts + #endif + #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) + #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM + #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels + #define WLED_MAX_I2S_CHANNELS 0 // I2S not supported by NPB + //#define WLED_MAX_ANALOG_CHANNELS 6 + #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! + #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB + #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S2 has 4 RMT output channels + #define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB + //#define WLED_MAX_ANALOG_CHANNELS 8 + #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI + #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1 + #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S3 has 4 RMT output channels + #define WLED_MAX_I2S_CHANNELS 8 // uses LCD parallel output not I2S + //#define WLED_MAX_ANALOG_CHANNELS 8 + #define WLED_PLATFORM_ID 3 // used in UI to distinguish ESP type in UI, needs a proper fix! #else - #if WLED_MAX_BUSSES > 20 - #error Maximum number of buses is 20. - #endif - #ifndef WLED_MAX_ANALOG_CHANNELS - #error You must also define WLED_MAX_ANALOG_CHANNELS. - #endif - #ifndef WLED_MAX_DIGITAL_CHANNELS - #error You must also define WLED_MAX_DIGITAL_CHANNELS. - #endif - #define WLED_MIN_VIRTUAL_BUSSES (20-WLED_MAX_BUSSES) + #define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels + #define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB + //#define WLED_MAX_ANALOG_CHANNELS 16 + #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! #endif + #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS) +#endif +// WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed +// instead it will help determine max number of buses that can be defined at compile time +#ifdef WLED_MAX_BUSSES + #undef WLED_MAX_BUSSES +#endif +#define WLED_MAX_BUSSES (WLED_MAX_DIGITAL_CHANNELS+WLED_MAX_ANALOG_CHANNELS) +static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); + +// Maximum number of pins per output. 5 for RGBCCT analog LEDs. +#define OUTPUT_MAX_PINS 5 + +// for pin manager +#ifdef ESP8266 +#define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) +#else +#define WLED_NUM_PINS (GPIO_PIN_COUNT) #endif #ifndef WLED_MAX_BUTTONS #ifdef ESP8266 - #define WLED_MAX_BUTTONS 2 + #define WLED_MAX_BUTTONS 10 #else - #define WLED_MAX_BUTTONS 4 + #define WLED_MAX_BUTTONS 32 #endif #else #if WLED_MAX_BUTTONS < 2 @@ -115,7 +119,9 @@ #endif #endif -#ifdef ESP8266 +#define RELAY_DELAY 50 // delay in ms between switching on relay and sending data to LEDs + +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_COLOR_ORDER_MAPPINGS 5 #else #define WLED_MAX_COLOR_ORDER_MAPPINGS 10 @@ -125,7 +131,7 @@ #undef WLED_MAX_LEDMAPS #endif #ifndef WLED_MAX_LEDMAPS - #ifdef ESP8266 + #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_LEDMAPS 10 #else #define WLED_MAX_LEDMAPS 16 @@ -147,6 +153,8 @@ #endif #endif +#define WLED_MAX_PANELS 18 // must not be more than 32 + //Usermod IDs #define USERMOD_ID_RESERVED 0 //Unused. Might indicate no usermod present #define USERMOD_ID_UNSPECIFIED 1 //Default value for a general user mod that does not specify a custom ID @@ -203,6 +211,16 @@ #define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" #define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h" #define USERMOD_ID_PIXELS_DICE_TRAY 54 //Usermod "pixels_dice_tray.h" +#define USERMOD_ID_DEEP_SLEEP 55 //Usermod "usermod_deep_sleep.h" +#define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" +#define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" +#define USERMOD_ID_USER_FX 58 //Usermod "user_fx" + +//Wifi encryption type +#ifdef WLED_ENABLE_WPA_ENTERPRISE + #define WIFI_ENCRYPTION_TYPE_PSK 0 //None/WPA/WPA2 + #define WIFI_ENCRYPTION_TYPE_ENTERPRISE 1 //WPA/WPA2-Enterprise +#endif //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -248,6 +266,7 @@ #define REALTIME_MODE_ARTNET 6 #define REALTIME_MODE_TPM2NET 7 #define REALTIME_MODE_DDP 8 +#define REALTIME_MODE_DMX 9 //realtime override modes #define REALTIME_OVERRIDE_NONE 0 @@ -294,7 +313,7 @@ #define TYPE_UCS8903 26 #define TYPE_APA106 27 #define TYPE_FW1906 28 //RGB + CW + WW + unused channel (6 channels per IC) -#define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp, memUsage()) +#define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp) #define TYPE_SK6812_RGBW 30 #define TYPE_TM1814 31 #define TYPE_WS2805 32 //RGB + WW + CW @@ -319,6 +338,12 @@ #define TYPE_P9813 53 #define TYPE_LPD6803 54 #define TYPE_2PIN_MAX 63 + +#define TYPE_HUB75MATRIX_MIN 64 +#define TYPE_HUB75MATRIX_HS 65 +#define TYPE_HUB75MATRIX_QS 66 +#define TYPE_HUB75MATRIX_MAX 71 + //Network types (master broadcast) (80-95) #define TYPE_VIRTUAL_MIN 80 #define TYPE_NET_DDP_RGB 80 //network DDP RGB bus (master broadcast bus) @@ -328,18 +353,6 @@ #define TYPE_NET_ARTNET_RGBW 89 //network ArtNet RGB bus (master broadcast bus, unused) #define TYPE_VIRTUAL_MAX 95 -/* -// old macros that have been moved to Bus class -#define IS_TYPE_VALID(t) ((t) > 15 && (t) < 128) -#define IS_DIGITAL(t) (((t) > 15 && (t) < 40) || ((t) > 47 && (t) < 64)) //digital are 16-39 and 48-63 -#define IS_2PIN(t) ((t) > 47 && (t) < 64) -#define IS_16BIT(t) ((t) == TYPE_UCS8903 || (t) == TYPE_UCS8904) -#define IS_ONOFF(t) ((t) == 40) -#define IS_PWM(t) ((t) > 40 && (t) < 46) //does not include on/Off type -#define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only -#define IS_VIRTUAL(t) ((t) >= 80 && (t) < 96) //this was a poor choice a better would be 96-111 -*/ - //Color orders #define COL_ORDER_GRB 0 //GRB(w),defaut #define COL_ORDER_RGB 1 //common for WS2811 @@ -367,7 +380,8 @@ #define BTN_TYPE_TOUCH_SWITCH 9 //Ethernet board types -#define WLED_NUM_ETH_TYPES 13 +#define WLED_NUM_ETH_TYPES 14 + #define WLED_ETH_NONE 0 #define WLED_ETH_WT32_ETH01 1 @@ -382,6 +396,7 @@ #define WLED_ETH_SERG74 10 #define WLED_ETH_ESP32_POE_WROVER 11 #define WLED_ETH_LILYGO_T_POE_PRO 12 +#define WLED_ETH_GLEDOPTO 13 //Hue error codes #define HUE_ERROR_INACTIVE 0 @@ -427,6 +442,7 @@ #define ERR_CONCURRENCY 2 // Conurrency (client active) #define ERR_NOBUF 3 // JSON buffer was not released in time, request cannot be handled at this time #define ERR_NOT_IMPL 4 // Not implemented +#define ERR_NORAM_PX 7 // not enough RAM for pixels #define ERR_NORAM 8 // effect RAM depleted #define ERR_JSON 9 // JSON parsing failed (input too large?) #define ERR_FS_BEGIN 10 // Could not init filesystem (no partition?) @@ -439,6 +455,31 @@ #define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented) #define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented) +// JSON buffer lock owners +#define JSON_LOCK_UNKNOWN 255 +#define JSON_LOCK_CFG_DES 1 +#define JSON_LOCK_CFG_SER 2 +#define JSON_LOCK_CFG_SEC_DES 3 +#define JSON_LOCK_CFG_SEC_SER 4 +#define JSON_LOCK_SETTINGS 5 +#define JSON_LOCK_XML 6 +#define JSON_LOCK_LEDMAP 7 +// unused 8 +#define JSON_LOCK_PRESET_LOAD 9 +#define JSON_LOCK_PRESET_SAVE 10 +#define JSON_LOCK_WS_RECEIVE 11 +#define JSON_LOCK_WS_SEND 12 +#define JSON_LOCK_IR 13 +#define JSON_LOCK_SERVER 14 +#define JSON_LOCK_MQTT 15 +#define JSON_LOCK_SERIAL 16 +#define JSON_LOCK_SERVEJSON 17 +#define JSON_LOCK_NOTIFY 18 +#define JSON_LOCK_PRESET_NAME 19 +#define JSON_LOCK_LEDGAP 20 +#define JSON_LOCK_LEDMAP_ENUM 21 +#define JSON_LOCK_REMOTE 22 + // Timer mode types #define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness #define NL_MODE_FADE 1 //Fade to target brightness gradually @@ -457,6 +498,8 @@ #define SUBPAGE_UM 8 #define SUBPAGE_UPDATE 9 #define SUBPAGE_2D 10 +#define SUBPAGE_PINS 11 +#define SUBPAGE_LAST SUBPAGE_PINS #define SUBPAGE_LOCK 251 #define SUBPAGE_PINREQ 252 #define SUBPAGE_CSS 253 @@ -466,26 +509,34 @@ #define NTP_PACKET_SIZE 48 // size of NTP receive buffer #define NTP_MIN_PACKET_SIZE 48 // min expected size - NTP v4 allows for "extended information" appended to the standard fields -// Maximum number of pins per output. 5 for RGBCCT analog LEDs. -#define OUTPUT_MAX_PINS 5 - //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 + #ifdef ESP8266 + #define MAX_LEDS 1536 //can't rely on memory limit to limit this to 1536 LEDs + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + #define MAX_LEDS 2048 //due to memory constraints S2 + #else + #define MAX_LEDS 16384 + #endif #endif +// maximum total memory that can be used for bus-buffers and pixel buffers #ifndef MAX_LED_MEMORY #ifdef ESP8266 - #define MAX_LED_MEMORY 4000 + #define MAX_LED_MEMORY (8*1024) #else - #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) - #define MAX_LED_MEMORY 32000 + #if defined(CONFIG_IDF_TARGET_ESP32S2) + #ifndef BOARD_HAS_PSRAM + #define MAX_LED_MEMORY (28*1024) // S2 has ~170k of free heap after boot, using 28k is the absolute limit to keep WLED functional + #else + #define MAX_LED_MEMORY (48*1024) // with PSRAM there is more wiggle room as buffers get moved to PSRAM when needed (prioritize functionality over speed) + #endif + #elif defined(CONFIG_IDF_TARGET_ESP32S3) + #define MAX_LED_MEMORY (192*1024) // S3 has ~330k of free heap after boot + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + #define MAX_LED_MEMORY (100*1024) // C3 has ~240k of free heap after boot, even with 8000 LEDs configured (2D) there is 30k of contiguous heap left #else - #define MAX_LED_MEMORY 64000 + #define MAX_LED_MEMORY (85*1024) // ESP32 has ~160k of free heap after boot and an additional 64k of 32bit access memory that is used for pixel buffers #endif #endif #endif @@ -550,15 +601,45 @@ #ifdef ESP8266 #define JSON_BUFFER_SIZE 10240 #else - #if defined(ARDUINO_ARCH_ESP32S2) + #if defined(CONFIG_IDF_TARGET_ESP32S2) #define JSON_BUFFER_SIZE 24576 #else #define JSON_BUFFER_SIZE 32767 #endif #endif -//#define MIN_HEAP_SIZE (8k for AsyncWebServer) -#define MIN_HEAP_SIZE 8192 +// minimum heap size required to process web requests: try to keep free heap above this value +#ifdef ESP8266 + #define MIN_HEAP_SIZE (9*1024) +#else + #define MIN_HEAP_SIZE (15*1024) // WLED allocation functions (util.cpp) try to keep this much contiguous heap free for other tasks +#endif +// threshold for PSRAM use: if heap is running low, requests to allocate_buffer(prefer DRAM) above PSRAM_THRESHOLD may be put in PSRAM +// if heap is depleted, PSRAM will be used regardless of threshold +#if defined(CONFIG_IDF_TARGET_ESP32S3) + #define PSRAM_THRESHOLD (12*1024) // S3 has plenty of DRAM +#elif defined(CONFIG_IDF_TARGET_ESP32) + #define PSRAM_THRESHOLD (5*1024) +#else + #define PSRAM_THRESHOLD (2*1024) // S2 does not have a lot of RAM. C3 and ESP8266 do not support PSRAM: the value is not used +#endif + +// Web server limits +#ifdef ESP8266 +// Minimum heap to consider handling a request +#define WLED_REQUEST_MIN_HEAP (8*1024) +// Estimated maximum heap required by any one request +#define WLED_REQUEST_HEAP_USAGE (6*1024) +#else +// ESP32 TCP stack needs much more RAM than ESP8266 +// Minimum heap remaining before queuing a request +#define WLED_REQUEST_MIN_HEAP (12*1024) +// Estimated maximum heap required by any one request +#define WLED_REQUEST_HEAP_USAGE (12*1024) +#endif +// Maximum number of requests in queue; absolute cap on web server resource usage. +// Websockets do not count against this limit. +#define WLED_REQUEST_MAX_QUEUE 6 // Maximum size of node map (list of other WLED instances) #ifdef ESP8266 @@ -576,7 +657,12 @@ #define DEFAULT_LED_PIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board #endif #else - #define DEFAULT_LED_PIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit()) + #if defined(WLED_USE_ETHERNET) + #define DEFAULT_LED_PIN 4 // GPIO4 seems to be a "safe bet" for all known ethernet boards (issue #5155) + //#warning "Compiling with Ethernet support. The default LED pin has been changed to pin 4." + #else + #define DEFAULT_LED_PIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit()) + #endif #endif #define DEFAULT_LED_TYPE TYPE_WS2812_RGB #define DEFAULT_LED_COUNT 30 @@ -649,4 +735,6 @@ #define IRAM_ATTR_YN IRAM_ATTR #endif +#define WLED_O2_ATTR __attribute__((optimize("O2"))) + #endif diff --git a/wled00/data/404.htm b/wled00/data/404.htm index ff41fa6e03..caf6856def 100644 --- a/wled00/data/404.htm +++ b/wled00/data/404.htm @@ -1,5 +1,5 @@ - + diff --git a/wled00/data/common.js b/wled00/data/common.js index 9378ef07a8..a6223daa7c 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -2,7 +2,7 @@ var d=document; var loc = false, locip, locproto = "http:"; function H(pg="") { window.open("https://kno.wled.ge/"+pg); } -function GH() { window.open("https://github.com/Aircoookie/WLED"); } +function GH() { window.open("https://github.com/wled-dev/WLED"); } function gId(c) { return d.getElementById(c); } // getElementById function cE(e) { return d.createElement(e); } // createElement function gEBCN(c) { return d.getElementsByClassName(c); } // getElementsByClassName @@ -13,10 +13,10 @@ function isN(n) { return !isNaN(parseFloat(n)) && isFinite(n); } // isNumber // https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer function isF(n) { return n === +n && n !== (n|0); } // isFloat function isI(n) { return n === +n && n === (n|0); } // isInteger -function toggle(el) { gId(el).classList.toggle("hide"); gId('No'+el).classList.toggle("hide"); } +function toggle(el) { gId(el).classList.toggle("hide"); let n = gId('No'+el); if (n) n.classList.toggle("hide"); } function tooltip(cont=null) { d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ - element.addEventListener("mouseover", ()=>{ + element.addEventListener("pointerover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); const tooltip = d.createElement("span"); @@ -41,7 +41,7 @@ function tooltip(cont=null) { tooltip.classList.add("visible"); }); - element.addEventListener("mouseout", ()=>{ + element.addEventListener("pointerout", ()=>{ d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); @@ -51,6 +51,38 @@ function tooltip(cont=null) { }); }); }; +// sequential loading of external resources (JS or CSS) with retry, calls init() when done +function loadResources(files, init) { + let i = 0; + const loadNext = () => { + if (i >= files.length) { + if (init) { + d.documentElement.style.visibility = 'visible'; // make page visible after all files are loaded if it was hidden (prevent ugly display) + d.readyState === 'complete' ? init() : window.addEventListener('load', init); + } + return; + } + const file = files[i++]; + const isCSS = file.endsWith('.css'); + const el = d.createElement(isCSS ? 'link' : 'script'); + if (isCSS) { + el.rel = 'stylesheet'; + el.href = file; + const st = d.head.querySelector('style'); + if (st) d.head.insertBefore(el, st); // insert before any - + + + + WLED 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 -
-
-
- +
+

WLED Palette Editor

+ +
+
+
+ + + +
+
+ +
+
+ + +
+
+ + + +
+
+ +
+
+
+ + + +
+ +
+ Warning: Adding many custom palettes might cause stability issues, create backups +
+
+ +
+ +
+ +
+
+ +
+ +
by @dedehai
+
+ + + - - - + \ No newline at end of file diff --git a/wled00/data/dmxmap.htm b/wled00/data/dmxmap.htm index 25953b0e37..2b821c1e77 100644 --- a/wled00/data/dmxmap.htm +++ b/wled00/data/dmxmap.htm @@ -1,5 +1,5 @@ - + DMX Map - - -
-
-
-
- - + +
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/wled00/data/icons-ui/demo.html b/wled00/data/icons-ui/demo.html index 0416231fb0..bf5d4eebad 100644 --- a/wled00/data/icons-ui/demo.html +++ b/wled00/data/icons-ui/demo.html @@ -1,5 +1,5 @@ - + IcoMoon Demo diff --git a/wled00/data/index.css b/wled00/data/index.css index 0e36ff08c9..6b8e068a00 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -97,6 +97,7 @@ button { .labels { margin: 0; padding: 8px 0 2px 0; + font-size: 19px; } #namelabel { @@ -352,12 +353,12 @@ button { padding: 4px 0 0; } -#segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, +#segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, #bsp, .fnd { max-width: 280px; } -#putil, #segutil, #segutil2 { +#putil, #segutil, #segutil2, #bsp { min-height: 42px; margin: 0 auto; } @@ -793,7 +794,7 @@ input[type=range]::-moz-range-thumb { /* buttons */ .btn { padding: 8px; - /*margin: 10px 4px;*/ + margin: 10px 4px; width: 230px; font-size: 19px; color: var(--c-d); @@ -890,12 +891,12 @@ a.btn { line-height: 28px; } -/* Quick color select Black button (has white border) */ -.qcsb { +/* Quick color select Black and White button (has white/black border, depending on the theme) */ +.qcsb, .qcsw { width: 26px; height: 26px; line-height: 26px; - border: 1px solid #fff; + border: 1px solid var(--c-f); } /* Hex color input wrapper div */ @@ -1299,6 +1300,14 @@ TD .checkmark, TD .radiomark { width: 100%; } +#segutil { + margin-bottom: 12px; +} + +#segcont > div:first-child, #fxFind { + margin-top: 4px; +} + /* Simplify segments */ .simplified #segcont .lstI { margin-top: 4px; @@ -1438,6 +1447,11 @@ dialog { position: relative; } +.presin { + width: 100%; + box-sizing: border-box; +} + .btn-s, .btn-n { border: 1px solid var(--c-2); @@ -1462,7 +1476,7 @@ dialog { .expanded { display: inline-block !important; } -.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .frz, .expanded .g-icon { +.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .g-icon { display: none !important; } diff --git a/wled00/data/index.htm b/wled00/data/index.htm index e74ea00764..4d680cfbe2 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -6,10 +6,11 @@ + WLED - +
Loading WLED UI...
@@ -106,7 +107,7 @@
-
+

@@ -127,9 +128,8 @@
- - - + +

Color palette

@@ -266,7 +266,29 @@
-

Transition:  s

+

Transition:  s

+

@@ -306,7 +328,7 @@
- +

@@ -340,9 +362,10 @@ - + diff --git a/wled00/data/index.js b/wled00/data/index.js index 1482c27b55..cd508e8642 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -8,6 +8,7 @@ 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 cpick; // iro color picker var currentPreset = -1; var lastUpdate = 0; var segCount = 0, ledCount = 0, lowestUnused = 0, maxSeg = 0, lSeg = 0; @@ -16,13 +17,12 @@ var simplifiedUI = false; var tr = 7; var d = document; const ranges = RangeTouch.setup('input[type="range"]', {}); -var retry = false; var palettesData; var fxdata = []; var pJson = {}, eJson = {}, lJson = {}; var plJson = {}; // array of playlists var pN = "", pI = 0, pNum = 0; -var pmt = 1, pmtLS = 0, pmtLast = 0; +var pmt = 1, pmtLS = 0; var lastinfo = {}; var isM = false, mw = 0, mh=0; var ws, wsRpt=0; @@ -35,29 +35,38 @@ var cfg = { // [year, month (0 -> January, 11 -> December), day, duration in days, image url] 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"], // easter 2025 - [2024, 2, 31, 2, "https://aircoookie.github.io/easter.png"], // easter 2024 + [0, 2, 17, 1, "https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day + [2026, 3, 5, 2, "https://aircoookie.github.io/easter.png"], // easter 2026 + [2027, 2, 28, 2, "https://aircoookie.github.io/easter.png"], // easter 2027 + //[2028, 3, 16, 2, "https://aircoookie.github.io/easter.png"], // easter 2028 [0, 6, 4, 1, "https://images.alphacoders.com/516/516792.jpg"], // 4th of July [0, 0, 1, 1, "https://images.alphacoders.com/119/1198800.jpg"] // new year ]; -var cpick = new iro.ColorPicker("#picker", { - width: 260, - wheelLightness: false, - wheelAngle: 270, - wheelDirection: "clockwise", - layout: [{ - component: iro.ui.Wheel, - options: {} - }] -}); +// load iro.js sequentially to avoid 503 errors, retries until successful +(function loadIro() { + const l = d.createElement('script'); + l.src = 'iro.js'; + l.onload = () => { + cpick = new iro.ColorPicker("#picker", { + width: 260, + wheelLightness: false, + wheelAngle: 270, + wheelDirection: "clockwise", + layout: [{component: iro.ui.Wheel, options: {}}] + }); + d.readyState === 'complete' ? onLoad() : window.addEventListener('load', onLoad); + }; + l.onerror = () => setTimeout(loadIro, 100); + document.head.appendChild(l); +})(); + 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 isEmpty(o) {for (const i in o) return false; return true;} function isObj(i) {return (i && typeof i === 'object' && !Array.isArray(i));} function isNumeric(n) {return !isNaN(parseFloat(n)) && isFinite(n);} @@ -199,19 +208,17 @@ function loadBg() { }); } -function loadSkinCSS(cId) -{ - if (!gId(cId)) // check if element exists - { - var h = d.getElementsByTagName('head')[0]; - var l = d.createElement('link'); - l.id = cId; - l.rel = 'stylesheet'; - l.type = 'text/css'; +function loadSkinCSS(cId) { + return new Promise((resolve, reject) => { + if (gId(cId)) return resolve(); + const l = d.createElement('link'); + l.id = cId; + l.rel = 'stylesheet'; l.href = getURL('/skin.css'); - l.media = 'all'; - h.appendChild(l); - } + l.onload = resolve; + l.onerror = reject; + d.head.appendChild(l); + }); } function getURL(path) { @@ -277,19 +284,23 @@ function onLoad() cpick.on("color:change", () => {updatePSliders()}); pmtLS = localStorage.getItem('wledPmt'); - // Load initial data - loadPalettes(()=>{ - // fill effect extra data array - loadFXData(()=>{ - // load and populate effects - setTimeout(()=>{loadFX(()=>{ - loadPalettesData(()=>{ - requestJson();// will load presets and create WS - if (cfg.comp.css) setTimeout(()=>{loadSkinCSS('skinCss')},50); - }); - })},50); - }); - }); + // Load initial data sequentially, no parallel requests to avoid "503" errors when heap is low (slower but much more reliable) + (async ()=>{ + try { + await loadPalettes(); // loads base palettes and builds #pallist (safe first) + await loadFXData(); // loads fx data + await loadFX(); // populates effect list + await requestJson(); // updates info variables + await loadPalettesData(); // fills palettesData[] for previews + populatePalettes(); // repopulate with custom palettes now that cpalcount is known + if(pmt == pmtLS) populatePresets(true); // load presets from localStorage if signature matches (i.e. no device reboot) + else await loadPresets(); // load and populate presets + if (cfg.comp.css) await loadSkinCSS('skinCss'); + if (!ws) makeWS(); + } catch(e) { + showToast("Init failed: " + e, true); + } + })(); resetUtil(); d.addEventListener("visibilitychange", handleVisibilityChange, false); @@ -409,7 +420,9 @@ function pName(i) function isPlaylist(i) { - return pJson[i].playlist && pJson[i].playlist.ps; + if (isNumeric(i)) return pJson[i].playlist && pJson[i].playlist.ps; + if (isObj(i)) return i.playlist && i.playlist.ps; + return false; } function papiVal(i) @@ -445,7 +458,7 @@ function presetError(empty) if (bckstr.length > 10) hasBackup = true; } catch (e) {} - var cn = `
`; + var cn = `
`; if (empty) cn += `You have no presets yet!`; else @@ -478,123 +491,81 @@ function restore(txt) { return false; } -function loadPresets(callback = null) -{ - // 1st boot (because there is a callback) - if (callback && pmt == pmtLS && pmt > 0) { - // we have a copy of the presets in local storage and don't need to fetch another one - populatePresets(true); - pmtLast = pmt; - callback(); - return; - } - - // afterwards - if (!callback && pmt == pmtLast) return; - - fetch(getURL('/presets.json'), { - method: 'get' - }) - .then(res => { - if (res.status=="404") return {"0":{}}; - //if (!res.ok) showErrorToast(); - return res.json(); - }) - .then(json => { - pJson = json; - pmtLast = pmt; - populatePresets(); - }) - .catch((e)=>{ - //showToast(e, true); - presetError(false); - }) - .finally(()=>{ - if (callback) setTimeout(callback,99); +async function loadPresets() { + return new Promise((resolve) => { + fetch(getURL('/presets.json'), {method: 'get'}) + .then(res => res.status=="404" ? {"0":{}} : res.json()) + .then(json => { + pJson = json; + populatePresets(); + resolve(); + }) + .catch(() => { + presetError(false); + resolve(); + }) }); } -function loadPalettes(callback = null) -{ - fetch(getURL('/json/palettes'), { - method: 'get' - }) - .then((res)=>{ - if (!res.ok) showErrorToast(); - return res.json(); - }) - .then((json)=>{ - lJson = Object.entries(json); - populatePalettes(); - retry = false; - }) - .catch((e)=>{ - if (!retry) { - retry = true; - setTimeout(loadPalettes, 500); // retry - } - showToast(e, true); - }) - .finally(()=>{ - if (callback) callback(); - updateUI(); +async function loadPalettes(retry=0) { + return new Promise((resolve) => { + fetch(getURL('/json/palettes'), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject()) + .then(json => { + lJson = Object.entries(json); + populatePalettes(); + resolve(); + }) + .catch((e) => { + if (retry<5) { + setTimeout(() => loadPalettes(retry+1).then(resolve), 100); + } else { + showToast(e, true); + resolve(); + } + }); }); } -function loadFX(callback = null) -{ - fetch(getURL('/json/effects'), { - method: 'get' - }) - .then((res)=>{ - if (!res.ok) showErrorToast(); - return res.json(); - }) - .then((json)=>{ - eJson = Object.entries(json); - populateEffects(); - retry = false; - }) - .catch((e)=>{ - if (!retry) { - retry = true; - setTimeout(loadFX, 500); // retry - } - showToast(e, true); - }) - .finally(()=>{ - if (callback) callback(); - updateUI(); +async function loadFX(retry=0) { + return new Promise((resolve) => { + fetch(getURL('/json/effects'), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject()) + .then(json => { + eJson = Object.entries(json); + populateEffects(); + resolve(); + }) + .catch((e) => { + if (retry<5) { + setTimeout(() => loadFX(retry+1).then(resolve), 100); + } else { + showToast(e, true); + resolve(); + } + }); }); } -function loadFXData(callback = null) -{ - fetch(getURL('/json/fxdata'), { - method: 'get' - }) - .then((res)=>{ - if (!res.ok) showErrorToast(); - return res.json(); - }) - .then((json)=>{ - fxdata = json||[]; - // add default value for Solid - fxdata.shift() - fxdata.unshift(";!;"); - retry = false; - }) - .catch((e)=>{ - fxdata = []; - if (!retry) { - retry = true; - setTimeout(()=>{loadFXData(loadFX);}, 500); // retry - } - showToast(e, true); - }) - .finally(()=>{ - if (callback) callback(); - updateUI(); +async function loadFXData(retry=0) { + return new Promise((resolve) => { + fetch(getURL('/json/fxdata'), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject()) + .then(json => { + fxdata = json||[]; + fxdata.shift(); + fxdata.unshift(";!;"); + resolve(); + }) + .catch((e) => { + fxdata = []; + if (retry<5) { + setTimeout(() => loadFXData(retry+1).then(resolve), 100); + } else { + showToast(e, true); + resolve(); + } + }); }); } @@ -616,7 +587,7 @@ function populateQL() function populatePresets(fromls) { if (fromls) pJson = JSON.parse(localStorage.getItem("wledP")); - if (!pJson) {setTimeout(loadPresets,250); return;} + if (!pJson) {loadPresets(); return;} // note: no await as this is a fallback that should not be needed as init function fetches pJson delete pJson["0"]; var cn = ""; var arr = Object.entries(pJson).sort(cmpP); @@ -669,7 +640,6 @@ function parseInfo(i) { //syncTglRecv = i.str; maxSeg = i.leds.maxseg; pmt = i.fs.pmt; - if (pcMode && !i.wifi.ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide"); gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none"; // do we have a matrix set-up mw = i.leds.matrix ? i.leds.matrix.w : 0; @@ -677,9 +647,12 @@ function parseInfo(i) { isM = mw>0 && mh>0; if (!isM) { gId("filter2D").classList.add('hide'); + gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='none';}); } else { gId("filter2D").classList.remove('hide'); + gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='';}); } + gId("updBt").style.display = (i.opt & 1) ? '':'none'; // if (i.noaudio) { // gId("filterVol").classList.add("hide"); // gId("filterFreq").classList.add("hide"); @@ -688,16 +661,18 @@ function parseInfo(i) { // gId("filterVol").classList.add("hide"); hideModes(" ♪"); // hide volume reactive effects // gId("filterFreq").classList.add("hide"); hideModes(" ♫"); // hide frequency reactive effects // } + // Check for version upgrades on page load + checkVersionUpgrade(i); } //https://stackoverflow.com/questions/2592092/executing-script-elements-inserted-with-innerhtml //var setInnerHTML = function(elm, html) { // elm.innerHTML = html; // Array.from(elm.querySelectorAll("script")).forEach( oldScript => { -// const newScript = document.createElement("script"); +// const newScript = d.createElement("script"); // Array.from(oldScript.attributes) // .forEach( attr => newScript.setAttribute(attr.name, attr.value) ); -// newScript.appendChild(document.createTextNode(oldScript.innerHTML)); +// newScript.appendChild(d.createTextNode(oldScript.innerHTML)); // oldScript.parentNode.replaceChild(newScript, oldScript); // }); //} @@ -773,8 +748,8 @@ function populateSegments(s) } let segp = `
`+ - ``+ - `
`+ + ``+ + `
`+ ``+ `
`+ `
`+ @@ -801,16 +776,38 @@ function populateSegments(s) ``+ `
`+ `
`; + let blend = `
Blend mode
`+ + `
`+ + `
`; let sndSim = `
Sound sim
`+ `
`+ `
`; cn += `
`+ ``+ `
`+ `&#x${inst.frz ? (li.live && li.liveseg==i?'e410':'e0e8') : 'e325'};`+ @@ -854,6 +851,7 @@ function populateSegments(s) ``+ ``+ `
`+ + blend + (!isMSeg ? rvXck : '') + (isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') + (s.AudioReactive && s.AudioReactive.on ? "" : sndSim) + @@ -876,6 +874,7 @@ function populateSegments(s) gId("segcont").classList.remove("hide"); let noNewSegs = (lowestUnused >= maxSeg); resetUtil(noNewSegs); + if (segCount === 0) return; // no segments to populate for (var i = 0; i <= lSeg; i++) { if (!gId(`seg${i}`)) continue; updateLen(i); @@ -982,8 +981,6 @@ function populatePalettes() ); } } - if (li.cpalcount>0) gId("rmPal").classList.remove("hide"); - else gId("rmPal").classList.add("hide"); } function redrawPalPrev() @@ -1013,8 +1010,7 @@ function genPalPrevCss(id) } var gradient = []; - for (let j = 0; j < paletteData.length; j++) { - const e = paletteData[j]; + paletteData.forEach((e,j) => { let r, g, b; let index = false; if (Array.isArray(e)) { @@ -1036,9 +1032,8 @@ function genPalPrevCss(id) 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()});`; } @@ -1068,6 +1063,11 @@ function btype(b) case 34: return "ESP32-S3"; case 5: case 35: return "ESP32-C3"; + case 39: return "ESP32-C6"; + case 40: return "ESP32-C61"; + case 41: return "ESP32-C5"; + case 42: + case 43: return "ESP32-P4"; case 1: case 82: return "ESP8266"; } @@ -1248,7 +1248,6 @@ function updateUI() gId('buttonPower').className = (isOn) ? 'active':''; gId('buttonNl').className = (nlA) ? 'active':''; gId('buttonSync').className = (syncSend) ? 'active':''; - gId('pxmb').style.display = (isM) ? "inline-block" : "none"; updateSelectedFx(); updateSelectedPalette(selectedPal); // must be after updateSelectedFx() to un-hide color slots for * palettes @@ -1296,7 +1295,8 @@ function updateSelectedPalette(s) 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'); + if (!selectedPalette) return; // palette not yet loaded (custom palette on initial load) + selectedPalette.classList.add('selected'); // Display selected palette name on button in simplified UI let selectedName = selectedPalette.querySelector(".lstIname").innerText; @@ -1410,11 +1410,11 @@ function makeWS() { }; ws.onclose = (e)=>{ gId('connind').style.backgroundColor = "var(--c-r)"; - if (wsRpt++ < 5) setTimeout(makeWS,1500); // retry WS connection + if (wsRpt++ < 10) setTimeout(makeWS,wsRpt * 200); // retry WS connection ws = null; } ws.onopen = (e)=>{ - //ws.send("{'v':true}"); // unnecessary (https://github.com/Aircoookie/WLED/blob/master/wled00/ws.cpp#L18) + //ws.send("{'v':true}"); // unnecessary (https://github.com/wled/WLED/blob/master/wled00/ws.cpp#L18) wsRpt = 0; reqsLegal = true; } @@ -1437,43 +1437,38 @@ function readState(s,command=false) tr = s.transition; gId('tt').value = tr/10; + gId('bs').value = s.bs || 0; + if (tr===0) gId('bsp').classList.add('hide') + else gId('bsp').classList.remove('hide') populateSegments(s); - var selc=0; - var sellvl=0; // 0: selc is invalid, 1: selc is mainseg, 2: selc is first selected hasRGB = hasWhite = hasCCT = has2D = 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; - let w = (s.seg[i].stop - s.seg[i].start); - let h = s.seg[i].stopY ? (s.seg[i].stopY - s.seg[i].startY) : 1; - let lc = lastinfo.leds.seglc[i]; + segLmax = 0; // reset max selected segment length + let i = {}; + // determine light capabilities from selected segments + for (let seg of (s.seg||[])) { + let w = (seg.stop - seg.start); + let h = seg.stopY ? (seg.stopY - seg.startY) : 1; + let lc = seg.lc; + if (w*h > segLmax) segLmax = w*h; + if (seg.sel) { + if (isEmpty(i) || (i.id == s.mainseg && !i.sel)) i = seg; // get first selected segment (and replace mainseg if it is not selected) hasRGB |= !!(lc & 0x01); hasWhite |= !!(lc & 0x02); hasCCT |= !!(lc & 0x04); has2D |= w > 1 && h > 1; - if (w*h > segLmax) segLmax = w*h; - } - } - var i=s.seg[selc]; - if (sellvl == 1) { - let lc = lastinfo.leds.seglc[selc]; - hasRGB = !!(lc & 0x01); - hasWhite = !!(lc & 0x02); - hasCCT = !!(lc & 0x04); - has2D = (i.stop - i.start) > 1 && (i.stopY ? (i.stopY - i.startY) : 1) > 1; + } else if (isEmpty(i) && seg.id == s.mainseg) i = seg; // assign mainseg if no segments are selected } - if (!i) { - showToast('No Segments!', true); + if (isEmpty(i)) { + showToast('No segments!', true); updateUI(); return true; + } else if (i.id == s.mainseg) { + // fallback if no segments are selected but we have mainseg + hasRGB |= !!(i.lc & 0x01); + hasWhite |= !!(i.lc & 0x02); + hasCCT |= !!(i.lc & 0x04); + has2D |= (i.stop - i.start) > 1 && (i.stopY ? (i.stopY - i.startY) : 1) > 1; } var cd = gId('csl').querySelectorAll("button"); @@ -1505,6 +1500,9 @@ function readState(s,command=false) case 3: errstr = "Buffer locked!"; break; + case 7: + errstr = "No RAM for buffer!"; + break; case 8: errstr = "Effect RAM depleted!"; break; @@ -1659,13 +1657,15 @@ function setEffectParameters(idx) paOnOff[0] = paOnOff[0].substring(0,dPos); } if (paOnOff.length>0 && paOnOff[0] != "!") text = paOnOff[0]; + gId("editPal").classList.remove("hide"); } else { // disable palette list text += ' not used'; palw.style.display = "none"; + gId("editPal").classList.add("hide"); // Close palette dialog if not available - if (gId("palw").lastElementChild.tagName == "DIALOG") { - gId("palw").lastElementChild.close(); + if (palw.lastElementChild.tagName == "DIALOG") { + palw.lastElementChild.close(); } } pall.innerHTML = icon + text; @@ -1681,76 +1681,68 @@ function setEffectParameters(idx) 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; +async function requestJson(command=null, retry=0) { + return new Promise((resolve, reject) => { + gId('connind').style.backgroundColor = "var(--c-y)"; + if (command && !reqsLegal) {resolve(); return;} + if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000); + + var useWs = (ws && ws.readyState === WebSocket.OPEN); + var req = null; + if (command) { + command.v = true; + command.time = Math.floor(Date.now() / 1000); + var t = gId('tt'); + if (t && t.validity.valid && command.transition==null) { + var tn = parseInt(t.value*10); + if (tn != tr) command.transition = tn; + } + req = JSON.stringify(command); + if (req.length > 1340) useWs = false; + if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; } - req = JSON.stringify(command); - if (req.length > 1340) useWs = false; // do not send very long requests over websocket - if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; // esp8266 can only handle 500 bytes - }; - - if (useWs) { - ws.send(req?req:'{"v":true}'); - return; - } - fetch(getURL('/json/si'), { - method: type, - headers: {"Content-Type": "application/json; charset=UTF-8"}, - body: req - }) - .then(res => { - clearTimeout(jsonTimeout); - jsonTimeout = null; - if (!res.ok) showErrorToast(); - return res.json(); - }) - .then(json => { - lastUpdate = new Date(); - clearErrorToast(3000); - gId('connind').style.backgroundColor = "var(--c-g)"; - if (!json) { showToast('Empty response', true); return; } - if (json.success) return; - if (json.info) { - let i = json.info; - parseInfo(i); - populatePalettes(i); - if (isInfo) populateInfo(i); - if (simplifiedUI) simplifyUI(); + if (useWs) { + ws.send(req?req:'{"v":true}'); + resolve(); + return; } - var s = json.state ? json.state : json; - readState(s); - //load presets and open websocket sequentially - if (!pJson || isEmpty(pJson)) setTimeout(()=>{ - loadPresets(()=>{ - wsRpt = 0; - if (!(ws && ws.readyState === WebSocket.OPEN)) makeWS(); - }); - },25); - reqsLegal = true; - retry = false; - }) - .catch((e)=>{ - if (!retry) { - retry = true; - setTimeout(requestJson,500); - } - showToast(e, true); + fetch(getURL('/json/si'), { + method: command ? 'post' : 'get', + headers: {"Content-Type": "application/json; charset=UTF-8"}, + body: req + }) + .then(res => { + clearTimeout(jsonTimeout); + jsonTimeout = null; + return res.ok ? res.json() : Promise.reject(); + }) + .then(json => { + lastUpdate = new Date(); + clearErrorToast(3000); + gId('connind').style.backgroundColor = "var(--c-g)"; + if (!json) { showToast('Empty response', true); resolve(); return; } + if (json.success) {resolve(); return;} + if (json.info) { + parseInfo(json.info); + if (isInfo) populateInfo(json.info); + if (simplifiedUI) simplifyUI(); + } + var s = json.state ? json.state : json; + readState(s); + + reqsLegal = true; + resolve(); + }) + .catch((e)=>{ + if (retry<10) { + setTimeout(() => requestJson(command,retry+1).then(resolve).catch(reject), retry*50); + } else { + showToast(e, true); + resolve(); + } + }); }); } @@ -1879,7 +1871,7 @@ function makeSeg() function resetUtil(off=false) { gId('segutil').innerHTML = `
` - + '' + + '' + `
Add segment
` + '
' + `` @@ -1893,15 +1885,16 @@ function resetUtil(off=false) if (lSeg>2) d.querySelectorAll("#Segments .pop").forEach((e)=>{e.classList.remove("hide");}); } -function makePlSel(el, incPl=false) +function makePlSel(p, el) { var plSelContent = ""; delete pJson["0"]; // remove filler preset Object.entries(pJson).sort(cmpP).forEach((a)=>{ var n = a[1].n ? a[1].n : "Preset " + a[0]; + if (isPlaylist(a[1])) n += ' ▶'; // mark playlist if (cfg.comp.idsort) n = a[0] + ' ' + n; - if (!(!incPl && a[1].playlist && a[1].playlist.ps)) // skip playlists, sub-playlists not yet supported - plSelContent += ``; + // skip endless playlists and itself + if (!isPlaylist(a[1]) || (a[1].playlist.repeat > 0 && a[0]!=p)) plSelContent += ``; }); return plSelContent; } @@ -1926,21 +1919,23 @@ function refreshPlE(p) }); } -// p: preset ID, i: ps index +// p: preset ID, i: playlist item 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]); + const pl = plJson[p]; + pl.ps.splice(i+1,0,1); + pl.dur.splice(i+1,0,pl.dur[i]); + pl.transition.splice(i+1,0,pl.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); + const pl = plJson[p]; + if (pl.ps.length < 2) return; + pl.ps.splice(i,1); + pl.dur.splice(i,1); + pl.transition.splice(i,1); refreshPlE(p); } @@ -1957,6 +1952,13 @@ function pleDur(p,i,field) function pleTr(p,i,field) { + const du = gId(`pl${p}du${i}`); + const dv = parseFloat(du.value); + if (dv > 0) { + field.max = dv; + if (parseFloat(field.value) > dv) + field.value = du.value; + } if (field.validity.valid) plJson[p].transition[i] = Math.floor(field.value*10); } @@ -1976,6 +1978,17 @@ function plR(p) } } +function plM(p) +{ + const man = gId(`pl${p}manual`).checked; + plJson[p].dur.forEach((e,i)=>{ + const d = gId(`pl${p}du${i}`); + plJson[p].dur[i] = e = man ? 0 : 100; + d.value = e/10; // 10s default + d.readOnly = man; + }); +} + function makeP(i,pl) { var content = ""; @@ -1989,12 +2002,17 @@ function makeP(i,pl) r: false, end: 0 }; - var rep = plJson[i].repeat ? plJson[i].repeat : 0; + const rep = plJson[i].repeat ? plJson[i].repeat : 0; + const man = plJson[i].dur == 0; content = `
+
`; @@ -2078,25 +2096,26 @@ function makePUtil() function makePlEntry(p,i) { + const man = gId(`pl${p}manual`).checked; return `
- + - - + +
DurationDuration (0=inf.) Transition #${i+1}
ssss
@@ -2298,6 +2317,13 @@ function setSi(s) requestJson(obj); } +function setBm(s) +{ + var value = gId(`seg${s}bm`).selectedIndex; + var obj = {"seg": {"id": s, "bm": value}}; + requestJson(obj); +} + function setTp(s) { var tp = gId(`seg${s}tp`).checked; @@ -2491,7 +2517,7 @@ function saveP(i,pl) } populatePresets(); resetPUtil(); - setTimeout(()=>{pmtLast=0; loadPresets();}, 750); // force reloading of presets + setTimeout(()=>{loadPresets();}, 750); // force reloading of presets } function testPl(i,bt) { @@ -2649,28 +2675,28 @@ function fromRgb() 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]); + let cd = gId('csl').children[csel]; // color slots + cd.dataset.r = r; + cd.dataset.g = g; + cd.dataset.b = b; + setCSL(cd); } function fromW() { let w = gId('sliderW'); - let cd = gId('csl').children; // color slots - cd[csel].dataset.w = w.value; - setCSL(cd[csel]); + let cd = gId('csl').children[csel]; // color slots + cd.dataset.w = w.value; + setCSL(cd); 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; + var cd = gId('csl').children[csel]; // color slots + let cdd = cd.dataset; + let w = parseInt(cdd.w), r = parseInt(cdd.r), g = parseInt(cdd.g), b = parseInt(cdd.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; @@ -2678,7 +2704,7 @@ function setColor(sr) cdd.g = g = hasRGB ? col.g : w; cdd.b = b = hasRGB ? col.b : w; cdd.w = w; - setCSL(cd[csel]); + setCSL(cd); var obj = {"seg": {"col": [[],[],[]]}}; obj.seg.col[csel] = [r, g, b, w]; requestJson(obj); @@ -2721,7 +2747,7 @@ setInterval(()=>{ gId('heart').style.color = `hsl(${hc}, 100%, 50%)`; }, 910); -function openGH() { window.open("https://github.com/Aircoookie/WLED/wiki"); } +function openGH() { window.open("https://github.com/wled/WLED/wiki"); } var cnfr = false; function cnfReset() @@ -2757,56 +2783,51 @@ function rSegs() requestJson(obj); } -function loadPalettesData(callback = null) -{ - if (palettesData) return; - const lsKey = "wledPalx"; - var lsPalData = localStorage.getItem(lsKey); - if (lsPalData) { - try { - var d = JSON.parse(lsPalData); - if (d && d.vid == d.vid) { - palettesData = d.p; - if (callback) callback(); - return; - } - } catch (e) {} - } +function loadPalettesData() { + return new Promise((resolve) => { + if (palettesData) return resolve(); // already loaded + var lsPalData = localStorage.getItem("wledPalx"); + if (lsPalData) { + try { + var d = JSON.parse(lsPalData); + if (d && d.vid == lastinfo.vid) { + palettesData = d.p; + redrawPalPrev(); + return resolve(); + } + } catch (e) {} + } - palettesData = {}; - getPalettesData(0, ()=>{ - localStorage.setItem(lsKey, JSON.stringify({ - p: palettesData, - vid: lastinfo.vid - })); - redrawPalPrev(); - if (callback) setTimeout(callback, 99); + palettesData = {}; + getPalettesData(0, () => { + localStorage.setItem("wledPalx", JSON.stringify({ + p: palettesData, + vid: lastinfo.vid + })); + redrawPalPrev(); + setTimeout(resolve, 99); // delay optional + }); }); } -function getPalettesData(page, callback) -{ - fetch(getURL(`/json/palx?page=${page}`), { - method: 'get' - }) - .then(res => { - if (!res.ok) showErrorToast(); - return res.json(); - }) +function getPalettesData(page, callback, retry=0) { + fetch(getURL(`/json/palx?page=${page}`), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject()) .then(json => { - retry = false; palettesData = Object.assign({}, palettesData, json.p); if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 75); else callback(); }) .catch((error)=>{ - if (!retry) { - retry = true; - setTimeout(()=>{getPalettesData(page,callback);}, 500); // retry + if (retry<5) { + setTimeout(()=>{getPalettesData(page,callback,retry+1);}, 100); + } else { + showToast(error, true); + callback(); } - showToast(error, true); }); } + /* function hideModes(txt) { @@ -2908,7 +2929,7 @@ function filterFocus(e) { } if (e.type === "blur") { setTimeout(() => { - if (e.target === document.activeElement && document.hasFocus()) return; + if (e.target === d.activeElement && d.hasFocus()) return; // do not hide if filter is active if (!c) { // compute sticky top @@ -3023,11 +3044,9 @@ 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; + let found = false; + classList.forEach((e)=>{ if (e.startsWith('Iro')) found = true; }); + return found; } //required by rangetouch.js function lock(e) @@ -3090,7 +3109,6 @@ function togglePcMode(fromB = false) if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size() if (pcMode) openTab(0, true); gId('buttonPcm').className = (pcMode) ? "active":""; - if (pcMode && !ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide"); gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto"; sCol('--bh', gId('bot').clientHeight + "px"); _C.style.width = (pcMode || simplifiedUI)?'100%':'400%'; @@ -3117,7 +3135,7 @@ function mergeDeep(target, ...sources) function tooltip(cont=null) { d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ - element.addEventListener("mouseover", ()=>{ + element.addEventListener("pointerover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); const tooltip = d.createElement("span"); @@ -3142,7 +3160,7 @@ function tooltip(cont=null) tooltip.classList.add("visible"); }); - element.addEventListener("mouseout", ()=>{ + element.addEventListener("pointerout", ()=>{ d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); @@ -3158,7 +3176,7 @@ function simplifyUI() { // Create dropdown dialog function createDropdown(id, buttonText, dialogElements = null) { // Create dropdown dialog - const dialog = document.createElement("dialog"); + const dialog = d.createElement("dialog"); // Move every dialogElement to the dropdown dialog or if none are given, move all children of the element with the given id if (dialogElements) { dialogElements.forEach((e) => { @@ -3171,7 +3189,7 @@ function simplifyUI() { } // Create button for the dropdown - const btn = document.createElement("button"); + const btn = d.createElement("button"); btn.id = id + "btn"; btn.classList.add("btn"); btn.innerText = buttonText; @@ -3219,7 +3237,7 @@ function simplifyUI() { // Hide palette label gId("pall").style.display = "none"; - gId("Colors").insertBefore(document.createElement("br"), gId("pall")); + gId("Colors").insertBefore(d.createElement("br"), gId("pall")); // Hide effect label gId("modeLabel").style.display = "none"; @@ -3231,7 +3249,7 @@ function simplifyUI() { // Hide bottom bar gId("bot").style.display = "none"; - document.documentElement.style.setProperty('--bh', '0px'); + d.documentElement.style.setProperty('--bh', '0px'); // Hide other tabs gId("Effects").style.display = "none"; @@ -3245,6 +3263,197 @@ function simplifyUI() { gId("btns").style.display = "none"; } +// Version reporting feature +var versionCheckDone = false; + +function checkVersionUpgrade(info) { + // Only check once per page load + if (versionCheckDone) return; + versionCheckDone = true; + + // Suppress feature if in AP mode (no internet connection available) + if (info.wifi && info.wifi.ap) return; + + // Fetch version-info.json using existing /edit endpoint + fetch(getURL('/edit?func=edit&path=/version-info.json'), { + method: 'get' + }) + .then(res => { + if (res.status === 404) { + // File doesn't exist - first install, show install prompt + showVersionUpgradePrompt(info, null, info.ver); + return null; + } + if (!res.ok) { + throw new Error('Failed to fetch version-info.json'); + } + return res.json(); + }) + .then(versionInfo => { + if (!versionInfo) return; // 404 case already handled + + // Check if user opted out + if (versionInfo.neverAsk) return; + + // Check if version has changed + const currentVersion = info.ver; + const storedVersion = versionInfo.version || ''; + + if (storedVersion && storedVersion !== currentVersion) { + // Version has changed, show upgrade prompt + showVersionUpgradePrompt(info, storedVersion, currentVersion); + } else if (!storedVersion) { + // Empty version in file, show install prompt + showVersionUpgradePrompt(info, null, currentVersion); + } + }) + .catch(e => { + console.log('Failed to load version-info.json', e); + // On error, save current version for next time + if (info && info.ver) { + updateVersionInfo(info.ver, false); + } + }); +} + +function showVersionUpgradePrompt(info, oldVersion, newVersion) { + // Determine if this is an install or upgrade + const isInstall = !oldVersion; + + // Create overlay and dialog + const overlay = d.createElement('div'); + overlay.id = 'versionUpgradeOverlay'; + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;'; + + const dialog = d.createElement('div'); + dialog.style.cssText = 'background:var(--c-1);border-radius:10px;padding:25px;max-width:500px;margin:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3);'; + + // Build contextual message based on install vs upgrade + const title = isInstall + ? '🎉 Thank you for installing WLED!' + : '🎉 WLED Upgrade Detected!'; + + const description = isInstall + ? `You are now running WLED ${newVersion}.` + : `Your WLED has been upgraded from ${oldVersion} to ${newVersion}.`; + + const question = 'Help make WLED better with a one-time hardware report? It includes only device details like chip type, LED count, etc. — never personal data or your activities.' + + dialog.innerHTML = ` +

${title}

+

${description}

+

${question}

+

+ Learn more about what data is collected and why +

+
+ + + +
+ `; + + overlay.appendChild(dialog); + d.body.appendChild(overlay); + + // Add event listeners + gId('versionReportYes').addEventListener('click', () => { + reportUpgradeEvent(info, oldVersion); + d.body.removeChild(overlay); + }); + + gId('versionReportNo').addEventListener('click', () => { + // Don't update version, will ask again on next load + d.body.removeChild(overlay); + }); + + gId('versionReportNever').addEventListener('click', () => { + updateVersionInfo(newVersion, true); + d.body.removeChild(overlay); + showToast('You will not be asked again.'); + }); +} + +function reportUpgradeEvent(info, oldVersion) { + showToast('Reporting upgrade...'); + + // Fetch fresh data from /json/info endpoint as requested + fetch(getURL('/json/info'), { + method: 'get' + }) + .then(res => res.json()) + .then(infoData => { + // Map to UpgradeEventRequest structure per OpenAPI spec + // Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256 + const upgradeData = { + deviceId: infoData.deviceId, // Use anonymous unique device ID + version: infoData.ver || '', // Current version string + previousVersion: oldVersion || '', // Previous version from version-info.json + releaseName: infoData.release || '', // Release name (e.g., "WLED 0.15.0") + chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc) + ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs + isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup + bootloaderSHA256: infoData.bootloaderSHA256 || '', // Bootloader SHA256 hash + brand: infoData.brand, // Device brand (always present) + product: infoData.product, // Product name (always present) + flashSize: infoData.flash, // Flash size (always present) + repo: infoData.repo // GitHub repository (always present) + }; + + // Add optional fields if available + if (infoData.psrSz !== undefined) upgradeData.psramSize = infoData.psrSz; // Total PSRAM size in MB; can be 0 + + // Note: partitionSizes not currently available in /json/info endpoint + + // Make AJAX call to postUpgradeEvent API + return fetch('https://usage.wled.me/api/usage/upgrade', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(upgradeData) + }); + }) + .then(res => { + if (res.ok) { + showToast('Thank you for reporting!'); + updateVersionInfo(info.ver, false); + } else { + showToast('Report failed. Please try again later.', true); + // Do NOT update version info on failure - user will be prompted again + } + }) + .catch(e => { + console.log('Failed to report upgrade', e); + showToast('Report failed. Please try again later.', true); + // Do NOT update version info on error - user will be prompted again + }); +} + +function updateVersionInfo(version, neverAsk) { + const versionInfo = { + version: version, + neverAsk: neverAsk + }; + + // Create a Blob with JSON content and use /upload endpoint + const blob = new Blob([JSON.stringify(versionInfo)], {type: 'application/json'}); + const formData = new FormData(); + formData.append('data', blob, 'version-info.json'); + + fetch(getURL('/upload'), { + method: 'POST', + body: formData + }) + .then(res => res.text()) + .then(data => { + console.log('Version info updated', data); + }) + .catch(e => { + console.log('Failed to update version-info.json', e); + }); +} + size(); _C.style.setProperty('--n', N); @@ -3256,4 +3465,4 @@ _C.addEventListener('touchstart', lock, false); _C.addEventListener('mouseout', move, false); _C.addEventListener('mouseup', move, false); -_C.addEventListener('touchend', move, false); +_C.addEventListener('touchend', move, false); \ No newline at end of file diff --git a/wled00/data/liveview.htm b/wled00/data/liveview.htm index 8c10ba9624..6515f5d56a 100644 --- a/wled00/data/liveview.htm +++ b/wled00/data/liveview.htm @@ -1,5 +1,5 @@ - + @@ -18,7 +18,14 @@ } - + - + \ No newline at end of file diff --git a/wled00/data/liveviewws2D.htm b/wled00/data/liveviewws2D.htm index c50f40fbc6..2b66b47724 100644 --- a/wled00/data/liveviewws2D.htm +++ b/wled00/data/liveviewws2D.htm @@ -1,5 +1,5 @@ - + @@ -14,6 +14,14 @@ + + \ No newline at end of file diff --git a/wled00/data/pxmagic/pxmagic.htm b/wled00/data/pxmagic/pxmagic.htm index 8ec11f454a..f8de51198a 100644 --- a/wled00/data/pxmagic/pxmagic.htm +++ b/wled00/data/pxmagic/pxmagic.htm @@ -1977,4 +1977,4 @@ return isValid; } - + \ No newline at end of file diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm index 82c7782140..ce3c246bad 100644 --- a/wled00/data/settings.htm +++ b/wled00/data/settings.htm @@ -4,17 +4,23 @@ WLED Settings - - + - - + + + diff --git a/wled00/data/settings_2D.htm b/wled00/data/settings_2D.htm index a70ca76bea..76f84fa61b 100644 --- a/wled00/data/settings_2D.htm +++ b/wled00/data/settings_2D.htm @@ -4,11 +4,20 @@ 2D Set-up - + - - +

2D setup

+
Strip or panel:
+ +

diff --git a/wled00/data/settings_dmx.htm b/wled00/data/settings_dmx.htm index e800be6ea1..391c2bdc97 100644 --- a/wled00/data/settings_dmx.htm +++ b/wled00/data/settings_dmx.htm @@ -4,9 +4,17 @@ DMX Settings - + - - +
diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index baf80a5d79..c10affc545 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -4,10 +4,9 @@ LED Settings - + - - - -
-
-
-
-

LED & Hardware setup

+ + +
+
+
+
+

LED setup

+
Total LEDs: ?
Recommended power supply for brightest white:
?


+ Global brightness factor: %
Enable automatic brightness limiter:
Automatically limits brightness to stay close to the limit.
- Keep at <1A if poweing LEDs directly from the ESP 5V pin!
+ Keep at <1A if powering LEDs directly from the ESP 5V pin!
If using multiple outputs it is recommended to use per-output limiter.
Analog (PWM) and virtual LEDs cannot use automatic brightness limiter.
Maximum PSU Current: mA
- Use per-output limiter:
+ Use per-output limiter:

-

Hardware setup

-
LED outputs:
+

LED outputs:



@@ -781,23 +1058,60 @@

Hardware setup

⚠ You might run into stability or lag issues.
Use less than 800 LEDs per output for the best experience!
+
+ Show Advanced Settings
Make a segment for each output:
Custom bus start indices:
- Use global LED buffer:

Color Order Override:

-
+ +
+
+
+

Color & White

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

+ White Balance correction:
+
+ Global override for Auto-calculate white: + +
+ Calculate CCT from RGB:
+ CCT IC used (Athom 15W):
+ CCT blending (±100%): %
+ Positive: additive blend, Negative: exclusive blend
+ Set to 0 when using 2-wire (reverse polarity) CCT strips

+
+
+

Hardware setup

+
+

Buttons

+
+
+
+ +
-
-
Disable internal pull-up/down:
- Touch threshold:
-
+ Touch threshold:

+
+
+

IR Remote

IR GPIO:  ✕
Apply IR change to main segment only:
- IR info
-
+ IR info

+
+
+

Relay

Relay GPIO:  ✕
- Invert Open drain
-
-

Defaults

+ Invert Open drain

+
+

General settings

+
+

Power up

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

- Apply preset at boot (0 uses values from above)

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

- Brightness factor: % + with brightness: (1-255)
+ (disable if using boot preset to turn LEDs on)

+ Apply preset at boot (0 = none)

+
+

Transitions

- Enable transitions:
- - Effect blending:
- Default transition time: ms
- Palette transitions:
-
- Random Cycle Palette Time: s
- Use harmonic Random Cycle Palette:
+ Default transition time: ms

+
+
+

Random Palettes

+ Use harmonic colors in Random palettes:
+ Random Palette Cycle Time: s

+
+

Timed light

Default duration: min
Default target brightness:
@@ -843,24 +1160,9 @@

Timed light

-

White management

- White Balance correction:
-
- Global override for Auto-calculate white:
- -
- Calculate CCT from RGB:
- CCT IC used (Athom 15W):
- CCT additive blending: %
- WARNING: When using H-bridge for reverse polarity (2-wire) CCT LED strip
make sure this value is 0.
(ESP32 variants only, ESP8266 does not support H-bridges)
-
+

+
+

Advanced

Palette wrapping:
- Target refresh rate: FPS -
-
Config template:
-
- - -
+ Target refresh rate: FPS + + + +

+
+
Config template:
+
+ + +
diff --git a/wled00/data/settings_pin.htm b/wled00/data/settings_pin.htm index 22f94e3790..a6d8984c34 100644 --- a/wled00/data/settings_pin.htm +++ b/wled00/data/settings_pin.htm @@ -14,10 +14,12 @@
+

Please enter settings PIN code


+
\ No newline at end of file diff --git a/wled00/data/settings_pininfo.htm b/wled00/data/settings_pininfo.htm new file mode 100644 index 0000000000..7fe4e889bb --- /dev/null +++ b/wled00/data/settings_pininfo.htm @@ -0,0 +1,101 @@ + + + + + + Pin Info + + + + + +

Pin Info

+
Loading...
+ + + diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index 7fe0fe62ee..cca6e000ad 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -4,8 +4,16 @@ 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:
@@ -48,16 +54,21 @@

Security & Update setup

To enable OTA, for security reasons you need to also enter the correct password!
The password should be changed when OTA is enabled.
Disable OTA when not in use, otherwise an attacker can reflash device software!
- Settings on this page are only changable if OTA lock is disabled!
+ Settings on this page are only changeable if OTA lock is disabled!
Deny access to WiFi settings if locked:

Factory reset:
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:
+ Only allow update from same network/WiFi:
+ ⚠ If you are using multiple VLANs (i.e. IoT or guest network) either set PIN or disable this option.
+ Disabling this option will make your device less secure.

+
+

Backup & Restore

⚠ Restoring presets/configuration will OVERWRITE your current presets/configuration.
Incorrect upload or configuration may require a factory reset or re-flashing of your ESP.
@@ -66,14 +77,16 @@

Backup & Restore

Restore presets


Backup configuration
Restore configuration

-
+
+

About

- WLED version ##VERSION##

- Contributors, dependencies and special thanks
+ WLED version ##VERSION##

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

(c) 2016-2024 Christian Schwinne
- Licensed under the EUPL v1.2 license

- Server message: Response error!
+ Licensed under the EUPL v1.2 license

+ Installed version: WLED ##VERSION## +
diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index 34b9fc6cdb..43baaf6718 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -4,8 +4,16 @@ Sync Settings - + - - +

Sync setup

+

WLED Broadcast

UDP Port:
2nd Port:
+
+
+

ESP-NOW

-ESP-NOW support is disabled.
+Disabled. Enable ESP-NOW in WiFi settings.
Use ESP-NOW sync:
(in AP mode or no WiFi)
+
+

Sync groups

@@ -97,9 +113,13 @@

Sync groups

+
+

Receive

Brightness, Color, Effects, and Palette
Segment options, bounds +
+

Send

Enable Sync on start:
Send notifications on direct change:
@@ -108,11 +128,13 @@

Send

Send Philips Hue change notifications:
UDP packet retransmissions:

Reboot required to apply changes. -
+
+

Instance List

Enable instance list:
Make this instance discoverable: -
+
+

Realtime

Receive UDP realtime:
Use main segment only:
@@ -151,7 +173,21 @@

Realtime

Force max brightness:
Disable realtime gamma correction:
Realtime LED offset: -
+
+

Wired DMX Input Pins

+ DMX RX: RO
+ DMX TX: DI
+ DMX Enable: RE+DE
+ DMX Port:
+
+
+
This firmware build does not include DMX Input support.
+
+
+
This firmware build does not include DMX output support.
+
+
+

Alexa Voice Assistant

This firmware build does not include Alexa support.

@@ -161,13 +197,15 @@

Alexa Voice Assistant

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

-
-
MQTT and Hue sync all connect to external hosts!
+
+ +
MQTT and Hue sync 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) -
+ +

MQTT

This firmware build does not include MQTT support.
@@ -187,6 +225,8 @@

MQTT

Retain brightness & color messages:
Reboot required to apply changes. MQTT info
+
+

Philips Hue

This firmware build does not include Philips Hue support.
@@ -204,6 +244,8 @@

Philips Hue

(when first connecting)
Hue status: Disabled in this build
+
+

Serial

This firmware build does not support Serial interface.
@@ -222,6 +264,7 @@

Serial


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 df054f4174..b7848b862b 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -4,10 +4,19 @@ Time Settings - + - - -
-
-
-
-
-

Time setup

+ + +
+
+
+
+

Time setup

+
Get time from NTP server:

Use 24h format:
@@ -157,6 +166,7 @@

Time setup

+
UTC offset: seconds (max. 18 hours)
Current local time is unknown.
@@ -165,11 +175,13 @@

Time setup

(opens new tab, only works in browser)
+
+

Clock

Analog Clock overlay:
- First LED: Last LED:
- 12h LED:
+ First LED: Last LED:
+ 12h LED:
Show 5min marks:
Seconds (as trail):
Show clock overlay only if all LEDs are solid black:
@@ -178,15 +190,22 @@

Clock

Countdown Goal:
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:
+
+
+

Macro Presets

+ Presets can be used as macros for both JSON and HTTP API commands.
+ Enter the preset ID below.
+ Use 0 for the default action instead of a preset
+ JSON API
+ HTTP API
+
+

Timer & Alexa Presets

Countdown-Over Preset:
Timed-Light-Over Presets:
-

Button actions

+ Alexa On/Off Preset:
+
+
+

Button Action Presets

@@ -200,12 +219,15 @@

Button actions

Analog Button setup -

Time-controlled presets

+
+
+

Time-Controlled Presets

-
- - +
+
+ + diff --git a/wled00/data/settings_ui.htm b/wled00/data/settings_ui.htm index cb7ded7e67..d133aaf844 100644 --- a/wled00/data/settings_ui.htm +++ b/wled00/data/settings_ui.htm @@ -4,10 +4,19 @@ UI Settings - + - - -
-
-
-
- -
-
-

Web Setup

- Server description:
- + + +
+
+
+ +
+
+

User Interface

+
+ Device Name:
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

:
+ :
:
:
:
@@ -238,10 +249,13 @@

UI Appearance

:
:
:
- :
I hate dark mode:
:
+ :
+
Custom CSS:
+ +

UI Background

:
:
BG image:
@@ -257,10 +271,9 @@

Random BG image settings

:
- :
-
Custom CSS:
:
Holidays:
+


diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index c2f0ffbf2e..bb2113ddbc 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -4,16 +4,26 @@ Usermod Settings - + - - +
@@ -281,18 +291,20 @@

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?
+
+

Global I2C & SPI

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

diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm index 30b6600ae2..614d795329 100644 --- a/wled00/data/settings_wifi.htm +++ b/wled00/data/settings_wifi.htm @@ -4,8 +4,17 @@ WiFi Settings - + - - -
-
-
-
-
-

WiFi setup

-

Connect to existing network

+ + +
+
+
+
+

WiFi & Network Settings

+
+

Wireless network


- Wireless networks


+
+
+

Ethernet Type

+

+
+
+

DNS & mDNS

DNS server address:
...

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

Configure Access Point

AP SSID (leave empty for no AP):

Hide AP name:
@@ -175,12 +269,14 @@

Configure Access Point


AP IP: Not active
-

Experimental

+
+
+

WiFi Power

Force 802.11g mode (ESP8266 only):
Disable WiFi sleep:
- Can help with connectivity issues and Audioreactive sync.
- Disabling WiFi sleep increases power consumption.

-
TX power: @@ -195,39 +291,27 @@

Experimental


WARNING: Modifying TX power may render device unreachable.
- +
+

ESP-NOW Wireless

This firmware build does not include ESP-NOW support.
- Enable ESP-NOW:
+ Enable ESP-NOW:
Listen for events over ESP-NOW
- Keep disabled if not using a remote or wireless sync, increases power consumption.
- Paired Remote MAC:
- Last device seen: None
-
- -
-

Ethernet Type

-

+ Keep disabled if not using a remote or ESP-NOW sync, increases power consumption.
+
+ Last device seen: None +
+ Linked MACs (10 max):
+
+
+
-
- - +
+
+ + diff --git a/wled00/data/style.css b/wled00/data/style.css index b6cb0f9e61..69bc5aa96b 100644 --- a/wled00/data/style.css +++ b/wled00/data/style.css @@ -5,7 +5,7 @@ body { font-family: Verdana, sans-serif; font-size: 1rem; text-align: center; - background: #222; + background: #111; color: #fff; line-height: 200%; margin: 0; @@ -23,6 +23,13 @@ a, a:hover { color: #28f; text-decoration: none; } +.sec { + background: #222; + border-radius: 20px; + padding: 8px; + margin: 12px auto; + max-width: 520px; +} button, .btn { background: #333; color: #fff; @@ -36,6 +43,7 @@ button, .btn { min-width: 48px; cursor: pointer; text-decoration: none; + transition: all 0.3s ease; } button.sml { padding: 8px; @@ -44,6 +52,11 @@ button.sml { min-width: 40px; margin: 0 0 0 10px; } +button:hover, .btn:hover{ + background:#555; + border-color:#555; +} + #scan { margin-top: -10px; } @@ -79,6 +92,9 @@ input { input:disabled { color: #888; } +input:invalid { + color: #f00; +} input[type="text"], input[type="number"], input[type="password"], @@ -202,4 +218,4 @@ td { #btns select { width: 144px; } -} \ No newline at end of file +} diff --git a/wled00/data/update.htm b/wled00/data/update.htm index b68645a527..f2143a372c 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -1,29 +1,85 @@ - + WLED Update + - - -

WLED Software Update

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

WLED Software Update

+ + Installed version: Loading...
+ Release: Loading...
+ Download the latest binary:
-
+
+
+ +

+
+
-
Updating...
Please do not close or refresh the page :)
+
+
+

Bootloader Update

+
+
+ Warning: Only upload verified ESP32 bootloader files!
+
+ +
+
+
Updating...
Please do not close or refresh the page :)
\ No newline at end of file diff --git a/wled00/data/usermod.htm b/wled00/data/usermod.htm index 3f7734a724..53aca464cb 100644 --- a/wled00/data/usermod.htm +++ b/wled00/data/usermod.htm @@ -1,5 +1,5 @@ - + No usermod custom web page set. diff --git a/wled00/data/welcome.htm b/wled00/data/welcome.htm index 671212a388..3c9f0a74e0 100644 --- a/wled00/data/welcome.htm +++ b/wled00/data/welcome.htm @@ -1,5 +1,5 @@ - + @@ -52,9 +52,10 @@

Welcome to WLED!

-

Thank you for installing my application!

+

A versatile tool for controlling LEDs

+

Find out more at wled.me

Next steps:

- Connect the module to your local WiFi here!
+ Connect to your local WiFi here!

Just trying this out in AP mode?

diff --git a/wled00/dmx_input.cpp b/wled00/dmx_input.cpp new file mode 100644 index 0000000000..83ab606688 --- /dev/null +++ b/wled00/dmx_input.cpp @@ -0,0 +1,280 @@ +#include "wled.h" + +#ifdef WLED_ENABLE_DMX_INPUT + +#ifdef ESP8266 +#error DMX input is only supported on ESP32 +#endif + +#include "dmx_input.h" +#include + +void rdmPersonalityChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, + void *context) +{ + DMXInput *dmx = static_cast(context); + + if (!dmx) { + DEBUG_PRINTLN("DMX: Error: no context in rdmPersonalityChangedCb"); + return; + } + + if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) { + const uint8_t personality = dmx_get_current_personality(dmx->inputPortNum); + DMXMode = std::min(DMX_MODE_PRESET, std::max(DMX_MODE_SINGLE_RGB, int(personality))); + configNeedsWrite = true; + DEBUG_PRINTF("DMX personality changed to to: %d\n", DMXMode); + } +} + +void rdmAddressChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, + void *context) +{ + DMXInput *dmx = static_cast(context); + + if (!dmx) { + DEBUG_PRINTLN("DMX: Error: no context in rdmAddressChangedCb"); + return; + } + + if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) { + const uint16_t addr = dmx_get_start_address(dmx->inputPortNum); + DMXAddress = std::min(512, int(addr)); + configNeedsWrite = true; + DEBUG_PRINTF("DMX start addr changed to: %d\n", DMXAddress); + } +} + +static dmx_config_t createConfig() +{ + dmx_config_t config; + config.pd_size = 255; + config.dmx_start_address = DMXAddress; + config.model_id = 0; + config.product_category = RDM_PRODUCT_CATEGORY_FIXTURE; + config.software_version_id = VERSION; + strcpy(config.device_label, "WLED_MM"); + + const std::string dmxWledVersionString = "WLED_V" + std::to_string(VERSION); + strncpy(config.software_version_label, dmxWledVersionString.c_str(), 32); + config.software_version_label[32] = '\0'; // zero termination in case versionString string was longer than 32 chars + + config.personalities[0].description = "SINGLE_RGB"; + config.personalities[0].footprint = 3; + config.personalities[1].description = "SINGLE_DRGB"; + config.personalities[1].footprint = 4; + config.personalities[2].description = "EFFECT"; + config.personalities[2].footprint = 15; + config.personalities[3].description = "MULTIPLE_RGB"; + config.personalities[3].footprint = std::min(512, int(strip.getLengthTotal()) * 3); + config.personalities[4].description = "MULTIPLE_DRGB"; + config.personalities[4].footprint = std::min(512, int(strip.getLengthTotal()) * 3 + 1); + config.personalities[5].description = "MULTIPLE_RGBW"; + config.personalities[5].footprint = std::min(512, int(strip.getLengthTotal()) * 4); + config.personalities[6].description = "EFFECT_W"; + config.personalities[6].footprint = 18; + config.personalities[7].description = "EFFECT_SEGMENT"; + config.personalities[7].footprint = std::min(512, strip.getSegmentsNum() * 15); + config.personalities[8].description = "EFFECT_SEGMENT_W"; + config.personalities[8].footprint = std::min(512, strip.getSegmentsNum() * 18); + config.personalities[9].description = "PRESET"; + config.personalities[9].footprint = 1; + + config.personality_count = 10; + // rdm personalities are numbered from 1, thus we can just set the DMXMode directly. + config.current_personality = DMXMode; + + return config; +} + +void dmxReceiverTask(void *context) +{ + DMXInput *instance = static_cast(context); + if (instance == nullptr) { + return; + } + + if (instance->installDriver()) { + while (true) { + instance->updateInternal(); + } + } +} + +bool DMXInput::installDriver() +{ + + const auto config = createConfig(); + DEBUG_PRINTF("DMX port: %u\n", inputPortNum); + if (!dmx_driver_install(inputPortNum, &config, DMX_INTR_FLAGS_DEFAULT)) { + DEBUG_PRINTF("Error: Failed to install dmx driver\n"); + return false; + } + + DEBUG_PRINTF("Listening for DMX on pin %u\n", rxPin); + DEBUG_PRINTF("Sending DMX on pin %u\n", txPin); + DEBUG_PRINTF("DMX enable pin is: %u\n", enPin); + dmx_set_pin(inputPortNum, txPin, rxPin, enPin); + + rdm_register_dmx_start_address(inputPortNum, rdmAddressChangedCb, this); + rdm_register_dmx_personality(inputPortNum, rdmPersonalityChangedCb, this); + initialized = true; + return true; +} + +void DMXInput::init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum) +{ + +#ifdef WLED_ENABLE_DMX_OUTPUT + //TODO add again once dmx output has been merged + // if(inputPortNum == dmxOutputPort) + // { + // DEBUG_PRINTF("DMXInput: Error: Input port == output port"); + // return; + // } +#endif + + if (inputPortNum <= (SOC_UART_NUM - 1) && inputPortNum > 0) { + this->inputPortNum = inputPortNum; + } + else { + DEBUG_PRINTF("DMXInput: Error: invalid inputPortNum: %d\n", inputPortNum); + return; + } + + if (rxPin > 0 && enPin > 0 && txPin > 0) { + + const managed_pin_type pins[] = { + {(int8_t)txPin, false}, // these are not used as gpio pins, thus isOutput is always false. + {(int8_t)rxPin, false}, + {(int8_t)enPin, false}}; + const bool pinsAllocated = PinManager::allocateMultiplePins(pins, 3, PinOwner::DMX_INPUT); + if (!pinsAllocated) { + DEBUG_PRINTF("DMXInput: Error: Failed to allocate pins for DMX_INPUT. Pins already in use:\n"); + DEBUG_PRINTF("rx in use by: %s\n", PinManager::getPinOwner(rxPin)); + DEBUG_PRINTF("tx in use by: %s\n", PinManager::getPinOwner(txPin)); + DEBUG_PRINTF("en in use by: %s\n", PinManager::getPinOwner(enPin)); + return; + } + + this->rxPin = rxPin; + this->txPin = txPin; + this->enPin = enPin; + + // put dmx receiver into seperate task because it should not be blocked + // pin to core 0 because wled is running on core 1 + xTaskCreatePinnedToCore(dmxReceiverTask, "DMX_RCV_TASK", 10240, this, 2, &task, 0); + if (!task) { + DEBUG_PRINTF("Error: Failed to create dmx rcv task"); + } + } + else { + DEBUG_PRINTLN("DMX input disabled due to rxPin, enPin or txPin not set"); + return; + } +} + +void DMXInput::updateInternal() +{ + if (!initialized) { + return; + } + + checkAndUpdateConfig(); + + dmx_packet_t packet; + unsigned long now = millis(); + if (dmx_receive(inputPortNum, &packet, DMX_TIMEOUT_TICK)) { + if (!packet.err) { + if(!connected) { + DEBUG_PRINTLN("DMX Input - connected"); + } + connected = true; + identify = isIdentifyOn(); + if (!packet.is_rdm) { + const std::lock_guard lock(dmxDataLock); + dmx_read(inputPortNum, dmxdata, packet.size); + } + } + else { + connected = false; + } + } + else { + if(connected) { + DEBUG_PRINTLN("DMX Input - disconnected"); + } + connected = false; + } +} + + +void DMXInput::update() +{ + if (identify) { + turnOnAllLeds(); + } + else if (connected) { + const std::lock_guard lock(dmxDataLock); + handleDMXData(1, 512, dmxdata, REALTIME_MODE_DMX, 0); + } +} + +void DMXInput::turnOnAllLeds() +{ + // TODO not sure if this is the correct way? + const uint16_t numPixels = strip.getLengthTotal(); + for (uint16_t i = 0; i < numPixels; ++i) + { + strip.setPixelColor(i, 255, 255, 255, 255); + } + strip.setBrightness(255, true); + strip.show(); +} + +void DMXInput::disable() +{ + if (initialized) { + dmx_driver_disable(inputPortNum); + } +} +void DMXInput::enable() +{ + if (initialized) { + dmx_driver_enable(inputPortNum); + } +} + +bool DMXInput::isIdentifyOn() const +{ + + uint8_t identify = 0; + const bool gotIdentify = rdm_get_identify_device(inputPortNum, &identify); + // gotIdentify should never be false because it is a default parameter in rdm + // but just in case we check for it anyway + return bool(identify) && gotIdentify; +} + +void DMXInput::checkAndUpdateConfig() +{ + + /** + * The global configuration variables are modified by the web interface. + * If they differ from the driver configuration, we have to update the driver + * configuration. + */ + + const uint8_t currentPersonality = dmx_get_current_personality(inputPortNum); + if (currentPersonality != DMXMode) { + DEBUG_PRINTF("DMX personality has changed from %d to %d\n", currentPersonality, DMXMode); + dmx_set_current_personality(inputPortNum, DMXMode); + } + + const uint16_t currentAddr = dmx_get_start_address(inputPortNum); + if (currentAddr != DMXAddress) { + DEBUG_PRINTF("DMX address has changed from %d to %d\n", currentAddr, DMXAddress); + dmx_set_start_address(inputPortNum, DMXAddress); + } +} + +#endif \ No newline at end of file diff --git a/wled00/dmx_input.h b/wled00/dmx_input.h new file mode 100644 index 0000000000..29f015bdc8 --- /dev/null +++ b/wled00/dmx_input.h @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include +#include + +/* + * Support for DMX/RDM input via serial (e.g. max485) on ESP32 + * ESP32 Library from: + * https://github.com/someweisguy/esp_dmx + */ +class DMXInput +{ +public: + void init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum); + void update(); + + /**disable dmx receiver (do this before disabling the cache)*/ + void disable(); + void enable(); + + /// True if dmx is currently connected + bool isConnected() const { return connected; } + +private: + /// @return true if rdm identify is active + bool isIdentifyOn() const; + + /** + * Checks if the global dmx config has changed and updates the changes in rdm + */ + void checkAndUpdateConfig(); + + /// overrides everything and turns on all leds + void turnOnAllLeds(); + + /// installs the dmx driver + /// @return false on fail + bool installDriver(); + + /// is called by the dmx receive task regularly to receive new dmx data + void updateInternal(); + + // is invoked whenver the dmx start address is changed via rdm + friend void rdmAddressChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, + void *context); + + // is invoked whenever the personality is changed via rdm + friend void rdmPersonalityChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, + void *context); + + /// The internal dmx task. + /// This is the main loop of the dmx receiver. It never returns. + friend void dmxReceiverTask(void * context); + + uint8_t inputPortNum = 255; + uint8_t rxPin = 255; + uint8_t txPin = 255; + uint8_t enPin = 255; + + /// is written to by the dmx receive task. + byte dmxdata[DMX_PACKET_SIZE]; + /// True once the dmx input has been initialized successfully + bool initialized = false; // true once init finished successfully + /// True if dmx is currently connected + std::atomic connected{false}; + std::atomic identify{false}; + /// Timestamp of the last time a dmx frame was received + unsigned long lastUpdate = 0; + + /// Taskhandle of the dmx task that is running in the background + TaskHandle_t task; + /// Guards access to dmxData + std::mutex dmxDataLock; + +}; diff --git a/wled00/dmx.cpp b/wled00/dmx_output.cpp similarity index 93% rename from wled00/dmx.cpp rename to wled00/dmx_output.cpp index dbe70f2aac..eace2145e6 100644 --- a/wled00/dmx.cpp +++ b/wled00/dmx_output.cpp @@ -1,7 +1,7 @@ #include "wled.h" /* - * Support for DMX Output via MAX485. + * Support for DMX output via serial (e.g. 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: @@ -12,7 +12,7 @@ #ifdef WLED_ENABLE_DMX -void handleDMX() +void handleDMXOutput() { // don't act, when in DMX Proxy mode if (e131ProxyUniverse != 0) return; @@ -68,11 +68,14 @@ void handleDMX() dmx.update(); // update the DMX bus } -void initDMX() { +void initDMXOutput() { #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) dmx.init(512); // initialize with bus length #else dmx.initWrite(512); // initialize with bus length #endif } +#else +void initDMXOutput(){} +void handleDMXOutput() {} #endif diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 7c074759e7..8029feec3e 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -4,13 +4,20 @@ #define MAX_4_CH_LEDS_PER_UNIVERSE 128 #define MAX_CHANNELS_PER_UNIVERSE 512 +// forward declarations +static void handleDDPPacket(e131_packet_t* p); +static void handleArtnetPollReply(IPAddress ipAddress); +static void prepareArtnetPollReply(ArtPollReply *reply); +static void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress); + + /* * E1.31 handler */ //DDP protocol support, called by handleE131Packet //handles RGB data only -void handleDDPPacket(e131_packet_t* p) { +static void handleDDPPacket(e131_packet_t* p) { static bool ddpSeenPush = false; // have we seen a push yet? int lastPushSeq = e131LastSequenceNumber[0]; @@ -30,15 +37,23 @@ void handleDDPPacket(e131_packet_t* p) { uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed; start += DMXAddress / ddpChannelsPerLed; - unsigned stop = start + htons(p->dataLen) / ddpChannelsPerLed; + uint16_t dataLen = htons(p->dataLen); + unsigned stop = start + dataLen / ddpChannelsPerLed; uint8_t* data = p->data; unsigned c = 0; if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later + unsigned numLeds = stop - start; // stop >= start is guaranteed + unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array + if (maxDataIndex > dataLen) { + DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting.")); + return; + } + if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); - if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) { + if (!realtimeOverride) { for (unsigned i = start; i < stop; i++, c += ddpChannelsPerLed) { setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0); } @@ -115,6 +130,11 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // update status info realtimeIP = clientIP; + + handleDMXData(uni, dmxChannels, e131_data, mde, previousUniverses); +} + +void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses) { byte wChannel = 0; unsigned totalLen = strip.getLengthTotal(); unsigned availDMXLen = 0; @@ -129,7 +149,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } // DMX data in Art-Net packet starts at index 0, for E1.31 at index 1 - if (protocol == P_ARTNET && dataOffset > 0) { + if (mde == REALTIME_MODE_ARTNET && dataOffset > 0) { dataOffset--; } @@ -144,7 +164,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; wChannel = (availDMXLen > 3) ? e131_data[dataOffset+3] : 0; for (unsigned i = 0; i < totalLen; i++) @@ -156,7 +176,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (availDMXLen < 4) return; realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; wChannel = (availDMXLen > 4) ? e131_data[dataOffset+4] : 0; if (bri != e131_data[dataOffset+0]) { @@ -186,7 +206,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // only change brightness if value changed if (bri != e131_data[dataOffset]) { bri = e131_data[dataOffset]; - strip.setBrightness(scaledBri(bri), false); + strip.setBrightness(bri, false); stateUpdated(CALL_MODE_WS_SEND); } return; @@ -208,7 +228,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ else dataOffset = DMXAddress; // Modify address for Art-Net data - if (protocol == P_ARTNET && dataOffset > 0) + if (mde == REALTIME_MODE_ARTNET && dataOffset > 0) dataOffset--; // Skip out of universe addresses if (dataOffset > dmxChannels - dmxEffectChannels + 1) @@ -220,16 +240,16 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ 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]); - if ((e131_data[dataOffset+5] & 0b00000010) != seg.reverse_y) { seg.setOption(SEG_OPTION_REVERSED_Y, e131_data[dataOffset+5] & 0b00000010); } - if ((e131_data[dataOffset+5] & 0b00000100) != seg.mirror_y) { seg.setOption(SEG_OPTION_MIRROR_Y, e131_data[dataOffset+5] & 0b00000100); } - if ((e131_data[dataOffset+5] & 0b00001000) != seg.transpose) { seg.setOption(SEG_OPTION_TRANSPOSED, e131_data[dataOffset+5] & 0b00001000); } - if ((e131_data[dataOffset+5] & 0b00110000) / 8 != seg.map1D2D) { - seg.map1D2D = (e131_data[dataOffset+5] & 0b00110000) / 8; + if (bool(e131_data[dataOffset+5] & 0b00000010) != seg.reverse_y) { seg.reverse_y = bool(e131_data[dataOffset+5] & 0b00000010); } + if (bool(e131_data[dataOffset+5] & 0b00000100) != seg.mirror_y) { seg.mirror_y = bool(e131_data[dataOffset+5] & 0b00000100); } + if (bool(e131_data[dataOffset+5] & 0b00001000) != seg.transpose) { seg.transpose = bool(e131_data[dataOffset+5] & 0b00001000); } + if ((e131_data[dataOffset+5] & 0b00110000) >> 4 != seg.map1D2D) { + seg.map1D2D = (e131_data[dataOffset+5] & 0b00110000) >> 4; } // To maintain backwards compatibility with prior e1.31 values, reverse is fixed to mask 0x01000000 - if ((e131_data[dataOffset+5] & 0b01000000) != seg.reverse) { seg.setOption(SEG_OPTION_REVERSED, e131_data[dataOffset+5] & 0b01000000); } + if ((e131_data[dataOffset+5] & 0b01000000) != seg.reverse) { seg.reverse = bool(e131_data[dataOffset+5] & 0b01000000); } // To maintain backwards compatibility with prior e1.31 values, mirror is fixed to mask 0x10000000 - if ((e131_data[dataOffset+5] & 0b10000000) != seg.mirror) { seg.setOption(SEG_OPTION_MIRROR, e131_data[dataOffset+5] & 0b10000000); } + if ((e131_data[dataOffset+5] & 0b10000000) != seg.mirror) { seg.mirror = bool(e131_data[dataOffset+5] & 0b10000000); } uint32_t colors[3]; byte whites[3] = {0,0,0}; @@ -263,7 +283,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ case DMX_MODE_MULTIPLE_RGB: case DMX_MODE_MULTIPLE_RGBW: { - bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); + const bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3; const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; uint8_t stripBrightness = bri; @@ -282,7 +302,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } } else { // All subsequent universes start at the first channel. - dmxOffset = (protocol == P_ARTNET) ? 0 : 1; + dmxOffset = (mde == REALTIME_MODE_ARTNET) ? 0 : 1; const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; previousLeds = ledsInFirstUniverse + (previousUniverses - 1) * ledsPerUniverse; @@ -295,7 +315,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; if (ledsTotal > totalLen) { ledsTotal = totalLen; @@ -308,16 +328,9 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } } - if (!is4Chan) { - for (unsigned i = previousLeds; i < ledsTotal; i++) { - setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], 0); - dmxOffset+=3; - } - } else { - for (unsigned i = previousLeds; i < ledsTotal; i++) { - setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], e131_data[dmxOffset+3]); - dmxOffset+=4; - } + for (unsigned i = previousLeds; i < ledsTotal; i++) { + setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], is4Chan ? e131_data[dmxOffset+3] : 0); + dmxOffset += dmxChannelsPerLed; } break; } @@ -330,7 +343,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ e131NewData = true; } -void handleArtnetPollReply(IPAddress ipAddress) { +static void handleArtnetPollReply(IPAddress ipAddress) { ArtPollReply artnetPollReply; prepareArtnetPollReply(&artnetPollReply); @@ -396,7 +409,7 @@ void handleArtnetPollReply(IPAddress ipAddress) { #endif } -void prepareArtnetPollReply(ArtPollReply *reply) { +static void prepareArtnetPollReply(ArtPollReply *reply) { // Art-Net reply->reply_id[0] = 0x41; reply->reply_id[1] = 0x72; @@ -416,7 +429,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) { reply->reply_port = ARTNET_DEFAULT_PORT; - char * numberEnd = versionString; + char * numberEnd = (char*) versionString; // strtol promises not to try to edit this. reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10); numberEnd++; reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10); @@ -515,12 +528,12 @@ void prepareArtnetPollReply(ArtPollReply *reply) { } } -void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress) { +static 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); + snprintf_P((char *)reply->reply_node_report, sizeof(reply->reply_node_report)-1, PSTR("#0001 [%04u] OK - WLED v%s"), pollReplyCount, versionString); if (pollReplyCount < 9999) { pollReplyCount++; diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 7798fb9788..1c0b5a7a0d 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -1,3 +1,4 @@ +#pragma once #ifndef WLED_FCN_DECLARE_H #define WLED_FCN_DECLARE_H @@ -19,14 +20,21 @@ void longPressAction(uint8_t b=0); void doublePressAction(uint8_t b=0); bool isButtonPressed(uint8_t b=0); void handleButton(); +void handleOnOff(bool forceOff = false); void handleIO(); void IRAM_ATTR touchButtonISR(); //cfg.cpp +bool backupConfig(); +bool restoreConfig(); +bool verifyConfig(); +bool configBackupExists(); +void resetConfig(); bool deserializeConfig(JsonObject doc, bool fromFS = false); -void deserializeConfigFromFS(); +bool deserializeConfigFromFS(); bool deserializeConfigSec(); -void serializeConfig(); +void serializeConfig(JsonObject doc); +void serializeConfigToFS(); void serializeConfigSec(); template @@ -52,71 +60,67 @@ bool getJsonValue(const JsonVariant& element, DestType& destination, const Defau typedef struct WiFiConfig { char clientSSID[33]; char clientPass[65]; + uint8_t bssid[6]; IPAddress staticIP; IPAddress staticGW; IPAddress staticSN; +#ifdef WLED_ENABLE_WPA_ENTERPRISE + byte encryptionType; + char enterpriseAnonIdentity[65]; + char enterpriseIdentity[65]; + WiFiConfig(const char *ssid="", const char *pass="", uint32_t ip=0, uint32_t gw=0, uint32_t subnet=0x00FFFFFF // little endian + , byte enc_type=WIFI_ENCRYPTION_TYPE_PSK, const char *ent_anon="", const char *ent_iden="") +#else WiFiConfig(const char *ssid="", const char *pass="", uint32_t ip=0, uint32_t gw=0, uint32_t subnet=0x00FFFFFF) // little endian +#endif : staticIP(ip) , staticGW(gw) , staticSN(subnet) { strncpy(clientSSID, ssid, 32); clientSSID[32] = 0; strncpy(clientPass, pass, 64); clientPass[64] = 0; +#ifdef WLED_ENABLE_WPA_ENTERPRISE + encryptionType = enc_type; + strncpy(enterpriseAnonIdentity, ent_anon, 64); enterpriseAnonIdentity[64] = 0; + strncpy(enterpriseIdentity, ent_iden, 64); enterpriseIdentity[64] = 0; +#endif + memset(bssid, 0, sizeof(bssid)); } } wifi_config; -//colors.cpp -// similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) -class NeoGammaWLEDMethod { - public: - [[gnu::hot]] static uint8_t Correct(uint8_t value); // apply Gamma to single channel - [[gnu::hot]] 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) -[[gnu::hot]] uint32_t color_blend(uint32_t,uint32_t,uint16_t,bool b16=false); -[[gnu::hot]] uint32_t color_add(uint32_t,uint32_t, bool fast=false); -[[gnu::hot]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); -CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette); -CRGBPalette16 generateRandomPalette(); -inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); //hue, sat to rgb -void colorKtoRGB(uint16_t kelvin, byte* rgb); -void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb -void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO -void colorRGBtoXY(byte* rgb, float* xy); // only defined if huesync disabled TODO -void colorFromDecOrHexString(byte* rgb, char* in); -bool colorFromHexString(byte* rgb, const char* in); -uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); -uint16_t approximateKelvinFromRGB(uint32_t rgb); -void setRandomColor(byte* rgb); - -//dmx.cpp -void initDMX(); -void handleDMX(); +//dmx_output.cpp +void initDMXOutput(); +void handleDMXOutput(); + +//dmx_input.cpp +void initDMXInput(); +void handleDMXInput(); //e131.cpp void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol); -void handleArtnetPollReply(IPAddress ipAddress); -void prepareArtnetPollReply(ArtPollReply* reply); -void sendArtnetPollReply(ArtPollReply* reply, IPAddress ipAddress, uint16_t portAddress); +void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses); +// void handleArtnetPollReply(IPAddress ipAddress); // local function, only used in e131.cpp +// void prepareArtnetPollReply(ArtPollReply* reply); // local function, only used in e131.cpp +// void sendArtnetPollReply(ArtPollReply* reply, IPAddress ipAddress, uint16_t portAddress); // local function, only used in e131.cpp //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); +bool writeObjectToFileUsingId(const char* file, uint16_t id, const JsonDocument* content); +bool writeObjectToFile(const char* file, const char* key, const JsonDocument* content); +bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr); +bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr); void updateFSInfo(); void closeFile(); -inline bool writeObjectToFileUsingId(const String &file, uint16_t id, JsonDocument* content) { return writeObjectToFileUsingId(file.c_str(), id, content); }; -inline bool writeObjectToFile(const String &file, const char* key, JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); }; -inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest) { return readObjectFromFileUsingId(file.c_str(), id, dest); }; -inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest) { return readObjectFromFile(file.c_str(), key, dest); }; +inline bool writeObjectToFileUsingId(const String &file, uint16_t id, const JsonDocument* content) { return writeObjectToFileUsingId(file.c_str(), id, content); }; +inline bool writeObjectToFile(const String &file, const char* key, const JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); }; +inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); }; +inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); }; +bool copyFile(const char* src_path, const char* dst_path); +bool backupFile(const char* filename); +bool restoreFile(const char* filename); +bool checkBackupExists(const char* filename); +bool validateJsonFile(const char* filename); +void dumpFilesToSerial(); //hue.cpp void handleHue(); @@ -126,6 +130,18 @@ void onHueConnect(void* arg, AsyncClient* client); void sendHuePoll(); void onHueData(void* arg, AsyncClient* client, void *data, size_t len); +//image_loader.cpp +class Segment; +#ifdef WLED_ENABLE_GIF +bool fileSeekCallback(unsigned long position); +unsigned long filePositionCallback(void); +int fileReadCallback(void); +int fileReadBlockCallback(void * buffer, int numberOfBytes); +int fileSizeCallback(void); +byte renderImageToSegment(Segment &seg); +void endImagePlayback(Segment* seg); +#endif + //improv.cpp enum ImprovRPCType { Command_Wifi = 0x01, @@ -151,15 +167,14 @@ void handleIR(); #include "ESPAsyncWebServer.h" #include "src/dependencies/json/ArduinoJson-v6.h" #include "src/dependencies/json/AsyncJson-v6.h" -#include "FX.h" -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 serializeSegment(const JsonObject& root, const 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 serializeModeNames(JsonArray arr); +void serializeModeData(JsonArray fxdata); +void serializePins(JsonObject root); void serveJson(AsyncWebServerRequest* request); #ifdef WLED_ENABLE_JSONLIVE bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); @@ -167,8 +182,8 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); //led.cpp void setValuesFromSegment(uint8_t s); -void setValuesFromMainSeg(); -void setValuesFromFirstSelectedSeg(); +#define setValuesFromMainSeg() setValuesFromSegment(strip.getMainSegmentId()) +#define setValuesFromFirstSelectedSeg() setValuesFromSegment(strip.getFirstSelectedSegId()) void toggleOnOff(); void applyBri(); void applyFinalBri(); @@ -193,21 +208,22 @@ void publishMqtt(); //ntp.cpp void handleTime(); void handleNetworkTime(); -void sendNTPPacket(); -bool checkNTPResponse(); +// void sendNTPPacket(); // local function, only used in ntp.cpp +// bool checkNTPResponse(); // local function, only used in ntp.cpp void updateLocalTime(); void getTimeString(char* out); bool checkCountdown(); void setCountdown(); byte weekdayMondayFirst(); +bool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayEnd); void checkTimers(); void calculateSunriseAndSunset(); void setTimeFromAPI(uint32_t timein); //overlay.cpp void handleOverlayDraw(); -void _overlayAnalogCountdown(); -void _overlayAnalogClock(); +// void _overlayAnalogCountdown(); // local function, only used in overlay.cpp +// void _overlayAnalogClock(); // local function, only used in overlay.cpp //playlist.cpp void shufflePlaylist(); @@ -218,6 +234,7 @@ void serializePlaylist(JsonObject obj); //presets.cpp const char *getPresetsFileName(bool persistent = true); +bool presetNeedsSaving(); void initPresetsFile(); void handlePresets(); bool applyPreset(byte index, byte callMode = CALL_MODE_DIRECT_CHANGE); @@ -230,7 +247,8 @@ void deletePreset(byte index); bool getPresetName(byte index, String& name); //remote.cpp -void handleRemote(uint8_t *data, size_t len); +void handleWiZdata(uint8_t *incomingData, size_t len); +void handleRemote(); //set.cpp bool isAsterisksOnly(const char* str, byte maxLen); @@ -239,7 +257,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=tru //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); +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false); void realtimeLock(uint32_t timeoutMs, byte md = REALTIME_MODE_GENERIC); void exitRealtime(); void handleNotifications(); @@ -252,7 +270,12 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs #endif //network.cpp -int getSignalQuality(int rssi); +bool initEthernet(); // result is informational +int getSignalQuality(int rssi); +void fillMAC2Str(char *str, const uint8_t *mac); +void fillStr2MAC(uint8_t *mac, const char *str); +int findWiFi(bool doScan = false); +bool isWiFiConfigured(); void WiFiEvent(WiFiEvent_t event); //um_manager.cpp @@ -293,7 +316,7 @@ class Usermod { protected: um_data_t *um_data; // um_data should be allocated using new in (derived) Usermod's setup() or constructor public: - Usermod() { um_data = nullptr; } + 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 @@ -310,6 +333,7 @@ class Usermod { 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 bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { return false; } // fired upon ESP-NOW message received + virtual bool onUdpPacket(uint8_t* payload, size_t len) { return false; } //fired upon UDP packet received 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;} @@ -329,40 +353,35 @@ class Usermod { #endif }; -class UsermodManager { - private: - static Usermod* ums[WLED_MAX_USERMODS]; - static byte numMods; - - public: - static void loop(); - static void handleOverlayDraw(); - static bool handleButton(uint8_t b); - static bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods - static void setup(); - static void connected(); - static void appendConfigData(Print&); - static void addToJsonState(JsonObject& obj); - static void addToJsonInfo(JsonObject& obj); - static void readFromJsonState(JsonObject& obj); - static void addToConfig(JsonObject& obj); - static bool readFromConfig(JsonObject& obj); +namespace UsermodManager { + void loop(); + void handleOverlayDraw(); + bool handleButton(uint8_t b); + bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods + void setup(); + void connected(); + void appendConfigData(Print&); + void addToJsonState(JsonObject& obj); + void addToJsonInfo(JsonObject& obj); + void readFromJsonState(JsonObject& obj); + void addToConfig(JsonObject& obj); + bool readFromConfig(JsonObject& obj); #ifndef WLED_DISABLE_MQTT - static void onMqttConnect(bool sessionPresent); - static bool onMqttMessage(char* topic, char* payload); + void onMqttConnect(bool sessionPresent); + bool onMqttMessage(char* topic, char* payload); #endif #ifndef WLED_DISABLE_ESPNOW - static bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); + bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); #endif - static void onUpdateBegin(bool); - static void onStateChange(uint8_t); - static bool add(Usermod* um); - static Usermod* lookup(uint16_t mod_id); - static inline byte getModCount() {return numMods;}; + bool onUdpPacket(uint8_t* payload, size_t len); + void onUpdateBegin(bool); + void onStateChange(uint8_t); + Usermod* lookup(uint16_t mod_id); + size_t getModCount(); }; -//usermods_list.cpp -void registerUsermods(); +// Register usermods by building a static list via a linker section +#define REGISTER_USERMOD(x) Usermod* const um_##x __attribute__((__section__(".dtors.tbl.usermods.1"), used)) = &x //usermod.cpp void userSetup(); @@ -370,39 +389,120 @@ 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 getBoolVal(JsonVariant elem, bool dflt); -bool updateVal(const char* req, const char* key, byte* val, byte minv=0, byte maxv=255); +#ifdef ESP8266 +#define HW_RND_REGISTER RANDOM_REG32 +#else // ESP32 family +#include "soc/wdev_reg.h" +#define HW_RND_REGISTER REG_READ(WDEV_RND_REG) +#endif +#define inoise8 perlin8 // fastled legacy alias +#define inoise16 perlin16 // fastled legacy alias +#define hex2int(a) (((a)>='0' && (a)<='9') ? (a)-'0' : ((a)>='A' && (a)<='F') ? (a)-'A'+10 : ((a)>='a' && (a)<='f') ? (a)-'a'+10 : 0) +[[gnu::pure]] 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 vmin=0, byte vmax=255); // getVal supports inc/decrementing and random ("X~Y(r|[w]~[-][Z])" form) +[[gnu::pure]] bool getBoolVal(const JsonVariant &elem, bool dflt); +bool updateVal(const char* req, const char* key, byte &val, byte minv=0, byte maxv=255); size_t printSetFormCheckbox(Print& settingsScript, const char* key, int val); size_t printSetFormValue(Print& settingsScript, const char* key, int val); size_t printSetFormValue(Print& settingsScript, const char* key, const char* val); size_t printSetFormIndex(Print& settingsScript, const char* key, int index); size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val); void prepareHostname(char* hostname); -bool isAsterisksOnly(const char* str, byte maxLen); -bool requestJSONBufferLock(uint8_t module=255); +[[gnu::pure]] bool isAsterisksOnly(const char* str, byte maxLen); +bool requestJSONBufferLock(uint8_t moduleID=JSON_LOCK_UNKNOWN); 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); +String computeSHA1(const String& input); +String getDeviceId(); uint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0); uint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0); uint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0); um_data_t* simulateSound(uint8_t simulationId); void enumerateLedmaps(); -uint8_t get_random_wheel_index(uint8_t pos); -float mapf(float x, float in_min, float in_max, float out_min, float out_max); - +[[gnu::hot]] uint8_t get_random_wheel_index(uint8_t pos); +[[gnu::hot, gnu::pure]] float mapf(float x, float in_min, float in_max, float out_min, float out_max); +uint32_t hashInt(uint32_t s); +int32_t perlin1D_raw(uint32_t x, bool is16bit = false); +int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit = false); +int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit = false); +uint16_t perlin16(uint32_t x); +uint16_t perlin16(uint32_t x, uint32_t y); +uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z); +uint8_t perlin8(uint16_t x); +uint8_t perlin8(uint16_t x, uint16_t y); +uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z); + +// fast (true) random numbers using hardware RNG, all functions return values in the range lowerlimit to upperlimit-1 +// note: for true random numbers with high entropy, do not call faster than every 200ns (5MHz) +// tests show it is still highly random reading it quickly in a loop (better than fastled PRNG) +// for 8bit and 16bit random functions: no limit check is done for best speed +// 32bit inputs are used for speed and code size, limits don't work if inverted or out of range +// inlining does save code size except for random(a,b) and 32bit random with limits +#define random hw_random // replace arduino random() +inline uint32_t hw_random() { return HW_RND_REGISTER; }; +uint32_t hw_random(uint32_t upperlimit); // not inlined for code size +int32_t hw_random(int32_t lowerlimit, int32_t upperlimit); +inline uint16_t hw_random16() { return HW_RND_REGISTER; }; +inline uint16_t hw_random16(uint32_t upperlimit) { return (hw_random16() * upperlimit) >> 16; }; // input range 0-65535 (uint16_t) +inline int16_t hw_random16(int32_t lowerlimit, int32_t upperlimit) { int32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random16(range); }; // signed limits, use int16_t ranges +inline uint8_t hw_random8() { return HW_RND_REGISTER; }; +inline uint8_t hw_random8(uint32_t upperlimit) { return (hw_random8() * upperlimit) >> 8; }; // input range 0-255 +inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); }; // input range 0-255 + +// memory allocation wrappers (util.cpp) +extern "C" { + // prefer DRAM in d_xalloc functions, PSRAM as fallback + void *d_malloc(size_t); + void *d_calloc(size_t, size_t); + void *d_realloc_malloc(void *ptr, size_t size); + #ifndef ESP8266 + inline void d_free(void *ptr) { heap_caps_free(ptr); } + #else + inline void d_free(void *ptr) { free(ptr); } + #endif + #if defined(BOARD_HAS_PSRAM) + // prefer PSRAM in p_xalloc functions, DRAM as fallback + void *p_malloc(size_t); + void *p_calloc(size_t, size_t); + void *p_realloc_malloc(void *ptr, size_t size); + inline void p_free(void *ptr) { heap_caps_free(ptr); } + #else + #define p_malloc d_malloc + #define p_calloc d_calloc + #define p_realloc_malloc d_realloc_malloc + #define p_free d_free + #endif +} +#ifndef ESP8266 +inline size_t getFreeHeapSize() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns free heap (ESP.getFreeHeap() can include other memory types) +inline size_t getContiguousFreeHeap() { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns largest contiguous free block +#else +inline size_t getFreeHeapSize() { return ESP.getFreeHeap(); } // returns free heap +inline size_t getContiguousFreeHeap() { return ESP.getMaxFreeBlockSize(); } // returns largest contiguous free block +#endif +#define BFRALLOC_NOBYTEACCESS (1 << 0) // ESP32 has 32bit accessible DRAM (usually ~50kB free) that must not be byte-accessed +#define BFRALLOC_PREFER_DRAM (1 << 1) // prefer DRAM over PSRAM +#define BFRALLOC_ENFORCE_DRAM (1 << 2) // use DRAM only, no PSRAM +#define BFRALLOC_PREFER_PSRAM (1 << 3) // prefer PSRAM over DRAM +#define BFRALLOC_ENFORCE_PSRAM (1 << 4) // use PSRAM if available, otherwise uses DRAM +#define BFRALLOC_CLEAR (1 << 5) // clear allocated buffer after allocation +void *allocate_buffer(size_t size, uint32_t type); + +void handleBootLoop(); // detect and handle bootloops +#ifndef ESP8266 +void bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config +#endif // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard class JSONBufferGuard { bool holding_lock; public: - inline JSONBufferGuard(uint8_t module=255) : holding_lock(requestJSONBufferLock(module)) {}; + inline JSONBufferGuard(uint8_t module=JSON_LOCK_UNKNOWN) : holding_lock(requestJSONBufferLock(module)) {}; inline ~JSONBufferGuard() { if (holding_lock) releaseJSONBufferLock(); }; inline JSONBufferGuard(const JSONBufferGuard&) = delete; // Noncopyable inline JSONBufferGuard& operator=(const JSONBufferGuard&) = delete; @@ -413,14 +513,6 @@ class JSONBufferGuard { inline void release() { if (holding_lock) releaseJSONBufferLock(); holding_lock = false; } }; -#ifdef WLED_ADD_EEPROM_SUPPORT -//wled_eeprom.cpp -void applyMacro(byte index); -void deEEP(); -void deEEPSettings(); -void clearEEPROM(); -#endif - //wled_math.cpp //float cos_t(float phi); // use float math //float sin_t(float phi); @@ -438,6 +530,7 @@ float asin_t(float x); template T atan_t(T x); float floor_t(float x); float fmod_t(float num, float denom); +uint32_t sqrt32_bw(uint32_t x); #define sin_t sin_approx #define cos_t cos_approx #define tan_t tan_approx @@ -458,7 +551,6 @@ void handleSerial(); void updateBaudRate(uint32_t rate); //wled_server.cpp -void createEditHandler(bool enable); void initServer(); void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255); void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error); diff --git a/wled00/file.cpp b/wled00/file.cpp index bc34672023..dcc2d57c41 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -38,8 +38,8 @@ void closeFile() { DEBUGFS_PRINT(F("Close -> ")); uint32_t s = millis(); #endif - f.close(); - DEBUGFS_PRINTF("took %d ms\n", millis() - s); + f.close(); // "if (f)" check is aleady done inside f.close(), and f cannot be nullptr -> no need for double checking before closing the file handle. + DEBUGFS_PRINTF("took %lu ms\n", millis() - s); doCloseFile = false; } @@ -69,14 +69,14 @@ static bool bufferedFind(const char *target, bool fromStart = true) { 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); + DEBUGFS_PRINTF("Found at pos %d, took %lu ms", f.position(), millis() - s); return true; } } count++; } } - DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } @@ -111,7 +111,7 @@ static bool bufferedFindSpace(size_t targetLen, bool fromStart = true) { 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); + DEBUGFS_PRINTF("Found at pos %d, took %lu ms", f.position(), millis() - s); return true; } } else { @@ -125,7 +125,7 @@ static bool bufferedFindSpace(size_t targetLen, bool fromStart = true) { count++; } } - DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } @@ -151,13 +151,13 @@ static bool bufferedFindObjectEnd() { 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); + DEBUGFS_PRINTF("} at pos %d, took %lu ms", f.position(), millis() - s); return true; } count++; } } - DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } @@ -176,7 +176,7 @@ static void writeSpace(size_t l) if (knownLargestSpace < l) knownLargestSpace = l; } -bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint32_t contentLen = 0) +static bool appendObjectToFile(const char* key, const JsonDocument* content, uint32_t s, uint32_t contentLen = 0) { #ifdef WLED_DEBUG_FS DEBUGFS_PRINTLN(F("Append")); @@ -203,7 +203,7 @@ bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint 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); + DEBUGFS_PRINTF("Inserted, took %lu ms (total %lu)", millis() - s1, millis() - s); doCloseFile = true; return true; } @@ -251,18 +251,18 @@ bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint f.write('}'); doCloseFile = true; - DEBUGFS_PRINTF("Appended, took %d ms (total %d)", millis() - s1, millis() - s); + DEBUGFS_PRINTF("Appended, took %lu ms (total %lu)", millis() - s1, millis() - s); return true; } -bool writeObjectToFileUsingId(const char* file, uint16_t id, JsonDocument* content) +bool writeObjectToFileUsingId(const char* file, uint16_t id, const JsonDocument* content) { char objKey[10]; sprintf(objKey, "\"%d\":", id); return writeObjectToFile(file, objKey, content); } -bool writeObjectToFile(const char* file, const char* key, JsonDocument* content) +bool writeObjectToFile(const char* file, const char* key, const JsonDocument* content) { uint32_t s = 0; //timing #ifdef WLED_DEBUG_FS @@ -271,6 +271,8 @@ bool writeObjectToFile(const char* file, const char* key, JsonDocument* content) s = millis(); #endif + if (doCloseFile) closeFile(); // This prevents the loss of file data that is still cached in the File object. + size_t pos = 0; char fileName[129]; strncpy_P(fileName, file, 128); fileName[128] = 0; //use PROGMEM safe copy as FS.open() does not f = WLED_FS.open(fileName, WLED_FS.exists(fileName) ? "r+" : "w+"); @@ -321,19 +323,19 @@ bool writeObjectToFile(const char* file, const char* key, JsonDocument* content) } doCloseFile = true; - DEBUGFS_PRINTF("Replaced/deleted, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("Replaced/deleted, took %lu ms\n", millis() - s); return true; } -bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest) +bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest, const JsonDocument* filter) { char objKey[10]; sprintf(objKey, "\"%d\":", id); - return readObjectFromFile(file, objKey, dest); + return readObjectFromFile(file, objKey, dest, filter); } //if the key is a nullptr, deserialize entire object -bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest) +bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, const JsonDocument* filter) { if (doCloseFile) closeFile(); #ifdef WLED_DEBUG_FS @@ -352,10 +354,11 @@ bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest) return false; } - deserializeJson(*dest, f); + if (filter) deserializeJson(*dest, f, DeserializationOption::Filter(*filter)); + else deserializeJson(*dest, f); f.close(); - DEBUGFS_PRINTF("Read, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("Read, took %lu ms\n", millis() - s); return true; } @@ -391,8 +394,9 @@ static const uint8_t *getPresetCache(size_t &size) { if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) { if (presetsCached) { - free(presetsCached); + p_free(presetsCached); presetsCached = nullptr; + presetsCachedSize = 0; } } @@ -402,7 +406,7 @@ static const uint8_t *getPresetCache(size_t &size) { presetsCachedTime = presetsModifiedTime; presetsCachedValidate = cacheInvalidate; presetsCachedSize = 0; - presetsCached = (uint8_t*)ps_malloc(file.size() + 1); + presetsCached = (uint8_t*)p_malloc(file.size() + 1); if (presetsCached) { presetsCachedSize = file.size(); file.read(presetsCached, presetsCachedSize); @@ -418,11 +422,11 @@ static const uint8_t *getPresetCache(size_t &size) { #endif bool handleFileRead(AsyncWebServerRequest* request, String path){ - DEBUG_PRINT(F("WS FileRead: ")); DEBUG_PRINTLN(path); + DEBUGFS_PRINT(F("WS FileRead: ")); DEBUGFS_PRINTLN(path); if(path.endsWith("/")) path += "index.htm"; if(path.indexOf(F("sec")) > -1) return false; - #ifdef ARDUINO_ARCH_ESP32 - if (psramSafe && psramFound() && path.endsWith(FPSTR(getPresetsFileName()))) { + #ifdef BOARD_HAS_PSRAM + if (path.endsWith(FPSTR(getPresetsFileName()))) { size_t psize; const uint8_t *presets = getPresetCache(psize); if (presets) { @@ -438,3 +442,162 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){ } return false; } + +// copy a file, delete destination file if incomplete to prevent corrupted files +bool copyFile(const char* src_path, const char* dst_path) { + DEBUG_PRINTF("copyFile from %s to %s\n", src_path, dst_path); + if(!WLED_FS.exists(src_path)) { + DEBUG_PRINTLN(F("file not found")); + return false; + } + + bool success = true; // is set to false on error + File src = WLED_FS.open(src_path, "r"); + File dst = WLED_FS.open(dst_path, "w"); + + if (src && dst) { + uint8_t buf[128]; // copy file in 128-byte blocks + while (src.available() > 0) { + size_t bytesRead = src.read(buf, sizeof(buf)); + if (bytesRead == 0) { + success = false; + break; // error, no data read + } + size_t bytesWritten = dst.write(buf, bytesRead); + if (bytesWritten != bytesRead) { + success = false; + break; // error, not all data written + } + } + } else { + success = false; // error, could not open files + } + if(src) src.close(); + if(dst) dst.close(); + if (!success) { + DEBUG_PRINTLN(F("copy failed")); + WLED_FS.remove(dst_path); // delete incomplete file + } + return success; +} + +// compare two files, return true if identical +bool compareFiles(const char* path1, const char* path2) { + DEBUG_PRINTF("compareFile %s and %s\n", path1, path2); + if (!WLED_FS.exists(path1) || !WLED_FS.exists(path2)) { + DEBUG_PRINTLN(F("file not found")); + return false; + } + + bool identical = true; // set to false on mismatch + File f1 = WLED_FS.open(path1, "r"); + File f2 = WLED_FS.open(path2, "r"); + + if (f1 && f2) { + uint8_t buf1[128], buf2[128]; + while (f1.available() > 0 || f2.available() > 0) { + size_t len1 = f1.read(buf1, sizeof(buf1)); + size_t len2 = f2.read(buf2, sizeof(buf2)); + + if (len1 != len2) { + identical = false; + break; // files differ in size or read failed + } + + if (memcmp(buf1, buf2, len1) != 0) { + identical = false; + break; // files differ in content + } + } + } else { + identical = false; // error opening files + } + + if (f1) f1.close(); + if (f2) f2.close(); + return identical; +} + +static const char s_backup_fmt[] PROGMEM = "/bkp.%s"; + +bool backupFile(const char* filename) { + DEBUG_PRINTF("backup %s \n", filename); + if (!validateJsonFile(filename)) { + DEBUG_PRINTLN(F("broken file")); + return false; + } + char backupname[32]; + snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename + + if (copyFile(filename, backupname)) { + DEBUG_PRINTLN(F("backup ok")); + return true; + } + DEBUG_PRINTLN(F("backup failed")); + return false; +} + +bool restoreFile(const char* filename) { + DEBUG_PRINTF("restore %s \n", filename); + char backupname[32]; + snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename + + if (!WLED_FS.exists(backupname)) { + DEBUG_PRINTLN(F("no backup found")); + return false; + } + + if (!validateJsonFile(backupname)) { + DEBUG_PRINTLN(F("broken backup")); + return false; + } + + if (copyFile(backupname, filename)) { + DEBUG_PRINTLN(F("restore ok")); + return true; + } + DEBUG_PRINTLN(F("restore failed")); + return false; +} + +bool checkBackupExists(const char* filename) { + char backupname[32]; + snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename + return WLED_FS.exists(backupname); +} + +bool validateJsonFile(const char* filename) { + if (!WLED_FS.exists(filename)) return false; + File file = WLED_FS.open(filename, "r"); + if (!file) return false; + StaticJsonDocument<0> doc, filter; // https://arduinojson.org/v6/how-to/validate-json/ + bool result = deserializeJson(doc, file, DeserializationOption::Filter(filter)) == DeserializationError::Ok; + file.close(); + if (!result) { + DEBUG_PRINTF_P(PSTR("Invalid JSON file %s\n"), filename); + } else { + DEBUG_PRINTF_P(PSTR("Valid JSON file %s\n"), filename); + } + return result; +} + +// print contents of all files in root dir to Serial except wsec files +void dumpFilesToSerial() { + File rootdir = WLED_FS.open("/", "r"); + File rootfile = rootdir.openNextFile(); + while (rootfile) { + size_t len = strlen(rootfile.name()); + // skip files starting with "wsec" and dont end in .json + if (strncmp(rootfile.name(), "wsec", 4) != 0 && len >= 6 && strcmp(rootfile.name() + len - 5, ".json") == 0) { + Serial.println(rootfile.name()); + while (rootfile.available()) { + Serial.write(rootfile.read()); + } + Serial.println(); + Serial.println(); + } + rootfile.close(); + rootfile = rootdir.openNextFile(); + } +} + diff --git a/wled00/hue.cpp b/wled00/hue.cpp index 950c548967..d5fcb7cb93 100644 --- a/wled00/hue.cpp +++ b/wled00/hue.cpp @@ -195,9 +195,9 @@ void onHueData(void* arg, AsyncClient* client, void *data, size_t len) { switch(hueColormode) { - case 1: if (hueX != hueXLast || hueY != hueYLast) colorXYtoRGB(hueX,hueY,col); hueXLast = hueX; hueYLast = hueY; break; - case 2: if (hueHue != hueHueLast || hueSat != hueSatLast) colorHStoRGB(hueHue,hueSat,col); hueHueLast = hueHue; hueSatLast = hueSat; break; - case 3: if (hueCt != hueCtLast) colorCTtoRGB(hueCt,col); hueCtLast = hueCt; break; + case 1: if (hueX != hueXLast || hueY != hueYLast) colorXYtoRGB(hueX,hueY,colPri); hueXLast = hueX; hueYLast = hueY; break; + case 2: if (hueHue != hueHueLast || hueSat != hueSatLast) colorHStoRGB(hueHue,hueSat,colPri); hueHueLast = hueHue; hueSatLast = hueSat; break; + case 3: if (hueCt != hueCtLast) colorCTtoRGB(hueCt,colPri); hueCtLast = hueCt; break; } } hueReceived = true; diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp new file mode 100644 index 0000000000..0f4c38893e --- /dev/null +++ b/wled00/image_loader.cpp @@ -0,0 +1,237 @@ +#include "wled.h" + +#ifdef WLED_ENABLE_GIF + +#include "GifDecoder.h" + + +/* + * Functions to render images from filesystem to segments, used by the "Image" effect + */ + +static File file; +static char lastFilename[WLED_MAX_SEGNAME_LEN+2] = "/"; // enough space for "/" + seg.name + '\0' +static GifDecoder<320,320,12,true> decoder; // this creates the basic object; parameter lzwMaxBits is not used; decoder.alloc() always allocated "everything else" = 24Kb +static bool gifDecodeFailed = false; +static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; + +bool fileSeekCallback(unsigned long position) { + return file.seek(position); +} + +unsigned long filePositionCallback(void) { + return file.position(); +} + +int fileReadCallback(void) { + return file.read(); +} + +int fileReadBlockCallback(void * buffer, int numberOfBytes) { + return file.read((uint8_t*)buffer, numberOfBytes); +} + +int fileSizeCallback(void) { + return file.size(); +} + +bool openGif(const char *filename) { // side-effect: updates "file" + file = WLED_FS.open(filename, "r"); + DEBUG_PRINTF_P(PSTR("opening GIF file %s\n"), filename); + + if (!file) return false; + return true; +} + +static Segment* activeSeg; +static uint16_t gifWidth, gifHeight; +static int lastCoordinate; // last coordinate (x+y) that was set, used to reduce redundant pixel writes +static uint16_t perPixelX, perPixelY; // scaling factors when upscaling + +void screenClearCallback(void) { + activeSeg->fill(0); +} + +// this callback runs when the decoder has finished painting all pixels +void updateScreenCallback(void) { + // perfect time for adding blur + if (activeSeg->intensity > 1) { + uint8_t blurAmount = activeSeg->intensity; + if ((blurAmount < 24) && (activeSeg->is2D())) activeSeg->blurRows(activeSeg->intensity); // some blur - fast + else activeSeg->blur(blurAmount); // more blur - slower + } + lastCoordinate = -1; // invalidate last position +} + +// note: GifDecoder drawing is done top right to bottom left, line by line + +// callbacks to draw a pixel at (x,y) without scaling: used if GIF size matches (virtual)segment size (faster) works for 1D and 2D segments +void drawPixelCallbackNoScale(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + activeSeg->setPixelColor(y * gifWidth + x, red, green, blue); +} + +void drawPixelCallback1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs) + int totalImgPix = (int)gifWidth * gifHeight; + int start = ((int)y * gifWidth + (int)x) * activeSeg->vLength() / totalImgPix; // simple nearest-neighbor scaling + if (start == lastCoordinate) return; // skip setting same coordinate again + lastCoordinate = start; + for (int i = 0; i < perPixelX; i++) { + activeSeg->setPixelColor(start + i, red, green, blue); + } +} + +void drawPixelCallback2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + // simple nearest-neighbor scaling + int outY = (int)y * activeSeg->vHeight() / gifHeight; + int outX = (int)x * activeSeg->vWidth() / gifWidth; + // Pack coordinates uniquely: outY into upper 16 bits, outX into lower 16 bits + if (((outY << 16) | outX) == lastCoordinate) return; // skip setting same coordinate again + lastCoordinate = (outY << 16) | outX; // since input is a "scanline" this is sufficient to identify a "unique" coordinate + // set multiple pixels if upscaling + for (int i = 0; i < perPixelX; i++) { + for (int j = 0; j < perPixelY; j++) { + activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue); + } + } +} + +#define IMAGE_ERROR_NONE 0 +#define IMAGE_ERROR_NO_NAME 1 +#define IMAGE_ERROR_SEG_LIMIT 2 +#define IMAGE_ERROR_UNSUPPORTED_FORMAT 3 +#define IMAGE_ERROR_FILE_MISSING 4 +#define IMAGE_ERROR_DECODER_ALLOC 5 +#define IMAGE_ERROR_GIF_DECODE 6 +#define IMAGE_ERROR_FRAME_DECODE 7 +#define IMAGE_ERROR_WAITING 254 +#define IMAGE_ERROR_PREV 255 + +// renders an image (.gif only; .bmp and .fseq to be added soon) from FS to a segment +byte renderImageToSegment(Segment &seg) { + if (!seg.name) return IMAGE_ERROR_NO_NAME; + // disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining + //if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING; + if (activeSeg && activeSeg != &seg) { // only one segment at a time + if (!seg.isActive()) return IMAGE_ERROR_SEG_LIMIT; // sanity check: calling segment must be active + if (gifDecodeFailed || !activeSeg->isActive()) // decoder failed, or last segment became inactive + endImagePlayback(activeSeg); // => allow takeover but clean up first + else + return IMAGE_ERROR_SEG_LIMIT; + } + + activeSeg = &seg; + + if (strncmp(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN) != 0) { // segment name changed, load new image + strcpy(lastFilename, "/"); // filename always starts with '/' + strncpy(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN); + lastFilename[WLED_MAX_SEGNAME_LEN+1] ='\0'; // ensure proper string termination when segment name was truncated + gifDecodeFailed = false; + size_t fnameLen = strlen(lastFilename); + if ((fnameLen < 4) || strcmp(lastFilename + fnameLen - 4, ".gif") != 0) { // empty segment name, name too short, or name not ending in .gif + gifDecodeFailed = true; + DEBUG_PRINTF_P(PSTR("GIF decoder unsupported file: %s\n"), lastFilename); + return IMAGE_ERROR_UNSUPPORTED_FORMAT; + } + if (file) file.close(); + if (!openGif(lastFilename)) { + gifDecodeFailed = true; + DEBUG_PRINTF_P(PSTR("GIF file not found: %s\n"), lastFilename); + return IMAGE_ERROR_FILE_MISSING; + } + lastCoordinate = -1; + decoder.setScreenClearCallback(screenClearCallback); + decoder.setUpdateScreenCallback(updateScreenCallback); + decoder.setDrawPixelCallback(drawPixelCallbackNoScale); // default: use "fast path" callback without scaling + decoder.setFileSeekCallback(fileSeekCallback); + decoder.setFilePositionCallback(filePositionCallback); + decoder.setFileReadCallback(fileReadCallback); + decoder.setFileReadBlockCallback(fileReadBlockCallback); + decoder.setFileSizeCallback(fileSizeCallback); +#if __cpp_exceptions // use exception handler if we can (some targets don't support exceptions) + try { +#endif + decoder.alloc(); // this function may throw out-of memory and cause a crash +#if __cpp_exceptions + } catch (...) { // if we arrive here, the decoder has thrown an OOM exception + gifDecodeFailed = true; + errorFlag = ERR_NORAM_PX; + DEBUG_PRINTLN(F("\nGIF decoder out of memory. Please try a smaller image file.\n")); + return IMAGE_ERROR_DECODER_ALLOC; + // decoder cleanup (hi @coderabbitai): No additonal cleanup necessary - decoder.alloc() ultimately uses "new AnimatedGIF". + // If new throws, no pointer is assigned, previous decoder state (if any) has already been deleted inside alloc(), so calling decoder.dealloc() here is unnecessary. + } +#endif + DEBUG_PRINTLN(F("Starting decoding")); + int decoderError = decoder.startDecoding(); + if(decoderError < 0) { + DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in startDecoding().\n"), decoderError); + errorFlag = ERR_NORAM_PX; + gifDecodeFailed = true; + return IMAGE_ERROR_GIF_DECODE; + } + DEBUG_PRINTLN(F("Decoding started")); + // after startDecoding, we can get GIF size, update static variables and callbacks + decoder.getSize(&gifWidth, &gifHeight); + if (gifWidth == 0 || gifHeight == 0) { // bad gif size: prevent division by zero + gifDecodeFailed = true; + DEBUG_PRINTF_P(PSTR("Invalid GIF dimensions: %dx%d\n"), gifWidth, gifHeight); + return IMAGE_ERROR_GIF_DECODE; + } + if (activeSeg->is2D()) { + perPixelX = (activeSeg->vWidth() + gifWidth -1) / gifWidth; + perPixelY = (activeSeg->vHeight() + gifHeight-1) / gifHeight; + if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) { + decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling + //DEBUG_PRINTLN(F("scaling image")); + } + } else { + int totalImgPix = (int)gifWidth * gifHeight; + if (totalImgPix - activeSeg->vLength() == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input pad last pixel if length is odd) + perPixelX = (activeSeg->vLength() + totalImgPix-1) / totalImgPix; + if (totalImgPix != activeSeg->vLength()) { + decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling + //DEBUG_PRINTLN(F("scaling image")); + } + } + } + + if (gifDecodeFailed) return IMAGE_ERROR_PREV; + if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } + //if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; } + + // speed 0 = half speed, 128 = normal, 255 = full FX FPS + // TODO: 0 = 4x slow, 64 = 2x slow, 128 = normal, 192 = 2x fast, 255 = 4x fast + uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128; + + // TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions + if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING; + + int result = decoder.decodeFrame(false); + if (result < 0) { + DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in decodeFrame().\n"), result); + gifDecodeFailed = true; + return IMAGE_ERROR_FRAME_DECODE; + } + + currentFrameDelay = decoder.getFrameDelay_ms(); + unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate + currentFrameDelay = tooSlowBy > currentFrameDelay ? 0 : currentFrameDelay - tooSlowBy; + lastFrameDisplayTime = millis(); + + return IMAGE_ERROR_NONE; +} + +void endImagePlayback(Segment *seg) { + DEBUG_PRINTLN(F("Image playback end called")); + if (!activeSeg || activeSeg != seg) return; + if (file) file.close(); + decoder.dealloc(); + gifDecodeFailed = false; + activeSeg = nullptr; + strcpy(lastFilename, "/"); // reset filename + gifWidth = gifHeight = 0; // reset dimensions + DEBUG_PRINTLN(F("Image playback ended")); +} + +#endif \ No newline at end of file diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 197148b2bf..2d2d876d6e 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -16,8 +16,8 @@ #endif #define IMPROV_VERSION 1 - -void parseWiFiCommand(char *rpcData); +// forward declarations +static void parseWiFiCommand(char* rpcData); enum ImprovPacketType { Current_State = 0x01, @@ -252,7 +252,7 @@ void startImprovWifiScan() {} void handleImprovWifiScan() {} #endif -void parseWiFiCommand(char* rpcData) { +static void parseWiFiCommand(char* rpcData) { unsigned len = rpcData[0]; if (!len || len > 126) return; @@ -272,5 +272,5 @@ void parseWiFiCommand(char* rpcData) { improvActive = 2; forceReconnect = true; - serializeConfig(); + serializeConfigToFS(); } \ No newline at end of file diff --git a/wled00/ir.cpp b/wled00/ir.cpp index e4541cd909..6c9ea3ad5b 100644 --- a/wled00/ir.cpp +++ b/wled00/ir.cpp @@ -7,14 +7,14 @@ * Infrared sensor support for several generic RGB remotes and custom JSON remote */ -IRrecv* irrecv; -decode_results results; -unsigned long irCheckedTime = 0; -uint32_t lastValidCode = 0; -byte lastRepeatableAction = ACTION_NONE; -uint8_t lastRepeatableValue = 0; -uint16_t irTimesRepeated = 0; -uint8_t lastIR6ColourIdx = 0; +static IRrecv* irrecv; +static decode_results results; +static unsigned long irCheckedTime = 0; +static uint32_t lastValidCode = 0; +static byte lastRepeatableAction = ACTION_NONE; +static uint8_t lastRepeatableValue = 0; +static uint16_t irTimesRepeated = 0; +static uint8_t lastIR6ColourIdx = 0; // brightnessSteps: a static array of brightness levels following a geometric @@ -129,7 +129,7 @@ static void changeEffectSpeed(int8_t amount) } else { // if Effect == "solid Color", change the hue of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); CRGB fastled_col = CRGB(sseg.colors[0]); - CHSV prim_hsv = rgb2hsv_approximate(fastled_col); + CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t)prim_hsv.h + amount; 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 @@ -173,7 +173,7 @@ static void changeEffectIntensity(int8_t amount) } else { // if Effect == "solid Color", change the saturation of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); CRGB fastled_col = CRGB(sseg.colors[0]); - CHSV prim_hsv = rgb2hsv_approximate(fastled_col); + CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t) prim_hsv.s + amount; prim_hsv.s = (byte)constrain(new_val,0,255); // constrain to 0-255 hsv2rgb_rainbow(prim_hsv, fastled_col); @@ -425,8 +425,8 @@ static void decodeIR44(uint32_t code) case IR44_COLDWHITE2 : changeColor(COLOR_COLDWHITE2, 255); changeEffect(FX_MODE_STATIC); break; case IR44_REDPLUS : changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break; case IR44_REDMINUS : changeEffect(relativeChange(effectCurrent, -1, 0, strip.getModeCount() -1)); break; - case IR44_GREENPLUS : changePalette(relativeChange(effectPalette, 1, 0, strip.getPaletteCount() -1)); break; - case IR44_GREENMINUS : changePalette(relativeChange(effectPalette, -1, 0, strip.getPaletteCount() -1)); break; + case IR44_GREENPLUS : changePalette(relativeChange(effectPalette, 1, 0, getPaletteCount() -1)); break; + case IR44_GREENMINUS : changePalette(relativeChange(effectPalette, -1, 0, getPaletteCount() -1)); break; case IR44_BLUEPLUS : changeEffectIntensity( 16); break; case IR44_BLUEMINUS : changeEffectIntensity(-16); break; case IR44_QUICK : changeEffectSpeed( 16); break; @@ -435,7 +435,7 @@ static void decodeIR44(uint32_t code) case IR44_DIY2 : presetFallback(2, FX_MODE_BREATH, 0); break; case IR44_DIY3 : presetFallback(3, FX_MODE_FIRE_FLICKER, 0); break; case IR44_DIY4 : presetFallback(4, FX_MODE_RAINBOW, 0); break; - case IR44_DIY5 : presetFallback(5, FX_MODE_METEOR_SMOOTH, 0); break; + case IR44_DIY5 : presetFallback(5, FX_MODE_METEOR, 0); break; case IR44_DIY6 : presetFallback(6, FX_MODE_RAIN, 0); break; case IR44_AUTO : changeEffect(FX_MODE_STATIC); break; case IR44_FLASH : changeEffect(FX_MODE_PALETTE); break; @@ -484,7 +484,7 @@ static void decodeIR6(uint32_t code) case IR6_CHANNEL_UP: incBrightness(); break; case IR6_CHANNEL_DOWN: decBrightness(); break; case IR6_VOLUME_UP: changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break; - case IR6_VOLUME_DOWN: changePalette(relativeChange(effectPalette, 1, 0, strip.getPaletteCount() -1)); + case IR6_VOLUME_DOWN: changePalette(relativeChange(effectPalette, 1, 0, getPaletteCount() -1)); switch(lastIR6ColourIdx) { case 0: changeColor(COLOR_RED); break; case 1: changeColor(COLOR_REDDISH); break; @@ -530,7 +530,7 @@ static void decodeIR9(uint32_t code) /* This allows users to customize IR actions without the need to edit C code and compile. -From the https://github.com/Aircoookie/WLED/wiki/Infrared-Control page, download the starter +From the https://github.com/wled/WLED/wiki/Infrared-Control page, download the starter ir.json file that corresponds to the number of buttons on your remote. Many of the remotes with the same number of buttons emit the same codes, but will have different labels or colors. Once you edit the ir.json file, upload it to your controller @@ -559,7 +559,7 @@ static void decodeIRJson(uint32_t code) JsonObject fdo; JsonObject jsonCmdObj; - if (!requestJSONBufferLock(13)) return; + if (!requestJSONBufferLock(JSON_LOCK_IR)) return; sprintf_P(objKey, PSTR("\"0x%lX\":"), (unsigned long)code); strcpy_P(fileName, PSTR("/ir.json")); // for FS.exists() @@ -593,7 +593,7 @@ static void decodeIRJson(uint32_t code) decBrightness(); } else if (cmdStr.startsWith(F("!presetF"))) { //!presetFallback uint8_t p1 = fdo["PL"] | 1; - uint8_t p2 = fdo["FX"] | random8(strip.getModeCount() -1); + uint8_t p2 = fdo["FX"] | hw_random8(strip.getModeCount() -1); uint8_t p3 = fdo["FP"] | 0; presetFallback(p1, p2, p3); } @@ -611,9 +611,15 @@ static void decodeIRJson(uint32_t code) handleSet(nullptr, cmdStr, false); // no stateUpdated() call here } } else { - // command is JSON object (TODO: currently will not handle irApplyToAllSelected correctly) - if (jsonCmdObj[F("psave")].isNull()) deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET); - else { + // command is JSON object + if (jsonCmdObj[F("psave")].isNull()) { + if (irApplyToAllSelected && jsonCmdObj["seg"].is()) { + JsonObject seg = jsonCmdObj["seg"][0]; // take 1st segment from array and use it to apply to all selected segments + seg.remove("id"); // remove segment ID if it exists + jsonCmdObj["seg"] = seg; // replace array with object + } + deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET); // **will call stateUpdated() with correct CALL_MODE** + } else { uint8_t psave = jsonCmdObj[F("psave")].as(); char pname[33]; sprintf_P(pname, PSTR("IR Preset %d"), psave); @@ -628,6 +634,7 @@ static void applyRepeatActions() { if (irEnabled == 8) { decodeIRJson(lastValidCode); + stateUpdated(CALL_MODE_BUTTON_PRESET); return; } else switch (lastRepeatableAction) { case ACTION_BRIGHT_UP : incBrightness(); stateUpdated(CALL_MODE_BUTTON); return; @@ -664,7 +671,7 @@ static void decodeIR(uint32_t code) if (irEnabled == 8) { // any remote configurable with ir.json file decodeIRJson(code); - stateUpdated(CALL_MODE_BUTTON); + stateUpdated(CALL_MODE_BUTTON_PRESET); return; } if (code > 0xFFFFFF) return; //invalid code diff --git a/wled00/json.cpp b/wled00/json.cpp index 288059653f..d7521de42c 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1,6 +1,5 @@ #include "wled.h" -#include "palettes.h" #define JSON_PATH_STATE 1 #define JSON_PATH_INFO 2 @@ -14,11 +13,65 @@ /* * JSON API (De)serialization */ +namespace { + typedef struct { + uint32_t colors[NUM_COLORS]; + uint16_t start; + uint16_t stop; + uint16_t offset; + uint16_t grouping; + uint16_t spacing; + uint16_t startY; + uint16_t stopY; + uint16_t options; + uint8_t mode; + uint8_t palette; + uint8_t opacity; + uint8_t speed; + uint8_t intensity; + uint8_t custom1; + uint8_t custom2; + uint8_t custom3; + bool check1; + bool check2; + bool check3; + } SegmentCopy; + + uint8_t differs(const Segment& b, const SegmentCopy& a) { + uint8_t d = 0; + if (a.start != b.start) d |= SEG_DIFFERS_BOUNDS; + if (a.stop != b.stop) d |= SEG_DIFFERS_BOUNDS; + if (a.offset != b.offset) d |= SEG_DIFFERS_GSO; + if (a.grouping != b.grouping) d |= SEG_DIFFERS_GSO; + if (a.spacing != b.spacing) d |= SEG_DIFFERS_GSO; + if (a.opacity != b.opacity) d |= SEG_DIFFERS_BRI; + if (a.mode != b.mode) d |= SEG_DIFFERS_FX; + if (a.speed != b.speed) d |= SEG_DIFFERS_FX; + if (a.intensity != b.intensity) d |= SEG_DIFFERS_FX; + if (a.palette != b.palette) d |= SEG_DIFFERS_FX; + if (a.custom1 != b.custom1) d |= SEG_DIFFERS_FX; + if (a.custom2 != b.custom2) d |= SEG_DIFFERS_FX; + if (a.custom3 != b.custom3) d |= SEG_DIFFERS_FX; + if (a.check1 != b.check1) d |= SEG_DIFFERS_FX; + if (a.check2 != b.check2) d |= SEG_DIFFERS_FX; + if (a.check3 != b.check3) d |= SEG_DIFFERS_FX; + if (a.startY != b.startY) d |= SEG_DIFFERS_BOUNDS; + if (a.stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; + + //bit pattern: (msb first) + // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected] + if ((a.options & 0b1111111111011110U) != (b.options & 0b1111111111011110U)) d |= SEG_DIFFERS_OPT; + if ((a.options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; + for (unsigned i = 0; i < NUM_COLORS; i++) if (a.colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; + + return d; + } +} -bool deserializeSegment(JsonObject elem, byte it, byte presetId) +static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) { byte id = elem["id"] | it; - if (id >= strip.getMaxSegments()) return false; + if (id >= WS2812FX::getMaxSegments()) return false; bool newSeg = false; int stop = elem["stop"] | -1; @@ -26,16 +79,37 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) // append segment if (id >= strip.getSegmentsNum()) { if (stop <= 0) return false; // ignore empty/inactive segments - strip.appendSegment(Segment(0, strip.getLengthTotal())); + strip.appendSegment(0, strip.getLengthTotal()); id = strip.getSegmentsNum()-1; // segments are added at the end of list newSeg = true; } //DEBUG_PRINTLN(F("-- JSON deserialize segment.")); Segment& seg = strip.getSegment(id); - //DEBUG_PRINTF_P(PSTR("-- Original segment: %p (%p)\n"), &seg, seg.data); - Segment prev = seg; //make a backup so we can tell if something changed (calling copy constructor) - //DEBUG_PRINTF_P(PSTR("-- Duplicate segment: %p (%p)\n"), &prev, prev.data); + // we do not want to make segment copy as it may use a lot of RAM (effect data and pixel buffer) + // so we will create a copy of segment options and compare it with original segment when done processing + SegmentCopy prev = { + {seg.colors[0], seg.colors[1], seg.colors[2]}, + seg.start, + seg.stop, + seg.offset, + seg.grouping, + seg.spacing, + seg.startY, + seg.stopY, + seg.options, + seg.mode, + seg.palette, + seg.opacity, + seg.speed, + seg.intensity, + seg.custom1, + seg.custom2, + seg.custom3, + seg.check1, + seg.check2, + seg.check3 + }; int start = elem["start"] | seg.start; if (stop < 0) { @@ -53,7 +127,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) elem.remove("rpt"); // remove for recursive call elem.remove("n"); // remove for recursive call unsigned len = stop - start; - for (size_t i=id+1; i= strip.getLengthTotal()) break; //TODO: add support for 2D @@ -67,46 +141,35 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (elem["n"]) { // name field exists - if (seg.name) { //clear old name - delete[] seg.name; - seg.name = nullptr; - } - const char * name = elem["n"].as(); - size_t len = 0; - if (name != nullptr) len = strlen(name); - if (len > 0) { - if (len > WLED_MAX_SEGNAME_LEN) len = WLED_MAX_SEGNAME_LEN; - seg.name = new char[len+1]; - if (seg.name) strlcpy(seg.name, name, WLED_MAX_SEGNAME_LEN+1); - } else { - // but is empty (already deleted above) - elem.remove("n"); - } + seg.setName(name); // will resolve empty and null correctly } else if (start != seg.start || stop != seg.stop) { // clearing or setting segment without name field - if (seg.name) { - delete[] seg.name; - seg.name = nullptr; - } + seg.clearName(); } - uint16_t grp = elem["grp"] | seg.grouping; - uint16_t spc = elem[F("spc")] | seg.spacing; - uint16_t of = seg.offset; - uint8_t soundSim = elem["si"] | seg.soundSim; - uint8_t map1D2D = elem["m12"] | seg.map1D2D; - - if ((spc>0 && spc!=seg.spacing) || seg.map1D2D!=map1D2D) seg.fill(BLACK); // clear spacing gaps - - seg.map1D2D = constrain(map1D2D, 0, 7); - seg.soundSim = constrain(soundSim, 0, 3); + uint16_t grp = elem["grp"] | seg.grouping; + uint16_t spc = elem[F("spc")] | seg.spacing; + uint16_t of = seg.offset; + uint8_t soundSim = elem["si"] | seg.soundSim; + uint8_t map1D2D = elem["m12"] | seg.map1D2D; + uint8_t set = elem[F("set")] | seg.set; + bool selected = getBoolVal(elem["sel"], seg.selected); + bool reverse = getBoolVal(elem["rev"], seg.reverse); + bool mirror = getBoolVal(elem["mi"] , seg.mirror); + #ifndef WLED_DISABLE_2D + bool reverse_y = getBoolVal(elem["rY"] , seg.reverse_y); + bool mirror_y = getBoolVal(elem["mY"] , seg.mirror_y); + bool transpose = getBoolVal(elem[F("tp")], seg.transpose); + #endif - uint8_t set = elem[F("set")] | seg.set; - seg.set = constrain(set, 0, 3); + // if segment's virtual dimensions change we need to restart effect (segment blending and PS rely on dimensions) + if (seg.mirror != mirror) seg.markForReset(); + #ifndef WLED_DISABLE_2D + if (seg.mirror_y != mirror_y || seg.transpose != transpose) seg.markForReset(); + #endif - int len = 1; - if (stop > start) len = stop - start; + int len = (stop > start) ? stop - start : 1; int offset = elem[F("of")] | INT32_MAX; if (offset != INT32_MAX) { int offsetAbs = abs(offset); @@ -117,7 +180,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (stop > start && of > len -1) of = len -1; // update segment (delete if necessary) - seg.setUp(start, stop, grp, spc, of, startY, stopY); // strip needs to be suspended for this to work without issues + seg.setGeometry(start, stop, grp, spc, of, startY, stopY, map1D2D); // strip needs to be suspended for this to work without issues if (newSeg) seg.refreshLightCapabilities(); // fix for #3403 @@ -127,8 +190,8 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } byte segbri = seg.opacity; - if (getVal(elem["bri"], &segbri)) { - if (segbri > 0) seg.setOpacity(segbri); + if (getVal(elem["bri"], segbri)) { + if (segbri > 0) seg.setOpacity(segbri); // use transition seg.setOption(SEG_OPTION_ON, segbri); // use transition } @@ -146,7 +209,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) // JSON "col" array can contain the following values for each of segment's colors (primary, background, custom): // "col":[int|string|object|array, int|string|object|array, int|string|object|array] // int = Kelvin temperature or 0 for black - // string = hex representation of [WW]RRGGBB + // string = hex representation of [WW]RRGGBB or "r" for random color // object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {}) // array = direct channel values [r,g,b,w] (w element being optional) int rgbw[] = {0,0,0,0}; @@ -170,6 +233,9 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (kelvin == 0) seg.setColor(i, 0); if (kelvin > 0) colorKtoRGB(kelvin, brgbw); colValid = true; + } else if (hexCol[0] == 'r' && hexCol[1] == '\0') { // Random colors via JSON API in Segment object like col=["r","r","r"] · Issue #4996 + setRandomColor(brgbw); + colValid = true; } else { //HEX string, e.g. "FFAA00" colValid = colorFromHexString(brgbw, hexCol); } @@ -184,13 +250,13 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (!colValid) continue; - seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); + seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); // use transition if (seg.mode == FX_MODE_STATIC) strip.trigger(); //instant refresh } } else { // non RGB & non White segment (usually On/Off bus) - seg.setColor(0, ULTRAWHITE); - seg.setColor(1, BLACK); + seg.setColor(0, ULTRAWHITE); // use transition + seg.setColor(1, BLACK); // use transition } } @@ -206,87 +272,70 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } #endif + seg.set = constrain(set, 0, 3); + seg.soundSim = constrain(soundSim, 0, 3); + seg.selected = selected; + seg.reverse = reverse; + seg.mirror = mirror; #ifndef WLED_DISABLE_2D - bool reverse = seg.reverse; - bool mirror = seg.mirror; - #endif - seg.selected = getBoolVal(elem["sel"], seg.selected); - seg.reverse = getBoolVal(elem["rev"], seg.reverse); - seg.mirror = getBoolVal(elem["mi"] , seg.mirror); - #ifndef WLED_DISABLE_2D - bool reverse_y = seg.reverse_y; - bool mirror_y = seg.mirror_y; - seg.reverse_y = getBoolVal(elem["rY"] , seg.reverse_y); - seg.mirror_y = getBoolVal(elem["mY"] , seg.mirror_y); - seg.transpose = getBoolVal(elem[F("tp")], seg.transpose); - if (seg.is2D() && seg.map1D2D == M12_pArc && (reverse != seg.reverse || reverse_y != seg.reverse_y || mirror != seg.mirror || mirror_y != seg.mirror_y)) seg.fill(BLACK); // clear entire segment (in case of Arc 1D to 2D expansion) + seg.reverse_y = reverse_y; + seg.mirror_y = mirror_y; + seg.transpose = transpose; #endif byte fx = seg.mode; - byte last = strip.getModeCount(); - // partial fix for #3605 - if (!elem["fx"].isNull() && elem["fx"].is()) { - const char *tmp = elem["fx"].as(); - if (strlen(tmp) > 3 && (strchr(tmp,'r') || strchr(tmp,'~') != strrchr(tmp,'~'))) last = 0; // we have "X~Y(r|[w]~[-])" form - } - // end fix - if (getVal(elem["fx"], &fx, 0, last)) { //load effect ('r' random, '~' inc/dec, 0-255 exact value, 5~10r pick random between 5 & 10) + if (getVal(elem["fx"], fx, 0, strip.getModeCount())) { if (!presetId && currentPlaylist>=0) unloadPlaylist(); - if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); + if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); // use transition (WARNING: may change map1D2D causing geometry change) } - //getVal also supports inc/decrementing and random - getVal(elem["sx"], &seg.speed); - getVal(elem["ix"], &seg.intensity); + getVal(elem["sx"], seg.speed); + getVal(elem["ix"], seg.intensity); uint8_t pal = seg.palette; - last = strip.getPaletteCount(); - if (!elem["pal"].isNull() && elem["pal"].is()) { - const char *tmp = elem["pal"].as(); - if (strlen(tmp) > 3 && (strchr(tmp,'r') || strchr(tmp,'~') != strrchr(tmp,'~'))) last = 0; // we have "X~Y(r|[w]~[-])" form - } if (seg.getLightCapabilities() & 1) { // ignore palette for White and On/Off segments - if (getVal(elem["pal"], &pal, 0, last)) seg.setPalette(pal); + if (getVal(elem["pal"], pal, 0, getPaletteCount())) seg.setPalette(pal); } - getVal(elem["c1"], &seg.custom1); - getVal(elem["c2"], &seg.custom2); + getVal(elem["c1"], seg.custom1); + getVal(elem["c2"], seg.custom2); uint8_t cust3 = seg.custom3; - getVal(elem["c3"], &cust3, 0, 31); // we can't pass reference to bitfield + getVal(elem["c3"], cust3, 0, 31); // we can't pass reference to bitfield seg.custom3 = constrain(cust3, 0, 31); seg.check1 = getBoolVal(elem["o1"], seg.check1); seg.check2 = getBoolVal(elem["o2"], seg.check2); seg.check3 = getBoolVal(elem["o3"], seg.check3); + uint8_t blend = seg.blendMode; + getVal(elem["bm"], blend, 0, 15); // we can't pass reference to bitfield + seg.blendMode = constrain(blend, 0, 15); + JsonArray iarr = elem[F("i")]; //set individual LEDs if (!iarr.isNull()) { - uint8_t oldMap1D2D = seg.map1D2D; - seg.map1D2D = M12_Pixels; // no mapping - // set brightness immediately and disable transition jsonTransitionOnce = true; - seg.stopTransition(); + if (seg.isInTransition()) seg.startTransition(0); // setting transition time to 0 will stop transition in next frame strip.setTransition(0); - strip.setBrightness(scaledBri(bri), true); + strip.setBrightness(bri, true); // freeze and init to black if (!seg.freeze) { seg.freeze = true; - seg.fill(BLACK); + seg.clear(); } - start = 0, stop = 0; - set = 0; //0 nothing set, 1 start set, 2 range set + unsigned iStart = 0, iStop = 0; + unsigned iSet = 0; //0 nothing set, 1 start set, 2 range set for (size_t i = 0; i < iarr.size(); i++) { - if(iarr[i].is()) { - if (!set) { - start = abs(iarr[i].as()); - set++; + if (iarr[i].is()) { + if (!iSet) { + iStart = abs(iarr[i].as()); + iSet++; } else { - stop = abs(iarr[i].as()); - set++; + iStop = abs(iarr[i].as()); + iSet++; } } else { //color uint8_t rgbw[] = {0,0,0,0}; @@ -302,17 +351,16 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } } - if (set < 2 || stop <= start) stop = start + 1; - uint32_t c = gamma32(RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); - while (start < stop) seg.setPixelColor(start++, c); - set = 0; + if (iSet < 2 || iStop <= iStart) iStop = iStart + 1; + uint32_t c = RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3]); + while (iStart < iStop) seg.setRawPixelColor(iStart++, c); // sets pixel color without 1D->2D expansion, grouping or spacing + iSet = 0; } } - seg.map1D2D = oldMap1D2D; // restore mapping strip.trigger(); // force segment update } // send UDP/WS if segment options changed (except selection; will also deselect current preset) - if (seg.differs(prev) & 0x7F) stateChanged = true; + if (differs(seg, prev) & ~SEG_DIFFERS_SEL) stateChanged = true; return true; } @@ -328,7 +376,8 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) #endif bool onBefore = bri; - getVal(root["bri"], &bri); + getVal(root["bri"], bri); + if (bri != briOld) stateChanged = true; bool on = root["on"] | (bri > 0); if (!on != !bri) toggleOnOff(); @@ -351,21 +400,25 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) tr = root[F("transition")] | -1; if (tr >= 0) { transitionDelay = tr * 100; - if (fadeTransition) strip.setTransition(transitionDelay); + strip.setTransition(transitionDelay); } } + blendingStyle = root[F("bs")] | blendingStyle; + blendingStyle &= 0x1F; + // temporary transition (applies only once) tr = root[F("tt")] | -1; if (tr >= 0) { jsonTransitionOnce = true; - if (fadeTransition) strip.setTransition(tr * 100); + strip.setTransition(tr * 100); } tr = root[F("tb")] | -1; if (tr >= 0) strip.timebase = (unsigned long)tr - millis(); JsonObject nl = root["nl"]; + if (!nl.isNull()) stateChanged = true; nightlightActive = getBoolVal(nl["on"], nightlightActive); nightlightDelayMins = nl["dur"] | nightlightDelayMins; nightlightMode = nl["mode"] | nightlightMode; @@ -392,6 +445,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS; if (realtimeMode && useMainSegmentOnly) { strip.getMainSegment().freeze = !realtimeOverride; + realtimeOverride = REALTIME_OVERRIDE_NONE; // ignore request for override if using main segment only } if (root.containsKey("live")) { @@ -406,35 +460,34 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) int it = 0; JsonVariant segVar = root["seg"]; - if (!segVar.isNull()) strip.suspend(); - if (segVar.is()) - { - int id = segVar["id"] | -1; - //if "seg" is not an array and ID not specified, apply to all selected/checked segments - if (id < 0) { - //apply all selected segments - //bool didSet = false; - for (size_t s = 0; s < strip.getSegmentsNum(); s++) { - Segment &sg = strip.getSegment(s); - if (sg.isActive() && sg.isSelected()) { - deserializeSegment(segVar, s, presetId); - //didSet = true; + if (!segVar.isNull()) { + // we may be called during strip.service() so we must not modify segments while effects are executing + strip.suspend(); + strip.waitForIt(); + if (segVar.is()) { + int id = segVar["id"] | -1; + //if "seg" is not an array and ID not specified, apply to all selected/checked segments + if (id < 0) { + //apply all selected segments + for (size_t s = 0; s < strip.getSegmentsNum(); s++) { + const Segment &sg = strip.getSegment(s); + if (sg.isActive() && sg.isSelected()) { + deserializeSegment(segVar, s, presetId); + } } + } else { + deserializeSegment(segVar, id, presetId); //apply only the segment with the specified ID } - //TODO: not sure if it is good idea to change first active but unselected segment - //if (!didSet) deserializeSegment(segVar, strip.getMainSegmentId(), presetId); } else { - deserializeSegment(segVar, id, presetId); //apply only the segment with the specified ID - } - } else { - size_t deleted = 0; - JsonArray segs = segVar.as(); - for (JsonObject elem : segs) { - if (deserializeSegment(elem, it++, presetId) && !elem["stop"].isNull() && elem["stop"]==0) deleted++; + size_t deleted = 0; + JsonArray segs = segVar.as(); + for (JsonObject elem : segs) { + if (deserializeSegment(elem, it++, presetId) && !elem["stop"].isNull() && elem["stop"]==0) deleted++; + } + if (strip.getSegmentsNum() > 3 && deleted >= strip.getSegmentsNum()/2U) strip.purgeSegments(); // batch deleting more than half segments } - if (strip.getSegmentsNum() > 3 && deleted >= strip.getSegmentsNum()/2U) strip.purgeSegments(); // batch deleting more than half segments + strip.resume(); } - strip.resume(); UsermodManager::readFromJsonState(root); @@ -467,7 +520,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) DEBUG_PRINTF_P(PSTR("Preset direct: %d\n"), currentPreset); } else if (!root["ps"].isNull()) { // we have "ps" call (i.e. from button or external API call) or "pd" that includes "ps" (i.e. from UI call) - if (root["win"].isNull() && getVal(root["ps"], &presetCycCurr, 0, 0) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) { + if (root["win"].isNull() && getVal(root["ps"], presetCycCurr, 1, 250) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) { DEBUG_PRINTF_P(PSTR("Preset select: %d\n"), presetCycCurr); // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) applyPreset(presetCycCurr, callMode); // async load from file system (only preset ID was specified) @@ -482,17 +535,15 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) else callMode = CALL_MODE_DIRECT_CHANGE; // possible bugfix for playlist only containing HTTP API preset FX=~ } - if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { - if (strip.customPalettes.size()) { - char fileName[32]; - sprintf_P(fileName, PSTR("/palette%d.json"), strip.customPalettes.size()-1); - if (WLED_FS.exists(fileName)) WLED_FS.remove(fileName); - strip.loadCustomPalettes(); - } + if (root.containsKey(F("rmcpal"))) { + char fileName[32]; + sprintf_P(fileName, PSTR("/palette%d.json"), root[F("rmcpal")].as()); + if (WLED_FS.exists(fileName)) WLED_FS.remove(fileName); + loadCustomPalettes(); } doAdvancePlaylist = root[F("np")] | doAdvancePlaylist; //advances to next preset in playlist when true - + JsonObject wifi = root[F("wifi")]; if (!wifi.isNull()) { bool apMode = getBoolVal(wifi[F("ap")], apActive); @@ -506,13 +557,13 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) //if (restart) forceReconnect = true; } - stateUpdated(callMode); + if (stateChanged) stateUpdated(callMode); if (presetToRestore) currentPreset = presetToRestore; return stateResponse; } -void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset, bool segmentBounds) +static void serializeSegment(JsonObject& root, const Segment& seg, byte id, bool forPreset, bool segmentBounds) { root["id"] = id; if (segmentBounds) { @@ -535,6 +586,7 @@ void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset, b root["bri"] = (segbri) ? segbri : 255; root["cct"] = seg.cct; root[F("set")] = seg.set; + root["lc"] = seg.getLightCapabilities(); if (seg.name != nullptr) root["n"] = reinterpret_cast(seg.name); //not good practice, but decreases required JSON buffer else if (forPreset) root["n"] = ""; @@ -579,6 +631,7 @@ void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset, b root["o3"] = seg.check3; root["si"] = seg.soundSim; root["m12"] = seg.map1D2D; + root["bm"] = seg.blendMode; } void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segmentBounds, bool selectedSegmentsOnly) @@ -587,6 +640,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root["on"] = (bri > 0); root["bri"] = briLast; root[F("transition")] = transitionDelay/100; //in 100ms + root[F("bs")] = blendingStyle; } if (!forPreset) { @@ -617,7 +671,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root[F("mainseg")] = strip.getMainSegmentId(); JsonArray seg = root.createNestedArray("seg"); - for (size_t s = 0; s < strip.getMaxSegments(); s++) { + for (size_t s = 0; s < WS2812FX::getMaxSegments(); s++) { if (s >= strip.getSegmentsNum()) { if (forPreset && segmentBounds && !selectedSegmentsOnly) { //disable segments not part of preset JsonObject seg0 = seg.createNestedObject(); @@ -626,7 +680,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme } else break; } - Segment &sg = strip.getSegment(s); + const Segment &sg = strip.getSegment(s); if (forPreset && selectedSegmentsOnly && !sg.isSelected()) continue; if (sg.isActive()) { JsonObject seg0 = seg.createNestedObject(); @@ -638,19 +692,22 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme } } + void serializeInfo(JsonObject root) { root[F("ver")] = versionString; root[F("vid")] = VERSION; root[F("cn")] = F(WLED_CODENAME); root[F("release")] = releaseString; + root[F("repo")] = repoString; + root[F("deviceId")] = getDeviceId(); JsonObject leds = root.createNestedObject(F("leds")); leds[F("count")] = strip.getLengthTotal(); leds[F("pwr")] = BusManager::currentMilliamps(); leds["fps"] = strip.getFps(); leds[F("maxpwr")] = BusManager::currentMilliamps()>0 ? BusManager::ablMilliampsMax() : 0; - leds[F("maxseg")] = strip.getMaxSegments(); + leds[F("maxseg")] = WS2812FX::getMaxSegments(); //leds[F("actseg")] = strip.getActiveSegmentsNum(); //leds[F("seglock")] = false; //might be used in the future to prevent modifications to segment config leds[F("bootps")] = bootPreset; @@ -664,13 +721,13 @@ void serializeInfo(JsonObject root) #endif unsigned totalLC = 0; - JsonArray lcarr = leds.createNestedArray(F("seglc")); + JsonArray lcarr = leds.createNestedArray(F("seglc")); // deprecated, use state.seg[].lc size_t nSegs = strip.getSegmentsNum(); for (size_t s = 0; s < nSegs; s++) { if (!strip.getSegment(s).isActive()) continue; unsigned lc = strip.getSegment(s).getLightCapabilities(); totalLC |= lc; - lcarr.add(lc); + lcarr.add(lc); // deprecated, use state.seg[].lc } leds["lc"] = totalLC; @@ -689,7 +746,7 @@ void serializeInfo(JsonObject root) spi.add(spi_miso); #endif - root[F("str")] = false; //syncToggleReceive; + root[F("str")] = false; // sync toggle receive root[F("name")] = serverDescription; root[F("udpport")] = udpPort; @@ -718,8 +775,9 @@ void serializeInfo(JsonObject root) #endif root[F("fxcount")] = strip.getModeCount(); - root[F("palcount")] = strip.getPaletteCount(); - root[F("cpalcount")] = strip.customPalettes.size(); //number of custom palettes + root[F("palcount")] = getPaletteCount(); + root[F("cpalcount")] = customPalettes.size(); // number of custom palettes + root[F("cpalmax")] = WLED_MAX_CUSTOM_PALETTES; // maximum number of custom palettes JsonArray ledmaps = root.createNestedArray(F("maps")); for (size_t i=0; ireason; #endif root[F("lwip")] = LWIP_VERSION_MAJOR; #endif - root[F("freeheap")] = ESP.getFreeHeap(); - #if defined(ARDUINO_ARCH_ESP32) - if (psramSafe && psramFound()) root[F("psram")] = ESP.getFreePsram(); + root[F("freeheap")] = getFreeHeapSize(); + #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + // Report PSRAM information + // Free PSRAM in bytes (backward compatibility) + root[F("psram")] = ESP.getFreePsram(); + // Total PSRAM size in MB, round up to correct for allocator overhead + root[F("psrSz")] = (ESP.getPsramSize() + (1024U * 1024U - 1)) / (1024U * 1024U); #endif root[F("uptime")] = millis()/1000 + rolloverMillis*4294967; @@ -833,7 +898,7 @@ void serializeInfo(JsonObject root) root["ip"] = s; } -void setPaletteColors(JsonArray json, CRGBPalette16 palette) +static void setPaletteColors(JsonArray json, CRGBPalette16 palette) { for (int i = 0; i < 16; i++) { JsonArray colors = json.createNestedArray(); @@ -845,7 +910,7 @@ void setPaletteColors(JsonArray json, CRGBPalette16 palette) } } -void setPaletteColors(JsonArray json, byte* tcp) +static void setPaletteColors(JsonArray json, byte* tcp) { TRGBGradientPaletteEntryUnion* ent = (TRGBGradientPaletteEntryUnion*)(tcp); TRGBGradientPaletteEntryUnion u; @@ -877,35 +942,31 @@ void serializePalettes(JsonObject root, int page) { byte tcp[72]; #ifdef ESP8266 - int itemPerPage = 5; + constexpr int itemPerPage = 5; #else - int itemPerPage = 8; + constexpr int itemPerPage = 8; #endif - int customPalettes = strip.customPalettes.size(); - int palettesCount = strip.getPaletteCount() - customPalettes; + const int customPalettesCount = customPalettes.size(); + const int palettesCount = FIXED_PALETTE_COUNT; // palettesCount is number of palettes, not palette index - int maxPage = (palettesCount + customPalettes -1) / itemPerPage; + const int maxPage = (palettesCount + customPalettesCount) / itemPerPage; if (page > maxPage) page = maxPage; - int start = itemPerPage * page; - int end = start + itemPerPage; - if (end > palettesCount + customPalettes) end = palettesCount + customPalettes; + const int start = itemPerPage * page; + int end = min(start + itemPerPage, palettesCount + customPalettesCount); root[F("m")] = maxPage; // inform caller how many pages there are JsonObject palettes = root.createNestedObject("p"); for (int i = start; i < end; i++) { - JsonArray curPalette = palettes.createNestedArray(String(i>=palettesCount ? 255 - i + palettesCount : i)); + JsonArray curPalette = palettes.createNestedArray(String(i >= palettesCount ? 255 - i + palettesCount : i)); switch (i) { case 0: //default palette setPaletteColors(curPalette, PartyColors_p); break; case 1: //random - curPalette.add("r"); - curPalette.add("r"); - curPalette.add("r"); - curPalette.add("r"); + for (int j = 0; j < 4; j++) curPalette.add("r"); break; case 2: //primary color only curPalette.add("c1"); @@ -922,53 +983,20 @@ void serializePalettes(JsonObject root, int page) curPalette.add("c1"); break; case 5: //primary + secondary (+tertiary if not off), more distinct + for (int j = 0; j < 5; j++) curPalette.add("c1"); + for (int j = 0; j < 5; j++) curPalette.add("c2"); + for (int j = 0; j < 5; j++) curPalette.add("c3"); curPalette.add("c1"); - curPalette.add("c1"); - curPalette.add("c1"); - curPalette.add("c1"); - curPalette.add("c1"); - curPalette.add("c2"); - curPalette.add("c2"); - curPalette.add("c2"); - curPalette.add("c2"); - curPalette.add("c2"); - curPalette.add("c3"); - curPalette.add("c3"); - curPalette.add("c3"); - curPalette.add("c3"); - curPalette.add("c3"); - curPalette.add("c1"); - break; - case 6: //Party colors - setPaletteColors(curPalette, PartyColors_p); - break; - case 7: //Cloud colors - setPaletteColors(curPalette, CloudColors_p); - break; - case 8: //Lava colors - setPaletteColors(curPalette, LavaColors_p); - break; - case 9: //Ocean colors - setPaletteColors(curPalette, OceanColors_p); - break; - case 10: //Forest colors - setPaletteColors(curPalette, ForestColors_p); - break; - case 11: //Rainbow colors - setPaletteColors(curPalette, RainbowColors_p); - break; - case 12: //Rainbow stripe colors - setPaletteColors(curPalette, RainbowStripeColors_p); break; default: - { - if (i>=palettesCount) { - setPaletteColors(curPalette, strip.customPalettes[i - palettesCount]); - } else { - memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - 13])), 72); + if (i >= palettesCount) // custom palettes + setPaletteColors(curPalette, customPalettes[i - palettesCount]); + else if (i < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) // palette 6 - 12, fastled palettes + setPaletteColors(curPalette, *fastledPalettes[i - DYNAMIC_PALETTE_COUNT]); + else { + memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp)); setPaletteColors(curPalette, tcp); } - } break; } } @@ -1021,6 +1049,120 @@ void serializeNodes(JsonObject root) } } +void serializePins(JsonObject root) +{ + JsonArray pins = root.createNestedArray(F("pins")); + #ifdef ESP8266 + constexpr int ENUM_PINS = WLED_NUM_PINS; // GPIO0-16 (A0 (17) is analog input only and always assigned to any analog input, even if set "unused") TODO: can currently not be handled + #else + constexpr int ENUM_PINS = WLED_NUM_PINS; + #endif + for (int gpio = 0; gpio < ENUM_PINS; gpio++) { + bool canInput = PinManager::isPinOk(gpio, false); + bool canOutput = PinManager::isPinOk(gpio, true); + bool isAllocated = PinManager::isPinAllocated(gpio); + // Skip pins that are neither usable nor allocated (truly unusable pins) + if (!canInput && !canOutput && !isAllocated) continue; + + JsonObject pinObj = pins.createNestedObject(); + pinObj["p"] = gpio; // pin number + + // Pin capabilities + // Touch capability is provided by appendGPIOinfo() via d.touch + uint8_t caps = 0; + + #ifdef ARDUINO_ARCH_ESP32 + if (PinManager::isAnalogPin(gpio)) caps |= PIN_CAP_ADC; + + // PWM on all ESP32 variants: all output pins can use ledc PWM so this is redundant + //if (canOutput) caps |= PIN_CAP_PWM; + + // Input-only pins (ESP32 classic: GPIO34-39) + if (canInput && !canOutput) caps |= PIN_CAP_INPUT_ONLY; + + // Bootloader/strapping pins + #if defined(CONFIG_IDF_TARGET_ESP32S3) + if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode + if (gpio == 45 || gpio == 46) caps |= PIN_CAP_BOOTSTRAP; // IO46 must be low to enter bootloader mode, IO45 controls flash voltage, keep low for 3.3V flash + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode + if (gpio == 45 || gpio == 46) caps |= PIN_CAP_BOOTSTRAP; // IO46 must be low to enter bootloader mode, IO45 controls flash voltage, keep low for 3.3V flash + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + if (gpio == 9) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode + if (gpio == 2 || gpio == 8) caps |= PIN_CAP_BOOTSTRAP; // both GPIO2 and GPIO8 must be high to enter bootloader mode + #elif defined(CONFIG_IDF_TARGET_ESP32) // ESP32 classic + if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode + if (gpio == 2 || gpio == 12) caps |= PIN_CAP_BOOTSTRAP; // note: if GPIO12 must be low at boot, (high=1.8V flash mode), GPIO 2 must be low or floating to enter bootloader mode + #endif + #else + // ESP8266: GPIO 0-16 + GPIO17=A0 + // if (gpio < 16) caps |= PIN_CAP_PWM; // software PWM available on all GPIO except GPIO16 + // ESP8266 strapping pins + if (gpio == 0) caps |= PIN_CAP_BOOT; + if (gpio == 2 || gpio == 15) caps |= PIN_CAP_BOOTSTRAP; // GPIO2 must be high, GPIO15 low to boot normally + if (gpio == 17) caps = PIN_CAP_INPUT_ONLY | PIN_CAP_ADC; // TODO: display as A0 pin + #endif + + pinObj["c"] = caps; // capabilities + + // Add allocated status and owner + pinObj["a"] = isAllocated; // allocated status + + // check if this pin is used as a button (need to get button type for owner name) + int buttonIndex = PinManager::getButtonIndex(gpio); // returns -1 if not a button pin, otherwise returns index in buttons array + + // Add owner ID and name + PinOwner owner = PinManager::getPinOwner(gpio); + if (isAllocated) { + pinObj["o"] = static_cast(owner); // owner ID (can be used for UI lookup) + pinObj["n"] = PinManager::getPinOwnerName(gpio); // owner name (string) + + // Relay pin + if (owner == PinOwner::Relay) { + pinObj["m"] = 1; // mode: output + pinObj["s"] = digitalRead(rlyPin); // read state from hardware (digitalRead returns output state for output pins) + } + // Button pins, get type and state using isButtonPressed() + else if (buttonIndex >= 0) { + pinObj["m"] = 0; // mode: input + pinObj["t"] = buttons[buttonIndex].type; // button type + pinObj["s"] = isButtonPressed(buttonIndex) ? 1 : 0; // state + + // for touch buttons, get raw reading value (useful for debugging threshold) + #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + if (buttons[buttonIndex].type == BTN_TYPE_TOUCH || buttons[buttonIndex].type == BTN_TYPE_TOUCH_SWITCH) { + if (digitalPinToTouchChannel(gpio) >= 0) { + #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 + pinObj["r"] = touchRead(gpio) >> 4; // Touch V2 returns larger values, right shift by 4 to match threshold range, see set.cpp + #else + pinObj["r"] = touchRead(gpio); // send raw value + #endif + } + } + #endif + // for analog buttons, get raw reading value + if (buttons[buttonIndex].type == BTN_TYPE_ANALOG || buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) { + int analogRaw = 0; + #ifdef ESP8266 + analogRaw = analogRead(A0) >> 2; // convert 10bit read to 8bit, ESP8266 only has one analog pin + #else + if (digitalPinToAnalogChannel(gpio) >= 0) { + analogRaw = (analogRead(gpio)>>4); // right shift to match button value (8bit) see button.cpp + } + #endif + if (buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) analogRaw = 255 - analogRaw; + pinObj["r"] = analogRaw; // send raw value + } + } + // other allocated output pins that are simple GPIO (BusOnOff, Multi Relay, etc.) TODO: expand for other pin owners as needed + else if (owner == PinOwner::BusOnOff || owner == PinOwner::UM_MultiRelay) { + pinObj["m"] = 1; // mode: output + pinObj["s"] = digitalRead(gpio); // read state from hardware (digitalRead returns output state for output pins) + } + } + } +} + // deserializes mode data string into JsonArray void serializeModeData(JsonArray fxdata) { @@ -1078,16 +1220,22 @@ class LockedJsonResponse: public AsyncJsonResponse { void serveJson(AsyncWebServerRequest* request) { - byte subJson = 0; + enum class json_target { + all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config, pins + }; + json_target subJson = json_target::all; + const String& url = request->url(); - if (url.indexOf("state") > 0) subJson = JSON_PATH_STATE; - else if (url.indexOf("info") > 0) subJson = JSON_PATH_INFO; - else if (url.indexOf("si") > 0) subJson = JSON_PATH_STATE_INFO; - else if (url.indexOf(F("nodes")) > 0) subJson = JSON_PATH_NODES; - else if (url.indexOf(F("eff")) > 0) subJson = JSON_PATH_EFFECTS; - else if (url.indexOf(F("palx")) > 0) subJson = JSON_PATH_PALETTES; - else if (url.indexOf(F("fxda")) > 0) subJson = JSON_PATH_FXDATA; - else if (url.indexOf(F("net")) > 0) subJson = JSON_PATH_NETWORKS; + if (url.indexOf("state") > 0) subJson = json_target::state; + else if (url.indexOf("info") > 0) subJson = json_target::info; + else if (url.indexOf("si") > 0) subJson = json_target::state_info; + else if (url.indexOf(F("nodes")) > 0) subJson = json_target::nodes; + else if (url.indexOf(F("eff")) > 0) subJson = json_target::effects; + else if (url.indexOf(F("palx")) > 0) subJson = json_target::palettes; + else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata; + else if (url.indexOf(F("net")) > 0) subJson = json_target::networks; + else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config; + else if (url.indexOf(F("pins")) > 0) subJson = json_target::pins; #ifdef WLED_ENABLE_JSONLIVE else if (url.indexOf("live") > 0) { serveLiveLeds(request); @@ -1098,46 +1246,48 @@ void serveJson(AsyncWebServerRequest* request) request->send_P(200, FPSTR(CONTENT_TYPE_JSON), JSON_palette_names); return; } - else if (url.indexOf(F("cfg")) > 0 && handleFileRead(request, F("/cfg.json"))) { - return; - } else if (url.length() > 6) { //not just /json serveJsonError(request, 501, ERR_NOT_IMPL); return; } - if (!requestJSONBufferLock(17)) { - serveJsonError(request, 503, ERR_NOBUF); + if (!requestJSONBufferLock(JSON_LOCK_SERVEJSON)) { + request->deferResponse(); return; } // releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer) // make sure you delete "response" if no "request->send(response);" is made - LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==JSON_PATH_FXDATA || subJson==JSON_PATH_EFFECTS); // will clear and convert JsonDocument into JsonArray if necessary + LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::fxdata || subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary JsonVariant lDoc = response->getRoot(); switch (subJson) { - case JSON_PATH_STATE: + case json_target::state: serializeState(lDoc); break; - case JSON_PATH_INFO: + case json_target::info: serializeInfo(lDoc); break; - case JSON_PATH_NODES: + case json_target::nodes: serializeNodes(lDoc); break; - case JSON_PATH_PALETTES: + case json_target::palettes: serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break; - case JSON_PATH_EFFECTS: + case json_target::effects: serializeModeNames(lDoc); break; - case JSON_PATH_FXDATA: + case json_target::fxdata: serializeModeData(lDoc); break; - case JSON_PATH_NETWORKS: + case json_target::networks: serializeNetworks(lDoc); break; - default: //all + case json_target::config: + serializeConfig(lDoc); break; + case json_target::pins: + serializePins(lDoc); break; + case json_target::state_info: + case json_target::all: JsonObject state = lDoc.createNestedObject("state"); serializeState(state); JsonObject info = lDoc.createNestedObject("info"); serializeInfo(info); - if (subJson != JSON_PATH_STATE_INFO) + if (subJson == json_target::all) { JsonArray effects = lDoc.createNestedArray(F("effects")); serializeModeNames(effects); // remove WLED-SR extensions from effect names diff --git a/wled00/led.cpp b/wled00/led.cpp index 9b97091e64..35f5003679 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -4,15 +4,13 @@ * LED methods */ -void setValuesFromMainSeg() { setValuesFromSegment(strip.getMainSegmentId()); } -void setValuesFromFirstSelectedSeg() { setValuesFromSegment(strip.getFirstSelectedSegId()); } -void setValuesFromSegment(uint8_t s) -{ - Segment& seg = strip.getSegment(s); - col[0] = R(seg.colors[0]); - col[1] = G(seg.colors[0]); - col[2] = B(seg.colors[0]); - col[3] = W(seg.colors[0]); + // applies chosen setment properties to legacy values +void setValuesFromSegment(uint8_t s) { + const Segment& seg = strip.getSegment(s); + colPri[0] = R(seg.colors[0]); + colPri[1] = G(seg.colors[0]); + colPri[2] = B(seg.colors[0]); + colPri[3] = W(seg.colors[0]); colSec[0] = R(seg.colors[1]); colSec[1] = G(seg.colors[1]); colSec[2] = B(seg.colors[1]); @@ -24,25 +22,19 @@ void setValuesFromSegment(uint8_t s) } -// applies global legacy values (col, colSec, effectCurrent...) -// problem: if the first selected segment already has the value to be set, other selected segments are not updated -void applyValuesToSelectedSegs() -{ - // copy of first selected segment to tell if value was updated - unsigned firstSel = strip.getFirstSelectedSegId(); - Segment selsegPrev = strip.getSegment(firstSel); +// applies global legacy values (colPri, colSec, effectCurrent...) to each selected segment +void applyValuesToSelectedSegs() { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); - if (i != firstSel && (!seg.isActive() || !seg.isSelected())) continue; - - if (effectSpeed != selsegPrev.speed) {seg.speed = effectSpeed; stateChanged = true;} - if (effectIntensity != selsegPrev.intensity) {seg.intensity = effectIntensity; stateChanged = true;} - if (effectPalette != selsegPrev.palette) {seg.setPalette(effectPalette);} - if (effectCurrent != selsegPrev.mode) {seg.setMode(effectCurrent);} - uint32_t col0 = RGBW32( col[0], col[1], col[2], col[3]); + if (!(seg.isActive() && seg.isSelected())) continue; + if (effectSpeed != seg.speed) {seg.speed = effectSpeed; stateChanged = true;} + if (effectIntensity != seg.intensity) {seg.intensity = effectIntensity; stateChanged = true;} + if (effectPalette != seg.palette) {seg.setPalette(effectPalette);} + if (effectCurrent != seg.mode) {seg.setMode(effectCurrent);} + uint32_t col0 = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); uint32_t col1 = RGBW32(colSec[0], colSec[1], colSec[2], colSec[3]); - if (col0 != selsegPrev.colors[0]) {seg.setColor(0, col0);} - if (col1 != selsegPrev.colors[1]) {seg.setColor(1, col1);} + if (col0 != seg.colors[0]) {seg.setColor(0, col0);} + if (col1 != seg.colors[1]) {seg.setColor(1, col1);} } } @@ -65,17 +57,18 @@ void toggleOnOff() //scales the brightness with the briMultiplier factor byte scaledBri(byte in) { - unsigned val = ((uint16_t)in*briMultiplier)/100; + unsigned val = ((unsigned)in*briMultiplier)/100; if (val > 255) val = 255; return (byte)val; } -//applies global brightness +//applies global temporary brightness (briT) to strip void applyBri() { - if (!realtimeMode || !arlsForceMaxBri) + if (realtimeOverride || !(realtimeMode && arlsForceMaxBri)) { - strip.setBrightness(scaledBri(briT)); + //DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); + strip.setBrightness(briT); } } @@ -85,6 +78,7 @@ void applyFinalBri() { briOld = bri; briT = bri; applyBri(); + strip.trigger(); // force one last update } @@ -93,7 +87,7 @@ void applyFinalBri() { void stateUpdated(byte callMode) { //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 11: ws send only 12: button preset - setValuesFromFirstSelectedSeg(); + setValuesFromFirstSelectedSeg(); // a much better approach would be to use main segment: setValuesFromMainSeg() if (bri != briOld || stateChanged) { if (stateChanged) currentPreset = 0; //something changed, so we are no longer in the preset @@ -103,7 +97,6 @@ void stateUpdated(byte callMode) { //set flag to update ws and mqtt interfaceUpdateCallMode = callMode; - stateChanged = false; } else { if (nightlightActive && !nightlightActiveOld && callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) { notify(CALL_MODE_NIGHTLIGHT); @@ -111,10 +104,11 @@ void stateUpdated(byte callMode) { } } + unsigned long now = millis(); if (callMode != CALL_MODE_NO_NOTIFY && nightlightActive && (nightlightMode == NL_MODE_FADE || nightlightMode == NL_MODE_COLORFADE)) { briNlT = bri; - nightlightDelayMs -= (millis() - nightlightStartTime); - nightlightStartTime = millis(); + nightlightDelayMs -= (now - nightlightStartTime); + nightlightStartTime = now; } if (briT == 0) { if (callMode != CALL_MODE_NOTIFICATION) strip.resetTimebase(); //effect start from beginning @@ -128,43 +122,36 @@ void stateUpdated(byte callMode) { // notify usermods of state change UsermodManager::onStateChange(callMode); - if (fadeTransition) { - if (strip.getTransition() == 0) { - jsonTransitionOnce = false; - transitionActive = false; - applyFinalBri(); - strip.trigger(); - return; - } - + if (strip.getTransition() == 0) { + jsonTransitionOnce = false; + transitionActive = false; + applyFinalBri(); + strip.trigger(); + } else { if (transitionActive) { briOld = briT; - tperLast = 0; - } else + } else if (bri != briOld || stateChanged) strip.setTransitionMode(true); // force all segments to transition mode transitionActive = true; - transitionStartTime = millis(); - } else { - applyFinalBri(); - strip.trigger(); + transitionStartTime = now; } + stateChanged = false; } -void updateInterfaces(uint8_t callMode) -{ +void updateInterfaces(uint8_t callMode) { if (!interfaceUpdateCallMode || millis() - lastInterfaceUpdate < INTERFACE_UPDATE_COOLDOWN) return; sendDataWs(); lastInterfaceUpdate = millis(); - interfaceUpdateCallMode = 0; //disable further updates + interfaceUpdateCallMode = CALL_MODE_INIT; //disable further updates if (callMode == CALL_MODE_WS_SEND) return; #ifndef WLED_DISABLE_ALEXA if (espalexaDevice != nullptr && callMode != CALL_MODE_ALEXA) { espalexaDevice->setValue(bri); - espalexaDevice->setColor(col[0], col[1], col[2]); + espalexaDevice->setColor(colPri[0], colPri[1], colPri[2]); } #endif #ifndef WLED_DISABLE_MQTT @@ -173,28 +160,26 @@ void updateInterfaces(uint8_t callMode) } -void handleTransitions() -{ +void handleTransitions() { //handle still pending interface update updateInterfaces(interfaceUpdateCallMode); if (transitionActive && strip.getTransition() > 0) { - float tper = (millis() - transitionStartTime)/(float)strip.getTransition(); - if (tper >= 1.0f) { + int ti = millis() - transitionStartTime; + int tr = strip.getTransition(); + if (ti/tr) { strip.setTransitionMode(false); // stop all transitions // restore (global) transition time if not called from UDP notifier or single/temporary transition from JSON (also playlist) if (jsonTransitionOnce) strip.setTransition(transitionDelay); transitionActive = false; jsonTransitionOnce = false; - tperLast = 0; applyFinalBri(); return; } - if (tper - tperLast < 0.004f) return; // less than 1 bit change (1/255) - tperLast = tper; - briT = briOld + ((bri - briOld) * tper); - - applyBri(); + byte briTO = briT; + int deltaBri = (int)bri - (int)briOld; + briT = briOld + (deltaBri * ti / tr); + if (briTO != briT) applyBri(); } } @@ -206,8 +191,7 @@ void colorUpdated(byte callMode) { } -void handleNightlight() -{ +void handleNightlight() { unsigned long now = millis(); if (now < 100 && lastNlUpdate > 0) lastNlUpdate = 0; // take care of millis() rollover if (now - lastNlUpdate < 100) return; // allow only 10 NL updates per second @@ -221,7 +205,7 @@ void handleNightlight() nightlightDelayMs = (unsigned)(nightlightDelayMins*60000); nightlightActiveOld = true; briNlT = bri; - for (unsigned i=0; i<4; i++) colNlT[i] = col[i]; // remember starting color + for (unsigned i=0; i<4; i++) colNlT[i] = colPri[i]; // remember starting color if (nightlightMode == NL_MODE_SUN) { //save current @@ -229,8 +213,8 @@ void handleNightlight() colNlT[1] = effectSpeed; colNlT[2] = effectPalette; - strip.setMode(strip.getFirstSelectedSegId(), FX_MODE_STATIC); // make sure seg runtime is reset if it was in sunrise mode - effectCurrent = FX_MODE_SUNRISE; + strip.getFirstSelectedSeg().setMode(FX_MODE_STATIC); // make sure seg runtime is reset if it was in sunrise mode + effectCurrent = FX_MODE_SUNRISE; // colorUpdated() will take care of assigning that to all selected segments effectSpeed = nightlightDelayMins; effectPalette = 0; if (effectSpeed > 60) effectSpeed = 60; //currently limited to 60 minutes @@ -246,7 +230,7 @@ void handleNightlight() bri = briNlT + ((nightlightTargetBri - briNlT)*nper); if (nightlightMode == NL_MODE_COLORFADE) // color fading only is enabled with "NF=2" { - for (unsigned i=0; i<4; i++) col[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color + for (unsigned i=0; i<4; i++) colPri[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color } colorUpdated(CALL_MODE_NO_NOTIFY); } @@ -287,7 +271,6 @@ void handleNightlight() } //utility for FastLED to use our custom timer -uint32_t get_millisecond_timer() -{ +uint32_t get_millisecond_timer() { return strip.now; } diff --git a/wled00/lx_parser.cpp b/wled00/lx_parser.cpp index 7fd91ea029..1318686ceb 100644 --- a/wled00/lx_parser.cpp +++ b/wled00/lx_parser.cpp @@ -22,7 +22,7 @@ bool parseLx(int lxValue, byte* rgbw) } else if ((lxValue >= 200000000) && (lxValue <= 201006500)) { // Loxone Lumitech ok = true; - float tmpBri = floor((lxValue - 200000000) / 10000); ; + float tmpBri = floor((lxValue - 200000000) / 10000); uint16_t ct = (lxValue - 200000000) - (((uint8_t)tmpBri) * 10000); tmpBri *= 2.55f; diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index a476db87a7..d64dc29d9b 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -7,6 +7,15 @@ #ifndef WLED_DISABLE_MQTT #define MQTT_KEEP_ALIVE_TIME 60 // contact the MQTT broker every 60 seconds +#if MQTT_MAX_TOPIC_LEN > 32 +#warning "MQTT topics length > 32 is not recommended for compatibility with usermods!" +#endif + +static const char* sTopicFormat PROGMEM = "%.*s/%s"; + +// parse payload for brightness, ON/OFF or toggle +// briLast is used to remember last brightness value in case of ON/OFF or toggle +// bri is set to 0 if payload is "0" or "OFF" or "false" static void parseMQTTBriPayload(char* payload) { if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);} @@ -23,31 +32,33 @@ static void parseMQTTBriPayload(char* payload) static void onMqttConnect(bool sessionPresent) { //(re)subscribe to required topics - char subuf[38]; + char subuf[MQTT_MAX_TOPIC_LEN + 9]; if (mqttDeviceTopic[0] != 0) { - strlcpy(subuf, mqttDeviceTopic, 33); - mqtt->subscribe(subuf, 0); - strcat_P(subuf, PSTR("/col")); + mqtt->subscribe(mqttDeviceTopic, 0); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "col"); mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/api")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "api"); mqtt->subscribe(subuf, 0); } if (mqttGroupTopic[0] != 0) { - strlcpy(subuf, mqttGroupTopic, 33); + mqtt->subscribe(mqttGroupTopic, 0); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttGroupTopic, "col"); mqtt->subscribe(subuf, 0); - strcat_P(subuf, PSTR("/col")); - mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttGroupTopic, 33); - strcat_P(subuf, PSTR("/api")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttGroupTopic, "api"); mqtt->subscribe(subuf, 0); } UsermodManager::onMqttConnect(sessionPresent); DEBUG_PRINTLN(F("MQTT ready")); + +#ifndef USERMOD_SMARTNEST + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "status"); + mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT +#endif + publishMqtt(); } @@ -64,8 +75,8 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } if (index == 0) { // start (1st partial packet or the only packet) - if (payloadStr) delete[] payloadStr; // fail-safe: release buffer - payloadStr = new char[total+1]; // allocate new buffer + p_free(payloadStr); // release buffer if it exists + payloadStr = static_cast(p_malloc(total+1)); // allocate new buffer } if (payloadStr == nullptr) return; // buffer not allocated @@ -90,7 +101,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } else { // Non-Wled Topic used here. Probably a usermod subscribed to this topic. UsermodManager::onMqttMessage(topic, payloadStr); - delete[] payloadStr; + p_free(payloadStr); payloadStr = nullptr; return; } @@ -99,10 +110,10 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp //Prefix is stripped from the topic at this point if (strcmp_P(topic, PSTR("/col")) == 0) { - colorFromDecOrHexString(col, payloadStr); + colorFromDecOrHexString(colPri, payloadStr); colorUpdated(CALL_MODE_DIRECT_CHANGE); } else if (strcmp_P(topic, PSTR("/api")) == 0) { - if (requestJSONBufferLock(15)) { + if (requestJSONBufferLock(JSON_LOCK_MQTT)) { if (payloadStr[0] == '{') { //JSON API deserializeJson(*pDoc, payloadStr); deserializeState(pDoc->as()); @@ -120,12 +131,12 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp // topmost topic (just wled/MAC) parseMQTTBriPayload(payloadStr); } - delete[] payloadStr; + p_free(payloadStr); payloadStr = nullptr; } // Print adapter for flat buffers -namespace { +namespace { class bufferPrint : public Print { char* _buf; size_t _size, _offset; @@ -158,28 +169,24 @@ void publishMqtt() #ifndef USERMOD_SMARTNEST char s[10]; - char subuf[48]; + char subuf[MQTT_MAX_TOPIC_LEN + 16]; sprintf_P(s, PSTR("%u"), bri); - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/g")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "g"); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) - sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/c")); + sprintf_P(s, PSTR("#%06X"), (colPri[3] << 24) | (colPri[0] << 16) | (colPri[1] << 8) | (colPri[2])); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "c"); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/status")); - mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "status"); + mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT // TODO: use a DynamicBufferList. Requires a list-read-capable MQTT client API. DynamicBuffer buf(1024); bufferPrint pbuf(buf.data(), buf.size()); XML_response(pbuf); - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/v")); + snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "v"); mqtt->publish(subuf, 0, retainMqttMsg, buf.data(), pbuf.size()); // optionally retain message (#2263) #endif } @@ -192,7 +199,8 @@ bool initMqtt() if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false; if (mqtt == nullptr) { - mqtt = new AsyncMqttClient(); + void *ptr = p_malloc(sizeof(AsyncMqttClient)); + mqtt = new (ptr) AsyncMqttClient(); // use placement new (into PSRAM), client will never be deleted if (!mqtt) return false; mqtt->onMessage(onMqttMessage); mqtt->onConnect(onMqttConnect); @@ -205,14 +213,26 @@ bool initMqtt() { mqtt->setServer(mqttIP, mqttPort); } else { - mqtt->setServer(mqttServer, mqttPort); + #ifdef ARDUINO_ARCH_ESP32 + String mqttMDNS = mqttServer; + mqttMDNS.toLowerCase(); // make sure we have a lowercase hostname + int pos = mqttMDNS.indexOf(F(".local")); + if (pos > 0) mqttMDNS.remove(pos); // remove .local domain if present (and anything following it) + if (strlen(cmDNS) > 0 && mqttMDNS.length() > 0 && mqttMDNS.indexOf('.') < 0) { // if mDNS is enabled and server does not have domain + mqttIP = MDNS.queryHost(mqttMDNS.c_str()); + if (mqttIP != IPAddress()) // if MDNS resolved the hostname + mqtt->setServer(mqttIP, mqttPort); + else + mqtt->setServer(mqttServer, mqttPort); + } else + #endif + mqtt->setServer(mqttServer, mqttPort); } mqtt->setClientId(mqttClientID); if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); #ifndef USERMOD_SMARTNEST - strlcpy(mqttStatusTopic, mqttDeviceTopic, 33); - strcat_P(mqttStatusTopic, PSTR("/status")); + snprintf_P(mqttStatusTopic, sizeof(mqttStatusTopic)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "status"); mqtt->setWill(mqttStatusTopic, 0, true, "offline"); // LWT message #endif mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME); diff --git a/wled00/network.cpp b/wled00/network.cpp index 06860cf4de..105fdf6b27 100644 --- a/wled00/network.cpp +++ b/wled00/network.cpp @@ -3,7 +3,7 @@ #include "wled_ethernet.h" -#ifdef WLED_USE_ETHERNET +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) // The following six pins are neither configurable nor // can they be re-assigned through IOMUX / GPIO matrix. // See https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-ethernet-kit-v1.1.html#ip101gri-phy-interface @@ -144,8 +144,120 @@ const ethernet_settings ethernetBoards[] = { 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO0_OUT // eth_clk_mode - } + }, + + // Gledopto Series With Ethernet + { + 1, // eth_address, + 5, // eth_power, + 23, // eth_mdc, + 33, // eth_mdio, + ETH_PHY_LAN8720, // eth_type, + ETH_CLOCK_GPIO0_IN // eth_clk_mode + }, }; + +bool initEthernet() +{ + static bool successfullyConfiguredEthernet = false; + + if (successfullyConfiguredEthernet) { + // DEBUG_PRINTLN(F("initE: ETH already successfully configured, ignoring")); + return false; + } + if (ethernetType == WLED_ETH_NONE) { + return false; + } + if (ethernetType >= WLED_NUM_ETH_TYPES) { + DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); + return false; + } + + DEBUG_PRINTF_P(PSTR("initE: Attempting ETH config: %d\n"), ethernetType); + + // Ethernet initialization should only succeed once -- else reboot required + ethernet_settings es = ethernetBoards[ethernetType]; + managed_pin_type pinsToAllocate[10] = { + // first six pins are non-configurable + esp32_nonconfigurable_ethernet_pins[0], + esp32_nonconfigurable_ethernet_pins[1], + esp32_nonconfigurable_ethernet_pins[2], + esp32_nonconfigurable_ethernet_pins[3], + esp32_nonconfigurable_ethernet_pins[4], + esp32_nonconfigurable_ethernet_pins[5], + { (int8_t)es.eth_mdc, true }, // [6] = MDC is output and mandatory + { (int8_t)es.eth_mdio, true }, // [7] = MDIO is bidirectional and mandatory + { (int8_t)es.eth_power, true }, // [8] = optional pin, not all boards use + { ((int8_t)0xFE), false }, // [9] = replaced with eth_clk_mode, mandatory + }; + // update the clock pin.... + if (es.eth_clk_mode == ETH_CLOCK_GPIO0_IN) { + pinsToAllocate[9].pin = 0; + pinsToAllocate[9].isOutput = false; + } else if (es.eth_clk_mode == ETH_CLOCK_GPIO0_OUT) { + pinsToAllocate[9].pin = 0; + pinsToAllocate[9].isOutput = true; + } else if (es.eth_clk_mode == ETH_CLOCK_GPIO16_OUT) { + pinsToAllocate[9].pin = 16; + pinsToAllocate[9].isOutput = true; + } else if (es.eth_clk_mode == ETH_CLOCK_GPIO17_OUT) { + pinsToAllocate[9].pin = 17; + pinsToAllocate[9].isOutput = true; + } else { + DEBUG_PRINTF_P(PSTR("initE: Failing due to invalid eth_clk_mode (%d)\n"), es.eth_clk_mode); + return false; + } + + if (!PinManager::allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { + DEBUG_PRINTLN(F("initE: Failed to allocate ethernet pins")); + return false; + } + + /* + For LAN8720 the most correct way is to perform clean reset each time before init + applying LOW to power or nRST pin for at least 100 us (please refer to datasheet, page 59) + ESP_IDF > V4 implements it (150 us, lan87xx_reset_hw(esp_eth_phy_t *phy) function in + /components/esp_eth/src/esp_eth_phy_lan87xx.c, line 280) + but ESP_IDF < V4 does not. Lets do it: + [not always needed, might be relevant in some EMI situations at startup and for hot resets] + */ + #if ESP_IDF_VERSION_MAJOR==3 + if(es.eth_power>0 && es.eth_type==ETH_PHY_LAN8720) { + pinMode(es.eth_power, OUTPUT); + digitalWrite(es.eth_power, 0); + delayMicroseconds(150); + digitalWrite(es.eth_power, 1); + delayMicroseconds(10); + } + #endif + + if (!ETH.begin( + (uint8_t) es.eth_address, + (int) es.eth_power, + (int) es.eth_mdc, + (int) es.eth_mdio, + (eth_phy_type_t) es.eth_type, + (eth_clock_mode_t) es.eth_clk_mode + )) { + DEBUG_PRINTLN(F("initE: ETH.begin() failed")); + // de-allocate the allocated pins + for (managed_pin_type mpt : pinsToAllocate) { + PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet); + } + return false; + } + + // https://github.com/wled/WLED/issues/5247 + if (multiWiFi[0].staticIP != (uint32_t)0x00000000 && multiWiFi[0].staticGW != (uint32_t)0x00000000) { + ETH.config(multiWiFi[0].staticIP, multiWiFi[0].staticGW, multiWiFi[0].staticSN, dnsAddress); + } else { + ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); + } + + successfullyConfiguredEthernet = true; + DEBUG_PRINTLN(F("initE: *** Ethernet successfully configured! ***")); + return true; +} #endif @@ -170,24 +282,136 @@ int getSignalQuality(int rssi) } +void fillMAC2Str(char *str, const uint8_t *mac) { + sprintf_P(str, PSTR("%02x%02x%02x%02x%02x%02x"), MAC2STR(mac)); + byte nul = 0; + for (int i = 0; i < 6; i++) nul |= *mac++; // do we have 0 + if (!nul) str[0] = '\0'; // empty string +} + +void fillStr2MAC(uint8_t *mac, const char *str) { + for (int i = 0; i < 6; i++) *mac++ = 0; // clear + if (!str) return; // null string + uint64_t MAC = strtoull(str, nullptr, 16); + for (int i = 0; i < 6; i++) { *--mac = MAC & 0xFF; MAC >>= 8; } +} + + +// performs asynchronous scan for available networks (which may take couple of seconds to finish) +// returns configured WiFi ID with the strongest signal (or default if no configured networks available) +int findWiFi(bool doScan) { + if (multiWiFi.size() <= 1) { + DEBUG_PRINTF_P(PSTR("WiFi: Default SSID (%s) used.\n"), multiWiFi[0].clientSSID); + return 0; + } + + int status = WiFi.scanComplete(); // complete scan may take as much as several seconds (usually <6s with not very crowded air) + + if (doScan || status == WIFI_SCAN_FAILED) { + DEBUG_PRINTF_P(PSTR("WiFi: Scan started. @ %lus\n"), millis()/1000); + WiFi.scanNetworks(true); // start scanning in asynchronous mode (will delete old scan) + } else if (status >= 0) { // status contains number of found networks (including duplicate SSIDs with different BSSID) + DEBUG_PRINTF_P(PSTR("WiFi: Found %d SSIDs. @ %lus\n"), status, millis()/1000); + int rssi = -9999; + int selected = selectedWiFi; + for (int o = 0; o < status; o++) { + DEBUG_PRINTF_P(PSTR(" SSID: %s (BSSID: %s) RSSI: %ddB\n"), WiFi.SSID(o).c_str(), WiFi.BSSIDstr(o).c_str(), WiFi.RSSI(o)); + for (unsigned n = 0; n < multiWiFi.size(); n++) + if (!strcmp(WiFi.SSID(o).c_str(), multiWiFi[n].clientSSID)) { + bool foundBSSID = memcmp(multiWiFi[n].bssid, WiFi.BSSID(o), 6) == 0; + // find the WiFi with the strongest signal (but keep priority of entry if signal difference is not big) + if (foundBSSID || (n < selected && WiFi.RSSI(o) > rssi-10) || WiFi.RSSI(o) > rssi) { + rssi = foundBSSID ? 0 : WiFi.RSSI(o); // RSSI is only ever negative + selected = n; + } + break; + } + } + DEBUG_PRINTF_P(PSTR("WiFi: Selected SSID: %s RSSI: %ddB\n"), multiWiFi[selected].clientSSID, rssi); + return selected; + } + //DEBUG_PRINT(F("WiFi scan running.")); + return status; // scan is still running or there was an error +} + + +bool isWiFiConfigured() { + return multiWiFi.size() > 1 || (strlen(multiWiFi[0].clientSSID) >= 1 && strcmp_P(multiWiFi[0].clientSSID, PSTR(DEFAULT_CLIENT_SSID)) != 0); +} + +#if defined(ESP8266) + #define ARDUINO_EVENT_WIFI_AP_STADISCONNECTED WIFI_EVENT_SOFTAPMODE_STADISCONNECTED + #define ARDUINO_EVENT_WIFI_AP_STACONNECTED WIFI_EVENT_SOFTAPMODE_STACONNECTED + #define ARDUINO_EVENT_WIFI_STA_GOT_IP WIFI_EVENT_STAMODE_GOT_IP + #define ARDUINO_EVENT_WIFI_STA_CONNECTED WIFI_EVENT_STAMODE_CONNECTED + #define ARDUINO_EVENT_WIFI_STA_DISCONNECTED WIFI_EVENT_STAMODE_DISCONNECTED +#elif defined(ARDUINO_ARCH_ESP32) && !defined(ESP_ARDUINO_VERSION_MAJOR) //ESP_IDF_VERSION_MAJOR==3 + // not strictly IDF v3 but Arduino core related + #define ARDUINO_EVENT_WIFI_AP_STADISCONNECTED SYSTEM_EVENT_AP_STADISCONNECTED + #define ARDUINO_EVENT_WIFI_AP_STACONNECTED SYSTEM_EVENT_AP_STACONNECTED + #define ARDUINO_EVENT_WIFI_STA_GOT_IP SYSTEM_EVENT_STA_GOT_IP + #define ARDUINO_EVENT_WIFI_STA_CONNECTED SYSTEM_EVENT_STA_CONNECTED + #define ARDUINO_EVENT_WIFI_STA_DISCONNECTED SYSTEM_EVENT_STA_DISCONNECTED + #define ARDUINO_EVENT_WIFI_AP_START SYSTEM_EVENT_AP_START + #define ARDUINO_EVENT_WIFI_AP_STOP SYSTEM_EVENT_AP_STOP + #define ARDUINO_EVENT_WIFI_SCAN_DONE SYSTEM_EVENT_SCAN_DONE + #define ARDUINO_EVENT_ETH_START SYSTEM_EVENT_ETH_START + #define ARDUINO_EVENT_ETH_CONNECTED SYSTEM_EVENT_ETH_CONNECTED + #define ARDUINO_EVENT_ETH_DISCONNECTED SYSTEM_EVENT_ETH_DISCONNECTED +#endif + //handle Ethernet connection event void WiFiEvent(WiFiEvent_t event) { switch (event) { -#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - case SYSTEM_EVENT_ETH_START: - DEBUG_PRINTLN(F("ETH Started")); + case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: + // AP client disconnected + if (--apClients == 0 && isWiFiConfigured()) forceReconnect = true; // no clients reconnect WiFi if awailable + DEBUG_PRINTF_P(PSTR("WiFi-E: AP Client Disconnected (%d) @ %lus.\n"), (int)apClients, millis()/1000); + break; + case ARDUINO_EVENT_WIFI_AP_STACONNECTED: + // AP client connected + apClients++; + DEBUG_PRINTF_P(PSTR("WiFi-E: AP Client Connected (%d) @ %lus.\n"), (int)apClients, millis()/1000); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + DEBUG_PRINT(F("WiFi-E: IP address: ")); DEBUG_PRINTLN(Network.localIP()); + break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + // followed by IDLE and SCAN_DONE + DEBUG_PRINTF_P(PSTR("WiFi-E: Connected! @ %lus\n"), millis()/1000); + wasConnected = true; + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + if (wasConnected && interfacesInited) { + DEBUG_PRINTF_P(PSTR("WiFi-E: Disconnected! @ %lus\n"), millis()/1000); + if (interfacesInited && multiWiFi.size() > 1 && WiFi.scanComplete() >= 0) { + findWiFi(true); // reinit WiFi scan + forceReconnect = true; + } + interfacesInited = false; + } + break; + #ifdef ARDUINO_ARCH_ESP32 + case ARDUINO_EVENT_WIFI_SCAN_DONE: + // also triggered when connected to selected SSID + DEBUG_PRINTLN(F("WiFi-E: SSID scan completed.")); + break; + case ARDUINO_EVENT_WIFI_AP_START: + DEBUG_PRINTLN(F("WiFi-E: AP Started")); + break; + case ARDUINO_EVENT_WIFI_AP_STOP: + DEBUG_PRINTLN(F("WiFi-E: AP Stopped")); break; - case SYSTEM_EVENT_ETH_CONNECTED: + #if defined(WLED_USE_ETHERNET) + case ARDUINO_EVENT_ETH_START: + DEBUG_PRINTLN(F("ETH-E: Started")); + break; + case ARDUINO_EVENT_ETH_CONNECTED: { - DEBUG_PRINTLN(F("ETH Connected")); + DEBUG_PRINTLN(F("ETH-E: Connected")); if (!apActive) { - WiFi.disconnect(true); - } - if (multiWiFi[0].staticIP != (uint32_t)0x00000000 && multiWiFi[0].staticGW != (uint32_t)0x00000000) { - ETH.config(multiWiFi[0].staticIP, multiWiFi[0].staticGW, multiWiFi[0].staticSN, dnsAddress); - } else { - ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); + WiFi.disconnect(true); // disable WiFi entirely } // convert the "serverDescription" into a valid DNS hostname (alphanumeric) char hostname[64]; @@ -196,17 +420,20 @@ void WiFiEvent(WiFiEvent_t event) showWelcomePage = false; break; } - case SYSTEM_EVENT_ETH_DISCONNECTED: - DEBUG_PRINTLN(F("ETH Disconnected")); + case ARDUINO_EVENT_ETH_DISCONNECTED: + DEBUG_PRINTLN(F("ETH-E: Disconnected")); // This doesn't really affect ethernet per se, // as it's only configured once. Rather, it // may be necessary to reconnect the WiFi when // ethernet disconnects, as a way to provide // alternative access to the device. + if (interfacesInited && WiFi.scanComplete() >= 0) findWiFi(true); // reinit WiFi scan forceReconnect = true; break; -#endif + #endif + #endif default: + DEBUG_PRINTF_P(PSTR("WiFi-E: Event %d\n"), (int)event); break; } } diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index 8d44e634ec..8244961d75 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -2,6 +2,11 @@ #include "wled.h" #include "fcn_declare.h" +// forward declarations +static void sendNTPPacket(); +static bool checkNTPResponse(); + + // WARNING: may cause errors in sunset calculations on ESP8266, see #3400 // building with `-D WLED_USE_REAL_MATH` will prevent those errors at the expense of flash and RAM @@ -11,7 +16,7 @@ //#define WLED_DEBUG_NTP #define NTP_SYNC_INTERVAL 42000UL //Get fresh NTP time about twice per day -Timezone* tz; +static Timezone* tz; #define TZ_UTC 0 #define TZ_UK 1 @@ -37,11 +42,12 @@ Timezone* tz; #define TZ_MX_CENTRAL 21 #define TZ_PAKISTAN 22 #define TZ_BRASILIA 23 +#define TZ_AUSTRALIA_WESTERN 24 -#define TZ_COUNT 24 +#define TZ_COUNT 25 #define TZ_INIT 255 -byte tzCurrent = TZ_INIT; //uninitialized +static byte tzCurrent = TZ_INIT; //uninitialized /* C++11 form -- static std::array, TZ_COUNT> TZ_TABLE PROGMEM = {{ */ static const std::pair TZ_TABLE[] PROGMEM = { @@ -140,6 +146,10 @@ static const std::pair TZ_TABLE[] PROGMEM = { /* TZ_BRASILIA */ { {Last, Sun, Mar, 1, -180}, //Brasília Standard Time = UTC - 3 hours {Last, Sun, Mar, 1, -180} + }, + /* TZ_AUSTRALIA_WESTERN */ { + {Last, Sun, Mar, 1, 480}, //AWST = UTC + 8 hours + {Last, Sun, Mar, 1, 480} //AWST = UTC + 8 hours (no DST) } }; @@ -194,7 +204,7 @@ void handleNetworkTime() } } -void sendNTPPacket() +static void sendNTPPacket() { if (!ntpServerIP.fromString(ntpServerName)) //see if server is IP or domain { @@ -224,7 +234,7 @@ void sendNTPPacket() ntpUdp.endPacket(); } -static bool isValidNtpResponse(byte * ntpPacket) { +static bool isValidNtpResponse(const byte* ntpPacket) { // Perform a few validity checks on the packet // based on https://github.com/taranais/NTPClient/blob/master/NTPClient.cpp if((ntpPacket[0] & 0b11000000) == 0b11000000) return false; //reject LI=UNSYNC @@ -240,7 +250,7 @@ static bool isValidNtpResponse(byte * ntpPacket) { return true; } -bool checkNTPResponse() +static bool checkNTPResponse() { int cb = ntpUdp.parsePacket(); if (cb < NTP_MIN_PACKET_SIZE) { diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp new file mode 100644 index 0000000000..b38df08ed2 --- /dev/null +++ b/wled00/ota_update.cpp @@ -0,0 +1,773 @@ +#include "ota_update.h" +#include "wled.h" + +#ifdef ESP32 +#include +#include +#include +#include +#endif + +// Platform-specific metadata locations +#ifdef ESP32 +constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata +#define UPDATE_ERROR errorString + +// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB +// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064 +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) +constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6 +constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size +#define BOOTLOADER_OTA_UNSUPPORTED // still needs validation on these platforms. +#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5) +constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5 +constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size +#define BOOTLOADER_OTA_UNSUPPORTED // still needs testing on these platforms +#else +constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2 +constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size +#endif + +#elif defined(ESP8266) +constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset +#define UPDATE_ERROR getErrorString +#endif + +constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes + + +/** + * Check if OTA should be allowed based on release compatibility using custom description + * @param binaryData Pointer to binary file data (not modified) + * @param dataSize Size of binary data in bytes + * @param errorMessage Buffer to store error message if validation fails + * @param errorMessageLen Maximum length of error message buffer + * @return true if OTA should proceed, false if it should be blocked + */ + +static bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) { + // Clear error message + if (errorMessage && errorMessageLen > 0) { + errorMessage[0] = '\0'; + } + + // Try to extract WLED structure directly from binary data + wled_metadata_t extractedDesc; + bool hasDesc = findWledMetadata(binaryData, dataSize, &extractedDesc); + + if (hasDesc) { + return shouldAllowOTA(extractedDesc, errorMessage, errorMessageLen); + } else { + // No custom description - this could be a legacy binary + if (errorMessage && errorMessageLen > 0) { + strncpy_P(errorMessage, PSTR("This firmware file is missing compatibility metadata."), errorMessageLen - 1); + errorMessage[errorMessageLen - 1] = '\0'; + } + return false; + } +} + +struct UpdateContext { + // State flags + // FUTURE: the flags could be replaced by a state machine + bool replySent = false; + bool needsRestart = false; + bool updateStarted = false; + bool uploadComplete = false; + bool releaseCheckPassed = false; + String errorMessage; + + // Buffer to hold block data across posts, if needed + std::vector releaseMetadataBuffer; +}; + + +static void endOTA(AsyncWebServerRequest *request) { + UpdateContext* context = reinterpret_cast(request->_tempObject); + request->_tempObject = nullptr; + + DEBUG_PRINTF_P(PSTR("EndOTA %x --> %x (%d)\n"), (uintptr_t)request,(uintptr_t) context, context ? context->uploadComplete : 0); + if (context) { + if (context->updateStarted) { // We initialized the update + // We use Update.end() because not all forms of Update() support an abort. + // If the upload is incomplete, Update.end(false) should error out. + if (Update.end(context->uploadComplete)) { + // Update successful! + #ifndef ESP8266 + bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update + #endif + doReboot = true; + context->needsRestart = false; + } + } + + if (context->needsRestart) { + strip.resume(); + UsermodManager::onUpdateBegin(false); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + } + delete context; + } +}; + +static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context) +{ + #ifdef ESP8266 + Update.runAsync(true); + #endif + + if (Update.isRunning()) { + request->send(503); + setOTAReplied(request); + return false; + } + + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().disableWatchdog(); + #endif + UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) + + strip.suspend(); + backupConfig(); // backup current config in case the update ends badly + strip.resetSegments(); // free as much memory as you can + context->needsRestart = true; + + DEBUG_PRINTF_P(PSTR("OTA Update Start, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context); + + auto skipValidationParam = request->getParam("skipValidation", true); + if (skipValidationParam && (skipValidationParam->value() == "1")) { + context->releaseCheckPassed = true; + DEBUG_PRINTLN(F("OTA validation skipped by user")); + } + + // Begin update with the firmware size from content length + size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); + if (!Update.begin(updateSize)) { + context->errorMessage = Update.UPDATE_ERROR(); + DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), context->errorMessage.c_str()); + return false; + } + + context->updateStarted = true; + return true; +} + +// Create an OTA context object on an AsyncWebServerRequest +// Returns true if successful, false on failure. +bool initOTA(AsyncWebServerRequest *request) { + // Allocate update context + UpdateContext* context = new (std::nothrow) UpdateContext {}; + if (context) { + request->_tempObject = context; + request->onDisconnect([=]() { endOTA(request); }); // ensures we restart on failure + }; + + DEBUG_PRINTF_P(PSTR("OTA Update init, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context); + return (context != nullptr); +} + +void setOTAReplied(AsyncWebServerRequest *request) { + UpdateContext* context = reinterpret_cast(request->_tempObject); + if (!context) return; + context->replySent = true; +}; + +// Returns pointer to error message, or nullptr if OTA was successful. +std::pair getOTAResult(AsyncWebServerRequest* request) { + UpdateContext* context = reinterpret_cast(request->_tempObject); + if (!context) return { true, F("OTA context unexpectedly missing") }; + if (context->replySent) return { false, {} }; + if (context->errorMessage.length()) return { true, context->errorMessage }; + + if (context->updateStarted) { + // Release the OTA context now. + endOTA(request); + if (Update.hasError()) { + return { true, Update.UPDATE_ERROR() }; + } else { + return { true, {} }; + } + } + + // Should never happen + return { true, F("Internal software failure") }; +} + + + +void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) +{ + UpdateContext* context = reinterpret_cast(request->_tempObject); + if (!context) return; + + //DEBUG_PRINTF_P(PSTR("HandleOTAData: %d %d %d\n"), index, len, isFinal); + + if (context->replySent || (context->errorMessage.length())) return; + + if (index == 0) { + if (!beginOTA(request, context)) return; + } + + // Perform validation if we haven't done it yet and we have reached the metadata offset + if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) { + // Current chunk contains the metadata offset + size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET; + + DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset); + + if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) { + // We have enough data to validate, one way or another + const uint8_t* search_data = data; + size_t search_len = len; + + // If we have saved data, use that instead + if (context->releaseMetadataBuffer.size()) { + // Add this data + context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len); + search_data = context->releaseMetadataBuffer.data(); + search_len = context->releaseMetadataBuffer.size(); + } + + // Do the checking + char errorMessage[128]; + bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage)); + + // Release buffer if there was one + context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){}; + + if (!OTA_ok) { + DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage); + context->errorMessage = errorMessage; + context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway."); + return; + } else { + DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed")); + context->releaseCheckPassed = true; + } + } else { + // Store the data we just got for next pass + context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len); + } + } + + // Check if validation was still pending (shouldn't happen normally) + // This is done before writing the last chunk, so endOTA can abort + if (isFinal && !context->releaseCheckPassed) { + DEBUG_PRINTLN(F("OTA failed: Validation never completed")); + // Don't write the last chunk to the updater: this will trip an error later + context->errorMessage = F("Release check data never arrived?"); + return; + } + + // Write chunk data to OTA update (only if release check passed or still pending) + if (!Update.hasError()) { + if (Update.write(data, len) != len) { + DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR()); + } + } + + if(isFinal) { + DEBUG_PRINTLN(F("OTA Update End")); + // Upload complete + context->uploadComplete = true; + } +} + +void markOTAvalid() { + #ifndef ESP8266 + const esp_partition_t* running = esp_ota_get_running_partition(); + esp_ota_img_states_t ota_state; + if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) { + if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) { + esp_ota_mark_app_valid_cancel_rollback(); // only needs to be called once, it marks the ota_state as ESP_OTA_IMG_VALID + DEBUG_PRINTLN(F("Current firmware validated")); + } + } + #endif +} + +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) + +// Class for computing the expected bootloader data size given a stream of the data. +// If the image includes an SHA256 appended after the data stream, we do not consider it here. +class BootloaderImageSizer { +public: + + bool feed(const uint8_t* data, size_t len) { + if (error) return false; + + //DEBUG_PRINTF("Feed %d\n", len); + + if (imageSize == 0) { + // Parse header first + if (len < sizeof(esp_image_header_t)) { + error = true; + return false; + } + + esp_image_header_t header; + memcpy(&header, data, sizeof(esp_image_header_t)); + + if (header.segment_count == 0) { + error = true; + return false; + } + + imageSize = sizeof(esp_image_header_t); + segmentsLeft = header.segment_count; + data += sizeof(esp_image_header_t); + len -= sizeof(esp_image_header_t); + //DEBUG_PRINTF("BLS parsed image header, segment count %d, is %d\n", segmentsLeft, imageSize); + } + + while (len && segmentsLeft) { + if (segmentHeaderBytes < sizeof(esp_image_segment_header_t)) { + size_t headerBytes = std::min(len, sizeof(esp_image_segment_header_t) - segmentHeaderBytes); + memcpy(reinterpret_cast(&segmentHeader) + segmentHeaderBytes, data, headerBytes); + segmentHeaderBytes += headerBytes; + if (segmentHeaderBytes < sizeof(esp_image_segment_header_t)) { + return true; // needs more bytes for the header + } + + //DEBUG_PRINTF("BLS parsed segment [%08X %08X=%d], segment count %d, is %d\n", segmentHeader.load_addr, segmentHeader.data_len, segmentHeader.data_len, segmentsLeft, imageSize); + + // Validate segment size + if (segmentHeader.data_len > BOOTLOADER_SIZE) { + error = true; + return false; + } + + data += headerBytes; + len -= headerBytes; + imageSize += sizeof(esp_image_segment_header_t) + segmentHeader.data_len; + --segmentsLeft; + if (segmentsLeft == 0) { + // all done, actually; we don't need to read any more + + // Round up to nearest 16 bytes. + // Always add 1 to account for the checksum byte. + imageSize = ((imageSize/ 16) + 1) * 16; + + //DEBUG_PRINTF("BLS complete, is %d\n", imageSize); + return false; + } + } + + // If we don't have enough bytes ... + if (len < segmentHeader.data_len) { + //DEBUG_PRINTF("Needs more bytes\n"); + segmentHeader.data_len -= len; + return true; // still in this segment + } + + // Segment complete + len -= segmentHeader.data_len; + data += segmentHeader.data_len; + segmentHeaderBytes = 0; + //DEBUG_PRINTF("Segment complete: len %d\n", len); + } + + return !error; + } + + bool hasError() const { return error; } + bool isSizeKnown() const { return !error && imageSize != 0 && segmentsLeft == 0; } + size_t totalSize() const { + if (!isSizeKnown()) return 0; + return imageSize; + } + +private: + size_t imageSize = 0; + size_t segmentsLeft = 0; + esp_image_segment_header_t segmentHeader; + size_t segmentHeaderBytes = 0; + bool error = false; +}; + +static bool bootloaderSHA256CacheValid = false; +static uint8_t bootloaderSHA256Cache[32]; + +/** + * Calculate and cache the bootloader SHA256 digest + * Reads the bootloader from flash and computes SHA256 hash + * + * Strictly speaking, most bootloader images already contain a hash at the end of the image; + * we could in theory just read it. The trouble is that we have to parse the structure anyways + * to find the actual endpoint, so we might as well always calculate it ourselves rather than + * handle a special case if the hash isn't stored. + * + */ +static void calculateBootloaderSHA256() { + // Calculate SHA256 + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224) + + const size_t chunkSize = 256; + alignas(esp_image_header_t) uint8_t buffer[chunkSize]; + size_t bootloaderSize = BOOTLOADER_SIZE; + BootloaderImageSizer sizer; + size_t totalHashLen = 0; + + for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) { + size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize); + if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { + sizer.feed(buffer, readSize); + + size_t hashLen = readSize; + if (sizer.isSizeKnown()) { + size_t totalSize = sizer.totalSize(); + if (totalSize > 0 && totalSize <= BOOTLOADER_SIZE) { + bootloaderSize = totalSize; + if (offset + readSize > totalSize) { + hashLen = (totalSize > offset) ? (totalSize - offset) : 0; + } + } + } + + if (hashLen > 0) { + totalHashLen += hashLen; + mbedtls_sha256_update(&ctx, buffer, hashLen); + } + } + } + + mbedtls_sha256_finish(&ctx, bootloaderSHA256Cache); + mbedtls_sha256_free(&ctx); + + bootloaderSHA256CacheValid = true; +} + +// Get bootloader SHA256 as hex string +String getBootloaderSHA256Hex() { + if (!bootloaderSHA256CacheValid) { + calculateBootloaderSHA256(); + } + + // Convert to hex string + String result; + result.reserve(65); + for (int i = 0; i < 32; i++) { + char b1 = bootloaderSHA256Cache[i]; + char b2 = b1 >> 4; + b1 &= 0x0F; + b1 += '0'; b2 += '0'; + if (b1 > '9') b1 += 39; + if (b2 > '9') b2 += 39; + result.concat(b2); + result.concat(b1); + } + return result; +} + +/** + * Invalidate cached bootloader SHA256 (call after bootloader update) + * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex + */ +static void invalidateBootloaderSHA256Cache() { + bootloaderSHA256CacheValid = false; +} + +/** + * Verify complete buffered bootloader using ESP-IDF validation approach + * This matches the key validation steps from esp_image_verify() in ESP-IDF + * @param buffer Reference to pointer to bootloader binary data (will be adjusted if offset detected) + * @param len Reference to length of bootloader data (will be adjusted to actual size) + * @param bootloaderErrorMsg Pointer to String to store error message (must not be null) + * @return true if validation passed, false otherwise + */ +static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& bootloaderErrorMsg) { + const size_t MIN_IMAGE_HEADER_SIZE = sizeof(esp_image_header_t); + + // 1. Validate minimum size for header + if (len < MIN_IMAGE_HEADER_SIZE) { + bootloaderErrorMsg = "Too small"; + return false; + } + + // Check if the bootloader starts at offset 0x1000 (common in partition table dumps) + // This happens when someone uploads a complete flash dump instead of just the bootloader + if (len > BOOTLOADER_OFFSET + MIN_IMAGE_HEADER_SIZE && + buffer[BOOTLOADER_OFFSET] == ESP_IMAGE_HEADER_MAGIC && + buffer[0] != ESP_IMAGE_HEADER_MAGIC) { + DEBUG_PRINTF_P(PSTR("Bootloader detected at offset\n")); + // Adjust buffer pointer to start at the actual bootloader + buffer = buffer + BOOTLOADER_OFFSET; + len = len - BOOTLOADER_OFFSET; + + // Re-validate size after adjustment + if (len < MIN_IMAGE_HEADER_SIZE) { + bootloaderErrorMsg = "Too small"; + return false; + } + } + + size_t availableLen = len; + esp_image_header_t imageHeader{}; + memcpy(&imageHeader, buffer, sizeof(imageHeader)); + + // 2. Basic header sanity checks (matches early esp_image_verify checks) + if (imageHeader.magic != ESP_IMAGE_HEADER_MAGIC || + imageHeader.segment_count == 0 || imageHeader.segment_count > 16 || + imageHeader.spi_mode > 3 || + imageHeader.entry_addr < 0x40000000 || imageHeader.entry_addr > 0x50000000) { + bootloaderErrorMsg = "Invalid header"; + return false; + } + + // 3. Chip ID validation (matches esp_image_verify step 3) + if (imageHeader.chip_id != CONFIG_IDF_FIRMWARE_CHIP_ID) { + bootloaderErrorMsg = "Chip ID mismatch"; + return false; + } + + // 4. Validate image size + BootloaderImageSizer sizer; + sizer.feed(buffer, availableLen); + if (!sizer.isSizeKnown()) { + bootloaderErrorMsg = "Invalid image"; + return false; + } + size_t actualBootloaderSize = sizer.totalSize(); + + // 5. SHA256 checksum (optional) + if (imageHeader.hash_appended == 1) { + actualBootloaderSize += 32; + } + + if (actualBootloaderSize > len) { + // Same as above + bootloaderErrorMsg = "Too small"; + return false; + } + + DEBUG_PRINTF_P(PSTR("Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\n"), + imageHeader.segment_count, actualBootloaderSize, len, imageHeader.hash_appended); + + // Update len to reflect actual bootloader size (including hash and checksum, with alignment) + // This is critical - we must write the complete image including checksums + len = actualBootloaderSize; + + return true; +} + +// Bootloader OTA context structure +struct BootloaderUpdateContext { + // State flags + bool replySent = false; + bool uploadComplete = false; + String errorMessage; + + // Buffer to hold bootloader data + uint8_t* buffer = nullptr; + size_t bytesBuffered = 0; + const uint32_t bootloaderOffset = 0x1000; + const uint32_t maxBootloaderSize = 0x10000; // 64KB buffer size +}; + +// Cleanup bootloader OTA context +static void endBootloaderOTA(AsyncWebServerRequest *request) { + BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject); + request->_tempObject = nullptr; + + DEBUG_PRINTF_P(PSTR("EndBootloaderOTA %x --> %x\n"), (uintptr_t)request, (uintptr_t)context); + if (context) { + if (context->buffer) { + free(context->buffer); + context->buffer = nullptr; + } + + // If update failed, restore system state + if (!context->uploadComplete || !context->errorMessage.isEmpty()) { + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + } + + delete context; + } +} + +// Initialize bootloader OTA context +bool initBootloaderOTA(AsyncWebServerRequest *request) { + if (request->_tempObject) { + return true; // Already initialized + } + + BootloaderUpdateContext* context = new BootloaderUpdateContext(); + if (!context) { + DEBUG_PRINTLN(F("Failed to allocate bootloader OTA context")); + return false; + } + request->_tempObject = context; + request->onDisconnect([=]() { endBootloaderOTA(request); }); // ensures cleanup on disconnect + +#ifdef BOOTLOADER_OTA_UNSUPPORTED + context->errorMessage = F("Bootloader update not supported on this chip"); + return false; +#else + DEBUG_PRINTLN(F("Bootloader Update Start - initializing buffer")); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().disableWatchdog(); + #endif + lastEditTime = millis(); // make sure PIN does not lock during update + strip.suspend(); + strip.resetSegments(); + + // Check available heap before attempting allocation + DEBUG_PRINTF_P(PSTR("Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\n"), getContiguousFreeHeap(), context->maxBootloaderSize); + + context->buffer = (uint8_t*)malloc(context->maxBootloaderSize); + if (!context->buffer) { + size_t freeHeapNow = getContiguousFreeHeap(); + DEBUG_PRINTF_P(PSTR("Failed to allocate %d byte bootloader buffer! Contiguous heap: %d bytes\n"), context->maxBootloaderSize, freeHeapNow); + context->errorMessage = "Out of memory! Contiguous heap: " + String(freeHeapNow) + " bytes, need: " + String(context->maxBootloaderSize) + " bytes"; + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + return false; + } + + context->bytesBuffered = 0; + return true; +#endif +} + +// Set bootloader OTA replied flag +void setBootloaderOTAReplied(AsyncWebServerRequest *request) { + BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject); + if (context) { + context->replySent = true; + } +} + +// Get bootloader OTA result +std::pair getBootloaderOTAResult(AsyncWebServerRequest *request) { + BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject); + + if (!context) { + return std::make_pair(true, String(F("Internal error: No bootloader OTA context"))); + } + + bool needsReply = !context->replySent; + String errorMsg = context->errorMessage; + + // If upload was successful, return empty string and trigger reboot + if (context->uploadComplete && errorMsg.isEmpty()) { + doReboot = true; + endBootloaderOTA(request); + return std::make_pair(needsReply, String()); + } + + // If there was an error, return it + if (!errorMsg.isEmpty()) { + endBootloaderOTA(request); + return std::make_pair(needsReply, errorMsg); + } + + // Should never happen + return std::make_pair(true, String(F("Internal software failure"))); +} + +// Handle bootloader OTA data +void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) { + BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject); + + if (!context) { + DEBUG_PRINTLN(F("No bootloader OTA context - ignoring data")); + return; + } + + if (!context->errorMessage.isEmpty()) { + return; + } + + // Buffer the incoming data + if (context->buffer && context->bytesBuffered + len <= context->maxBootloaderSize) { + memcpy(context->buffer + context->bytesBuffered, data, len); + context->bytesBuffered += len; + DEBUG_PRINTF_P(PSTR("Bootloader buffer progress: %d / %d bytes\n"), context->bytesBuffered, context->maxBootloaderSize); + } else if (!context->buffer) { + DEBUG_PRINTLN(F("Bootloader buffer not allocated!")); + context->errorMessage = "Internal error: Bootloader buffer not allocated"; + return; + } else { + size_t totalSize = context->bytesBuffered + len; + DEBUG_PRINTLN(F("Bootloader size exceeds maximum!")); + context->errorMessage = "Bootloader file too large: " + String(totalSize) + " bytes (max: " + String(context->maxBootloaderSize) + " bytes)"; + return; + } + + // Only write to flash when upload is complete + if (isFinal) { + DEBUG_PRINTLN(F("Bootloader Upload Complete - validating and flashing")); + + if (context->buffer && context->bytesBuffered > 0) { + // Prepare pointers for verification (may be adjusted if bootloader at offset) + const uint8_t* bootloaderData = context->buffer; + size_t bootloaderSize = context->bytesBuffered; + + // Verify the complete bootloader image before flashing + // Note: verifyBootloaderImage may adjust bootloaderData pointer and bootloaderSize + // for validation purposes only + if (!verifyBootloaderImage(bootloaderData, bootloaderSize, context->errorMessage)) { + DEBUG_PRINTLN(F("Bootloader validation failed!")); + // Error message already set by verifyBootloaderImage + } else { + // Calculate offset to write to flash + // If bootloaderData was adjusted (partition table detected), we need to skip it in flash too + size_t flashOffset = context->bootloaderOffset; + const uint8_t* dataToWrite = context->buffer; + size_t bytesToWrite = context->bytesBuffered; + + // If validation adjusted the pointer, it means we have a partition table at the start + // In this case, we should skip writing the partition table and write bootloader at 0x1000 + if (bootloaderData != context->buffer) { + // bootloaderData was adjusted - skip partition table in our data + size_t partitionTableSize = bootloaderData - context->buffer; + dataToWrite = bootloaderData; + bytesToWrite = bootloaderSize; + DEBUG_PRINTF_P(PSTR("Skipping %d bytes of partition table data\n"), partitionTableSize); + } + + DEBUG_PRINTF_P(PSTR("Bootloader validation passed - writing %d bytes to flash at 0x%04X\n"), + bytesToWrite, flashOffset); + + // Calculate erase size (must be multiple of 4KB) + size_t eraseSize = ((bytesToWrite + 0xFFF) / 0x1000) * 0x1000; + if (eraseSize > context->maxBootloaderSize) { + eraseSize = context->maxBootloaderSize; + } + + // Erase bootloader region + DEBUG_PRINTF_P(PSTR("Erasing %d bytes at 0x%04X...\n"), eraseSize, flashOffset); + esp_err_t err = esp_flash_erase_region(NULL, flashOffset, eraseSize); + if (err != ESP_OK) { + DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err); + context->errorMessage = "Flash erase failed (error code: " + String(err) + ")"; + } else { + // Write the validated bootloader data to flash + err = esp_flash_write(NULL, dataToWrite, flashOffset, bytesToWrite); + if (err != ESP_OK) { + DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err); + context->errorMessage = "Flash write failed (error code: " + String(err) + ")"; + } else { + DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written to 0x%04X\n"), + bytesToWrite, flashOffset); + // Invalidate cached bootloader hash + invalidateBootloaderSHA256Cache(); + context->uploadComplete = true; + } + } + } + } else if (context->bytesBuffered == 0) { + context->errorMessage = "No bootloader data received"; + } + } +} +#endif diff --git a/wled00/ota_update.h b/wled00/ota_update.h new file mode 100644 index 0000000000..d2fa887d30 --- /dev/null +++ b/wled00/ota_update.h @@ -0,0 +1,99 @@ +// WLED OTA update interface + +#include +#ifdef ESP8266 + #include +#else + #include +#endif + +#pragma once + +// Platform-specific metadata locations +#ifdef ESP32 +#define BUILD_METADATA_SECTION ".rodata_custom_desc" +#elif defined(ESP8266) +#define BUILD_METADATA_SECTION ".ver_number" +#endif + + +class AsyncWebServerRequest; + +/** + * Create an OTA context object on an AsyncWebServerRequest + * @param request Pointer to web request object + * @return true if allocation was successful, false if not + */ +bool initOTA(AsyncWebServerRequest *request); + +/** + * Indicate to the OTA subsystem that a reply has already been generated + * @param request Pointer to web request object + */ +void setOTAReplied(AsyncWebServerRequest *request); + +/** + * Retrieve the OTA result. + * @param request Pointer to web request object + * @return bool indicating if a reply is necessary; string with error message if the update failed. + */ +std::pair getOTAResult(AsyncWebServerRequest *request); + +/** + * Process a block of OTA data. This is a passthrough of an ArUploadHandlerFunction. + * Requires that initOTA be called on the handler object before any work will be done. + * @param request Pointer to web request object + * @param index Offset in to uploaded file + * @param data New data bytes + * @param len Length of new data bytes + * @param isFinal Indicates that this is the last block + * @return bool indicating if a reply is necessary; string with error message if the update failed. + */ +void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal); + +/** + * Mark currently running firmware as valid to prevent auto-rollback on reboot. + * This option can be enabled in some builds/bootloaders, it is an sdkconfig flag. + */ +void markOTAvalid(); + +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) + +/** + * Get bootloader SHA256 as hex string + * @return String containing 64-character hex representation of SHA256 hash + */ +String getBootloaderSHA256Hex(); + +/** + * Create a bootloader OTA context object on an AsyncWebServerRequest + * @param request Pointer to web request object + * @return true if allocation was successful, false if not + */ +bool initBootloaderOTA(AsyncWebServerRequest *request); + +/** + * Indicate to the bootloader OTA subsystem that a reply has already been generated + * @param request Pointer to web request object + */ +void setBootloaderOTAReplied(AsyncWebServerRequest *request); + +/** + * Retrieve the bootloader OTA result. + * @param request Pointer to web request object + * @return bool indicating if a reply is necessary; string with error message if the update failed. + */ +std::pair getBootloaderOTAResult(AsyncWebServerRequest *request); + +/** + * Process a block of bootloader OTA data. This is a passthrough of an ArUploadHandlerFunction. + * Requires that initBootloaderOTA be called on the handler object before any work will be done. + * @param request Pointer to web request object + * @param index Offset in to uploaded file + * @param data New data bytes + * @param len Length of new data bytes + * @param isFinal Indicates that this is the last block + */ +void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal); +#endif + diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index fcd0a40c2c..f5366f3b71 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -1,10 +1,13 @@ #include "wled.h" +// forward declarations +static void _overlayAnalogCountdown(); + /* * Used to draw clock overlays over the strip */ -void _overlayAnalogClock() +static void _overlayAnalogClock() { int overlaySize = overlayMax - overlayMin +1; if (countdownMode) @@ -47,7 +50,7 @@ void _overlayAnalogClock() } -void _overlayAnalogCountdown() +static void _overlayAnalogCountdown() { if ((unsigned long)toki.second() < countdownTime) { @@ -90,9 +93,8 @@ void _overlayAnalogCountdown() void handleOverlayDraw() { UsermodManager::handleOverlayDraw(); if (analogClockSolidBlack) { - const Segment* segments = strip.getSegments(); for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { - const Segment& segment = segments[i]; + const Segment& segment = strip.getSegment(i); if (!segment.isActive()) continue; if (segment.mode > 0 || segment.colors[0] > 0) { return; diff --git a/wled00/palettes.cpp b/wled00/palettes.cpp new file mode 100644 index 0000000000..4781ffc1cc --- /dev/null +++ b/wled00/palettes.cpp @@ -0,0 +1,769 @@ +#include "wled.h" + +/* + * WLED Color palettes + * + * Note: palettes imported from http://seaviewsensing.com/pub/cpt-city are gamma corrected using gammas (1.182, 1.0, 1.136) + * this is done to match colors of the palettes after applying the (default) global gamma of 2.2 to versions + * prior to WLED 0.16 which used pre-applied gammas of (2.6,2.2,2.5) for these palettes. + * Palettes from FastLED are intended to be used without gamma correction, an inverse gamma of 2.2 is applied to original colors + */ + +// Gradient palette "ib_jul01_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/ing/xmas/ib_jul01.c3g +const uint8_t ib_jul01_gp[] PROGMEM = { + 0, 226, 6, 12, + 94, 26, 96, 78, + 132, 130, 189, 94, + 255, 177, 3, 9}; + +// Gradient palette "es_vintage_57_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_57.c3g +const uint8_t es_vintage_57_gp[] PROGMEM = { + 0, 29, 8, 3, + 53, 76, 1, 0, + 104, 142, 96, 28, + 153, 211, 191, 61, + 255, 117, 129, 42}; + +// Gradient palette "es_vintage_01_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_01.c3g +const uint8_t es_vintage_01_gp[] PROGMEM = { + 0, 41, 18, 24, + 51, 73, 0, 22, + 76, 165, 170, 38, + 101, 255, 189, 80, + 127, 139, 56, 40, + 153, 73, 0, 22, + 229, 41, 18, 24, + 255, 41, 18, 24}; + +// Gradient palette "es_rivendell_15_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/es/rivendell/es_rivendell_15.c3g +const uint8_t es_rivendell_15_gp[] PROGMEM = { + 0, 24, 69, 44, + 101, 73, 105, 70, + 165, 129, 140, 97, + 242, 200, 204, 166, + 255, 200, 204, 166}; + +// Gradient palette "rgi_15_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/ds/rgi/rgi_15.c3g +const uint8_t rgi_15_gp[] PROGMEM = { + 0, 41, 14, 99, + 31, 128, 24, 74, + 63, 227, 34, 50, + 95, 132, 31, 76, + 127, 47, 29, 102, + 159, 109, 47, 101, + 191, 176, 66, 100, + 223, 129, 57, 104, + 255, 84, 48, 108}; + +// Gradient palette "retro2_16_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/ma/retro2/retro2_16.c3g +const uint8_t retro2_16_gp[] PROGMEM = { + 0, 222, 191, 8, + 255, 117, 52, 1}; + +// Gradient palette "Analogous_1_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/red/Analogous_1.c3g +const uint8_t Analogous_1_gp[] PROGMEM = { + 0, 38, 0, 255, + 63, 86, 0, 255, + 127, 139, 0, 255, + 191, 196, 0, 117, + 255, 255, 0, 0}; + +// Gradient palette "es_pinksplash_08_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/es/pink_splash/es_pinksplash_08.c3g +const uint8_t es_pinksplash_08_gp[] PROGMEM = { + 0, 186, 63, 255, + 127, 227, 9, 85, + 175, 234, 205, 213, + 221, 205, 38, 176, + 255, 205, 38, 176, +}; + +// Gradient palette "es_ocean_breeze_036_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/es/ocean_breeze/es_ocean_breeze_036.c3g +const uint8_t es_ocean_breeze_036_gp[] PROGMEM = { + 0, 16, 48, 51, + 89, 27, 166, 175, + 153, 197, 233, 255, + 255, 0, 145, 152}; + +// Gradient palette "departure_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/mjf/departure.c3g +const uint8_t departure_gp[] PROGMEM = { + 0, 53, 34, 0, + 42, 86, 51, 0, + 63, 147, 108, 49, + 84, 212, 166, 108, + 106, 235, 212, 180, + 116, 255, 255, 255, + 138, 191, 255, 193, + 148, 84, 255, 88, + 170, 0, 255, 0, + 191, 0, 192, 0, + 212, 0, 128, 0, + 255, 0, 128, 0}; + +// Gradient palette "es_landscape_64_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_64.c3g +const uint8_t es_landscape_64_gp[] PROGMEM = { + 0, 0, 0, 0, + 37, 31, 89, 19, + 76, 72, 178, 43, + 127, 150, 235, 5, + 128, 186, 234, 119, + 130, 222, 233, 252, + 153, 197, 219, 231, + 204, 132, 179, 253, + 255, 28, 107, 225}; + +// Gradient palette "es_landscape_33_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_33.c3g +const uint8_t es_landscape_33_gp[] PROGMEM = { + 0, 12, 45, 0, + 19, 101, 86, 2, + 38, 207, 128, 4, + 63, 243, 197, 18, + 66, 109, 196, 146, + 255, 5, 39, 7}; + +// Gradient palette "rainbowsherbet_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/ma/icecream/rainbowsherbet.c3g +const uint8_t rainbowsherbet_gp[] PROGMEM = { + 0, 255, 102, 41, + 43, 255, 140, 90, + 86, 255, 51, 90, + 127, 255, 153, 169, + 170, 255, 255, 249, + 209, 113, 255, 85, + 255, 157, 255, 137}; + +// Gradient palette "gr65_hult_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/hult/gr65_hult.c3g +const uint8_t gr65_hult_gp[] PROGMEM = { + 0, 251, 216, 252, + 48, 255, 192, 255, + 89, 239, 95, 241, + 160, 51, 153, 217, + 216, 24, 184, 174, + 255, 24, 184, 174}; + +// Gradient palette "gr64_hult_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/hult/gr64_hult.c3g +const uint8_t gr64_hult_gp[] PROGMEM = { + 0, 24, 184, 174, + 66, 8, 162, 150, + 104, 124, 137, 7, + 130, 178, 186, 22, + 150, 124, 137, 7, + 201, 6, 156, 144, + 239, 0, 128, 117, + 255, 0, 128, 117}; + +// Gradient palette "GMT_drywet_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/gmt/GMT_drywet.c3g +const uint8_t GMT_drywet_gp[] PROGMEM = { + 0, 119, 97, 33, + 42, 235, 199, 88, + 84, 169, 238, 124, + 127, 37, 238, 232, + 170, 7, 120, 236, + 212, 27, 1, 175, + 255, 4, 51, 101}; + +// Gradient palette "ib15_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/ing/general/ib15.c3g +const uint8_t ib15_gp[] PROGMEM = { + 0, 177, 160, 199, + 72, 205, 158, 149, + 89, 233, 155, 101, + 107, 255, 95, 63, + 141, 192, 98, 109, + 255, 132, 101, 159}; + +// Gradient palette "Tertiary_01_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/vermillion/Tertiary_01.c3g +const uint8_t Tertiary_01_gp[] PROGMEM = { + 0, 0, 25, 255, + 63, 38, 140, 117, + 127, 86, 255, 0, + 191, 167, 140, 19, + 255, 255, 25, 41}; + +// Gradient palette "lava_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/neota/elem/lava.c3g +const uint8_t lava_gp[] PROGMEM = { + 0, 0, 0, 0, + 46, 77, 0, 0, + 96, 177, 0, 0, + 108, 196, 38, 9, + 119, 215, 76, 19, + 146, 235, 115, 29, + 174, 255, 153, 41, + 188, 255, 178, 41, + 202, 255, 204, 41, + 218, 255, 230, 41, + 234, 255, 255, 41, + 244, 255, 255, 143, + 255, 255, 255, 255}; + +// Gradient palette "fierce-ice_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/neota/elem/fierce-ice.c3g +const uint8_t fierce_ice_gp[] PROGMEM = { + 0, 0, 0, 0, + 59, 0, 51, 117, + 119, 0, 102, 255, + 149, 38, 153, 255, + 180, 86, 204, 255, + 217, 167, 230, 255, + 255, 255, 255, 255}; + +// Gradient palette "Colorfull_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Colorfull.c3g +const uint8_t Colorfull_gp[] PROGMEM = { + 0, 61, 155, 44, + 25, 95, 174, 77, + 60, 132, 193, 113, + 93, 154, 166, 125, + 106, 175, 138, 136, + 109, 183, 121, 137, + 113, 194, 104, 138, + 116, 225, 179, 165, + 124, 255, 255, 192, + 168, 167, 218, 203, + 255, 84, 182, 215}; + +// Gradient palette "Pink_Purple_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Pink_Purple.c3g +const uint8_t Pink_Purple_gp[] PROGMEM = { + 0, 79, 32, 109, + 25, 90, 40, 117, + 51, 102, 48, 124, + 76, 141, 135, 185, + 102, 180, 222, 248, + 109, 208, 236, 252, + 114, 237, 250, 255, + 122, 206, 200, 239, + 149, 177, 149, 222, + 183, 187, 130, 203, + 255, 198, 111, 184}; + +// Gradient palette "Sunset_Real_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Real.c3g +const uint8_t Sunset_Real_gp[] PROGMEM = { + 0, 181, 0, 0, + 22, 218, 85, 0, + 51, 255, 170, 0, + 85, 211, 85, 77, + 135, 167, 0, 169, + 198, 73, 0, 188, + 255, 0, 0, 207}; + +// Gradient palette "Sunset_Yellow_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Yellow.c3g +const uint8_t Sunset_Yellow_gp[] PROGMEM = { + 0, 61, 135, 184, + 36, 129, 188, 169, + 87, 203, 241, 155, + 100, 228, 237, 141, + 107, 255, 232, 127, + 115, 251, 202, 130, + 120, 248, 172, 133, + 128, 251, 202, 130, + 180, 255, 232, 127, + 223, 255, 242, 120, + 255, 255, 252, 113}; + +// Gradient palette "Beech_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Beech.c3g +const uint8_t Beech_gp[] PROGMEM = { + 0, 255, 254, 236, + 12, 255, 254, 236, + 22, 255, 254, 236, + 26, 223, 224, 178, + 28, 192, 195, 124, + 28, 176, 255, 231, + 50, 123, 251, 236, + 71, 74, 246, 241, + 93, 33, 225, 228, + 120, 0, 204, 215, + 133, 4, 168, 178, + 136, 10, 132, 143, + 136, 51, 189, 212, + 208, 23, 159, 201, + 255, 0, 129, 190}; + +// Gradient palette "Another_Sunset_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Another_Sunset.c3g +const uint8_t Another_Sunset_gp[] PROGMEM = { + 0, 175, 121, 62, + 29, 128, 103, 60, + 68, 84, 84, 58, + 68, 248, 184, 55, + 97, 239, 204, 93, + 124, 230, 225, 133, + 178, 102, 125, 129, + 255, 0, 26, 125}; + +// Gradient palette "es_autumn_19_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/es/autumn/es_autumn_19.c3g +const uint8_t es_autumn_19_gp[] PROGMEM = { + 0, 90, 14, 5, + 51, 139, 41, 13, + 84, 180, 70, 17, + 104, 192, 202, 125, + 112, 177, 137, 3, + 122, 190, 200, 131, + 124, 192, 202, 124, + 135, 177, 137, 3, + 142, 194, 203, 118, + 163, 177, 68, 17, + 204, 128, 35, 12, + 249, 74, 5, 2, + 255, 74, 5, 2}; + +// Gradient palette "BlacK_Blue_Magenta_White_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Blue_Magenta_White.c3g +const uint8_t BlacK_Blue_Magenta_White_gp[] PROGMEM = { + 0, 0, 0, 0, + 42, 0, 0, 117, + 84, 0, 0, 255, + 127, 113, 0, 255, + 170, 255, 0, 255, + 212, 255, 128, 255, + 255, 255, 255, 255}; + +// Gradient palette "BlacK_Magenta_Red_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Magenta_Red.c3g +const uint8_t BlacK_Magenta_Red_gp[] PROGMEM = { + 0, 0, 0, 0, + 63, 113, 0, 117, + 127, 255, 0, 255, + 191, 255, 0, 117, + 255, 255, 0, 0}; + +// Gradient palette "BlacK_Red_Magenta_Yellow_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Red_Magenta_Yellow.c3g +const uint8_t BlacK_Red_Magenta_Yellow_gp[] PROGMEM = { + 0, 0, 0, 0, + 42, 113, 0, 0, + 84, 255, 0, 0, + 127, 255, 0, 117, + 170, 255, 0, 255, + 212, 255, 128, 117, + 255, 255, 255, 0}; + +// Gradient palette "Blue_Cyan_Yellow_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/nd/basic/Blue_Cyan_Yellow.c3g +const uint8_t Blue_Cyan_Yellow_gp[] PROGMEM = { + 0, 0, 0, 255, + 63, 0, 128, 255, + 127, 0, 255, 255, + 191, 113, 255, 117, + 255, 255, 255, 0}; + +//Custom palette by Aircoookie +const byte Orange_Teal_gp[] PROGMEM = { + 0, 0,150, 92, + 55, 0,150, 92, + 200, 255, 72, 0, + 255, 255, 72, 0}; + +//Custom palette by Aircoookie +const byte Tiamat_gp[] PROGMEM = { + 0, 1, 2, 14, //gc + 33, 2, 5, 35, //gc from 47, 61,126 + 100, 13,135, 92, //gc from 88,242,247 + 120, 43,255,193, //gc from 135,255,253 + 140, 247, 7,249, //gc from 252, 69,253 + 160, 193, 17,208, //gc from 231, 96,237 + 180, 39,255,154, //gc from 130, 77,213 + 200, 4,213,236, //gc from 57,122,248 + 220, 39,252,135, //gc from 177,254,255 + 240, 193,213,253, //gc from 203,239,253 + 255, 255,249,255}; + +//Custom palette by Aircoookie +const byte April_Night_gp[] PROGMEM = { + 0, 1, 5, 45, //deep blue + 10, 1, 5, 45, + 25, 5,169,175, //light blue + 40, 1, 5, 45, + 61, 1, 5, 45, + 76, 45,175, 31, //green + 91, 1, 5, 45, + 112, 1, 5, 45, + 127, 249,150, 5, //yellow + 143, 1, 5, 45, + 162, 1, 5, 45, + 178, 255, 92, 0, //pastel orange + 193, 1, 5, 45, + 214, 1, 5, 45, + 229, 223, 45, 72, //pink + 244, 1, 5, 45, + 255, 1, 5, 45}; + +const byte Orangery_gp[] PROGMEM = { + 0, 255, 95, 23, + 30, 255, 82, 0, + 60, 223, 13, 8, + 90, 144, 44, 2, + 120, 255,110, 17, + 150, 255, 69, 0, + 180, 158, 13, 11, + 210, 241, 82, 17, + 255, 213, 37, 4}; + +//inspired by Mark Kriegsman https://gist.github.com/kriegsman/756ea6dcae8e30845b5a +const byte C9_gp[] PROGMEM = { + 0, 184, 4, 0, //red + 60, 184, 4, 0, + 65, 144, 44, 2, //amber + 125, 144, 44, 2, + 130, 4, 96, 2, //green + 190, 4, 96, 2, + 195, 7, 7, 88, //blue + 255, 7, 7, 88}; + +const byte Sakura_gp[] PROGMEM = { + 0, 196, 19, 10, + 65, 255, 69, 45, + 130, 223, 45, 72, + 195, 255, 82,103, + 255, 223, 13, 17}; + +const byte Aurora_gp[] PROGMEM = { + 0, 1, 5, 45, //deep blue + 64, 0,200, 23, + 128, 0,255, 0, //green + 170, 0,243, 45, + 200, 0,135, 7, + 255, 1, 5, 45};//deep blue + +const byte Atlantica_gp[] PROGMEM = { + 0, 0, 28,112, //#001C70 + 50, 32, 96,255, //#2060FF + 100, 0,243, 45, + 150, 12, 95, 82, //#0C5F52 + 200, 25,190, 95, //#19BE5F + 255, 40,170, 80};//#28AA50 + + const byte C9_2_gp[] PROGMEM = { + 0, 6, 126, 2, //green + 45, 6, 126, 2, + 46, 4, 30, 114, //blue + 90, 4, 30, 114, + 91, 255, 5, 0, //red + 135, 255, 5, 0, + 136, 196, 57, 2, //amber + 180, 196, 57, 2, + 181, 137, 85, 2, //yellow + 255, 137, 85, 2}; + + //C9, but brighter and with a less purple blue + const byte C9_new_gp[] PROGMEM = { + 0, 255, 5, 0, //red + 60, 255, 5, 0, + 61, 196, 57, 2, //amber (start 61?) + 120, 196, 57, 2, + 121, 6, 126, 2, //green (start 126?) + 180, 6, 126, 2, + 181, 4, 30, 114, //blue (start 191?) + 255, 4, 30, 114}; + +// Gradient palette "temperature_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/arendal/temperature.c3g +const uint8_t temperature_gp[] PROGMEM = { + 0, 20, 92, 171, + 14, 15, 111, 186, + 28, 6, 142, 211, + 42, 2, 161, 227, + 56, 16, 181, 239, + 70, 38, 188, 201, + 84, 86, 204, 200, + 99, 139, 219, 176, + 113, 182, 229, 125, + 127, 196, 230, 63, + 141, 241, 240, 22, + 155, 254, 222, 30, + 170, 251, 199, 4, + 184, 247, 157, 9, + 198, 243, 114, 15, + 226, 213, 30, 29, + 240, 151, 38, 35, + 255, 151, 38, 35}; + +// Gradient palette "bhw1_01_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_01.c3g +const uint8_t retro_clown_gp[] PROGMEM = { + 0, 242, 168, 38, + 117, 226, 78, 80, + 255, 161, 54, 225, +}; + +// Gradient palette "bhw1_04_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_04.c3g +const uint8_t candy_gp[] PROGMEM = { + 0, 243, 242, 23, + 15, 242, 168, 38, + 142, 111, 21, 151, + 198, 74, 22, 150, + 255, 0, 0, 117}; + +// Gradient palette "bhw1_05_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_05.c3g +const uint8_t toxy_reaf_gp[] PROGMEM = { + 0, 2, 239, 126, + 255, 145, 35, 217}; + +// Gradient palette "bhw1_06_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_06.c3g +const uint8_t fairy_reaf_gp[] PROGMEM = { + 0, 220, 19, 187, + 160, 12, 225, 219, + 219, 203, 242, 223, + 255, 255, 255, 255}; + +// Gradient palette "bhw1_14_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_14.c3g +const uint8_t semi_blue_gp[] PROGMEM = { + 0, 0, 0, 0, + 12, 24, 4, 38, + 53, 55, 8, 84, + 80, 43, 48, 159, + 119, 31, 89, 237, + 145, 50, 59, 166, + 186, 71, 30, 98, + 233, 31, 15, 45, + 255, 0, 0, 0}; + +// Gradient palette "bhw1_three_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_three.c3g +const uint8_t pink_candy_gp[] PROGMEM = { + 0, 255, 255, 255, + 45, 50, 64, 255, + 112, 242, 16, 186, + 140, 255, 255, 255, + 155, 242, 16, 186, + 196, 116, 13, 166, + 255, 255, 255, 255}; + +// Gradient palette "bhw1_w00t_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_w00t.c3g +const uint8_t red_reaf_gp[] PROGMEM = { + 0, 36, 68, 114, + 104, 149, 195, 248, + 188, 255, 0, 0, + 255, 94, 14, 9}; + +// Gradient palette "bhw2_23_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_23.c3g +const uint8_t aqua_flash_gp[] PROGMEM = { + 0, 0, 0, 0, + 66, 130, 242, 245, + 96, 255, 255, 53, + 124, 255, 255, 255, + 153, 255, 255, 53, + 188, 130, 242, 245, + 255, 0, 0, 0}; + +// Gradient palette "bhw2_xc_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_xc.c3g +const uint8_t yelblu_hot_gp[] PROGMEM = { + 0, 43, 30, 57, + 58, 73, 0, 119, + 122, 87, 0, 74, + 158, 197, 57, 22, + 183, 218, 117, 27, + 219, 239, 177, 32, + 255, 246, 247, 27, +}; + +// Gradient palette "bhw2_45_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_45.c3g +const uint8_t lite_light_gp[] PROGMEM = { + 0, 0, 0, 0, + 9, 20, 21, 22, + 40, 46, 43, 49, + 66, 46, 43, 49, + 101, 61, 16, 65, + 255, 0, 0, 0}; + +// Gradient palette "bhw2_22_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_22.c3g +const uint8_t red_flash_gp[] PROGMEM = { + 0, 0, 0, 0, + 99, 242, 12, 8, + 130, 253, 228, 163, + 155, 242, 12, 8, + 255, 0, 0, 0}; + +// Gradient palette "bhw3_40_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_40.c3g +const uint8_t blink_red_gp[] PROGMEM = { + 0, 4, 7, 4, + 43, 40, 25, 62, + 76, 61, 15, 36, + 109, 207, 39, 96, + 127, 255, 156, 184, + 165, 185, 73, 207, + 204, 105, 66, 240, + 255, 77, 29, 78}; + +// Gradient palette "bhw3_52_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_52.c3g +const uint8_t red_shift_gp[] PROGMEM = { + 0, 98, 22, 93, + 45, 103, 22, 73, + 99, 192, 45, 56, + 132, 235, 187, 59, + 175, 228, 85, 26, + 201, 228, 56, 48, + 255, 2, 0, 2}; + +// Gradient palette "bhw4_097_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_097.c3g +const uint8_t red_tide_gp[] PROGMEM = { + 0, 251, 46, 0, + 28, 255, 139, 25, + 43, 246, 158, 63, + 58, 246, 216, 123, + 84, 243, 94, 10, + 114, 177, 65, 11, + 140, 255, 241, 115, + 168, 177, 65, 11, + 196, 250, 233, 158, + 216, 255, 94, 6, + 255, 126, 8, 4}; + +// Gradient palette "bhw4_017_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_017.c3g +const uint8_t candy2_gp[] PROGMEM = { + 0, 109, 102, 102, + 25, 42, 49, 71, + 48, 121, 96, 84, + 73, 241, 214, 26, + 89, 216, 104, 44, + 130, 42, 49, 71, + 163, 255, 177, 47, + 186, 241, 214, 26, + 211, 109, 102, 102, + 255, 20, 19, 13}; + +const byte trafficlight_gp[] PROGMEM = { + 0, 0, 0, 0, //black + 85, 0, 255, 0, //green + 170, 255, 255, 0, //yellow + 255, 255, 0, 0}; //red + +const byte Aurora2_gp[] PROGMEM = { + 0, 17, 177, 13, //Greenish + 64, 121, 242, 5, //Greenish + 128, 25, 173, 121, //Turquoise + 192, 250, 77, 127, //Pink + 255, 171, 101, 221}; //Purple + +// FastLed palettes, corrected with inverse gamma of 2.2 to match original looks + +// Party colors +const TProgmemRGBPalette16 PartyColors_gc22 FL_PROGMEM = { + 0x9B00D5, 0xBD00B8, 0xDA0092, 0xF3005C, + 0xF45500, 0xDC8F00, 0xD5B400, 0xD5D500, + 0xD59B00, 0xEF6600, 0xF90044, 0xE10086, + 0xC400B0, 0xA300CF, 0x7600E8, 0x0032FC}; + +// Rainbow colors +const TProgmemRGBPalette16 RainbowColors_gc22 FL_PROGMEM = { + 0xFF0000, 0xEB7000, 0xD59B00, 0xD5BA00, + 0xD5D500, 0x9CEB00, 0x00FF00, 0x00EB70, + 0x00D59B, 0x009CD4, 0x0000FF, 0x7000EB, + 0x9B00D5, 0xBA00BB, 0xD5009B, 0xEB0072}; + +// Rainbow colors with alternatating stripes of black +const TProgmemRGBPalette16 RainbowStripeColors_gc22 FL_PROGMEM = { + 0xFF0000, 0x000000, 0xD59B00, 0x000000, + 0xD5D500, 0x000000, 0x00FF00, 0x000000, + 0x00D59B, 0x000000, 0x0000FF, 0x000000, + 0x9B00D5, 0x000000, 0xD5009B, 0x000000}; + +// array of fastled palettes (palette 6 - 12) +const TProgmemRGBPalette16 *const fastledPalettes[] PROGMEM = { + &PartyColors_gc22, //06-00 Party + &CloudColors_p, //07-01 Cloud + &LavaColors_p, //08-02 Lava + &OceanColors_p, //09-03 Ocean + &ForestColors_p, //10-04 Forest + &RainbowColors_gc22, //11-05 Rainbow + &RainbowStripeColors_gc22 //12-06 Rainbow Bands +}; + +// Single array of defined cpt-city color palettes. +// This will let us programmatically choose one based on +// a number, rather than having to activate each explicitly +// by name every time. +const uint8_t* const gGradientPalettes[] PROGMEM = { + Sunset_Real_gp, //13-00 Sunset + es_rivendell_15_gp, //14-01 Rivendell + es_ocean_breeze_036_gp, //15-02 Breeze + rgi_15_gp, //16-03 Red & Blue + retro2_16_gp, //17-04 Yellowout + Analogous_1_gp, //18-05 Analogous + es_pinksplash_08_gp, //19-06 Splash + Sunset_Yellow_gp, //20-07 Pastel + Another_Sunset_gp, //21-08 Sunset2 + Beech_gp, //22-09 Beech + es_vintage_01_gp, //23-10 Vintage + departure_gp, //24-11 Departure + es_landscape_64_gp, //25-12 Landscape + es_landscape_33_gp, //26-13 Beach + rainbowsherbet_gp, //27-14 Sherbet + gr65_hult_gp, //28-15 Hult + gr64_hult_gp, //29-16 Hult64 + GMT_drywet_gp, //30-17 Drywet + ib_jul01_gp, //31-18 Jul + es_vintage_57_gp, //32-19 Grintage + ib15_gp, //33-20 Rewhi + Tertiary_01_gp, //34-21 Tertiary + lava_gp, //35-22 Fire + fierce_ice_gp, //36-23 Icefire + Colorfull_gp, //37-24 Cyane + Pink_Purple_gp, //38-25 Light Pink + es_autumn_19_gp, //39-26 Autumn + BlacK_Blue_Magenta_White_gp, //40-27 Magenta + BlacK_Magenta_Red_gp, //41-28 Magred + BlacK_Red_Magenta_Yellow_gp, //42-29 Yelmag + Blue_Cyan_Yellow_gp, //43-30 Yelblu + Orange_Teal_gp, //44-31 Orange & Teal + Tiamat_gp, //45-32 Tiamat + April_Night_gp, //46-33 April Night + Orangery_gp, //47-34 Orangery + C9_gp, //48-35 C9 + Sakura_gp, //49-36 Sakura + Aurora_gp, //50-37 Aurora + Atlantica_gp, //51-38 Atlantica + C9_2_gp, //52-39 C9 2 + C9_new_gp, //53-40 C9 New + temperature_gp, //54-41 Temperature + Aurora2_gp, //55-42 Aurora 2 + retro_clown_gp, //56-43 Retro Clown + candy_gp, //57-44 Candy + toxy_reaf_gp, //58-45 Toxy Reaf + fairy_reaf_gp, //59-46 Fairy Reaf + semi_blue_gp, //60-47 Semi Blue + pink_candy_gp, //61-48 Pink Candy + red_reaf_gp, //62-49 Red Reaf + aqua_flash_gp, //63-50 Aqua Flash + yelblu_hot_gp, //64-51 Yelblu Hot + lite_light_gp, //65-52 Lite Light + red_flash_gp, //66-53 Red Flash + blink_red_gp, //67-54 Blink Red + red_shift_gp, //68-55 Red Shift + red_tide_gp, //69-56 Red Tide + candy2_gp, //70-57 Candy2 + trafficlight_gp //71-58 Traffic Light +}; diff --git a/wled00/palettes.h b/wled00/palettes.h deleted file mode 100644 index 41dfbbc163..0000000000 --- a/wled00/palettes.h +++ /dev/null @@ -1,912 +0,0 @@ -/* - * Color palettes for FastLED effects (65-73). - */ - -// From ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb -// Unfortunately, these are stored in RAM! - -// Gradient palette "ib_jul01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ing/xmas/tn/ib_jul01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -#ifndef PalettesWLED_h -#define PalettesWLED_h - -const byte ib_jul01_gp[] PROGMEM = { - 0, 194, 1, 1, - 94, 1, 29, 18, - 132, 57,131, 28, - 255, 113, 1, 1}; - -// Gradient palette "es_vintage_57_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/vintage/tn/es_vintage_57.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte es_vintage_57_gp[] PROGMEM = { - 0, 2, 1, 1, - 53, 18, 1, 0, - 104, 69, 29, 1, - 153, 167,135, 10, - 255, 46, 56, 4}; - - -// Gradient palette "es_vintage_01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/vintage/tn/es_vintage_01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte es_vintage_01_gp[] PROGMEM = { - 0, 4, 1, 1, - 51, 16, 0, 1, - 76, 97,104, 3, - 101, 255,131, 19, - 127, 67, 9, 4, - 153, 16, 0, 1, - 229, 4, 1, 1, - 255, 4, 1, 1}; - - -// Gradient palette "es_rivendell_15_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/rivendell/tn/es_rivendell_15.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte es_rivendell_15_gp[] PROGMEM = { - 0, 1, 14, 5, - 101, 16, 36, 14, - 165, 56, 68, 30, - 242, 150,156, 99, - 255, 150,156, 99}; - - -// Gradient palette "rgi_15_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ds/rgi/tn/rgi_15.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 36 bytes of program space. -// Edited to be brighter - -const byte rgi_15_gp[] PROGMEM = { - 0, 4, 1, 70, - 31, 55, 1, 30, - 63, 255, 4, 7, - 95, 59, 2, 29, - 127, 11, 3, 50, - 159, 39, 8, 60, - 191, 112, 19, 40, - 223, 78, 11, 39, - 255, 29, 8, 59}; - - -// Gradient palette "retro2_16_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ma/retro2/tn/retro2_16.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 8 bytes of program space. - -const byte retro2_16_gp[] PROGMEM = { - 0, 188,135, 1, - 255, 46, 7, 1}; - - -// Gradient palette "Analogous_1_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/red/tn/Analogous_1.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte Analogous_1_gp[] PROGMEM = { - 0, 3, 0,255, - 63, 23, 0,255, - 127, 67, 0,255, - 191, 142, 0, 45, - 255, 255, 0, 0}; - - -// Gradient palette "es_pinksplash_08_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/pink_splash/tn/es_pinksplash_08.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte es_pinksplash_08_gp[] PROGMEM = { - 0, 126, 11,255, - 127, 197, 1, 22, - 175, 210,157,172, - 221, 157, 3,112, - 255, 157, 3,112}; - - -// Gradient palette "es_ocean_breeze_036_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/ocean_breeze/tn/es_ocean_breeze_036.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -const byte es_ocean_breeze_036_gp[] PROGMEM = { - 0, 1, 6, 7, - 89, 1, 99,111, - 153, 144,209,255, - 255, 0, 73, 82}; - - -// Gradient palette "departure_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/mjf/tn/departure.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 88 bytes of program space. - -const byte departure_gp[] PROGMEM = { - 0, 8, 3, 0, - 42, 23, 7, 0, - 63, 75, 38, 6, - 84, 169, 99, 38, - 106, 213,169,119, - 116, 255,255,255, - 138, 135,255,138, - 148, 22,255, 24, - 170, 0,255, 0, - 191, 0,136, 0, - 212, 0, 55, 0, - 255, 0, 55, 0}; - - -// Gradient palette "es_landscape_64_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_64.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 36 bytes of program space. - -const byte es_landscape_64_gp[] PROGMEM = { - 0, 0, 0, 0, - 37, 2, 25, 1, - 76, 15,115, 5, - 127, 79,213, 1, - 128, 126,211, 47, - 130, 188,209,247, - 153, 144,182,205, - 204, 59,117,250, - 255, 1, 37,192}; - - -// Gradient palette "es_landscape_33_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_33.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte es_landscape_33_gp[] PROGMEM = { - 0, 1, 5, 0, - 19, 32, 23, 1, - 38, 161, 55, 1, - 63, 229,144, 1, - 66, 39,142, 74, - 255, 1, 4, 1}; - - -// Gradient palette "rainbowsherbet_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ma/icecream/tn/rainbowsherbet.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte rainbowsherbet_gp[] PROGMEM = { - 0, 255, 33, 4, - 43, 255, 68, 25, - 86, 255, 7, 25, - 127, 255, 82,103, - 170, 255,255,242, - 209, 42,255, 22, - 255, 87,255, 65}; - - -// Gradient palette "gr65_hult_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/hult/tn/gr65_hult.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte gr65_hult_gp[] PROGMEM = { - 0, 247,176,247, - 48, 255,136,255, - 89, 220, 29,226, - 160, 7, 82,178, - 216, 1,124,109, - 255, 1,124,109}; - - -// Gradient palette "gr64_hult_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/hult/tn/gr64_hult.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte gr64_hult_gp[] PROGMEM = { - 0, 1,124,109, - 66, 1, 93, 79, - 104, 52, 65, 1, - 130, 115,127, 1, - 150, 52, 65, 1, - 201, 1, 86, 72, - 239, 0, 55, 45, - 255, 0, 55, 45}; - - -// Gradient palette "GMT_drywet_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gmt/tn/GMT_drywet.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte GMT_drywet_gp[] PROGMEM = { - 0, 47, 30, 2, - 42, 213,147, 24, - 84, 103,219, 52, - 127, 3,219,207, - 170, 1, 48,214, - 212, 1, 1,111, - 255, 1, 7, 33}; - - -// Gradient palette "ib15_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ing/general/tn/ib15.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte ib15_gp[] PROGMEM = { - 0, 113, 91,147, - 72, 157, 88, 78, - 89, 208, 85, 33, - 107, 255, 29, 11, - 141, 137, 31, 39, - 255, 59, 33, 89}; - - -// Gradient palette "Tertiary_01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/vermillion/tn/Tertiary_01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte Tertiary_01_gp[] PROGMEM = { - 0, 0, 1,255, - 63, 3, 68, 45, - 127, 23,255, 0, - 191, 100, 68, 1, - 255, 255, 1, 4}; - - -// Gradient palette "lava_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/lava.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 52 bytes of program space. - -const byte lava_gp[] PROGMEM = { - 0, 0, 0, 0, - 46, 18, 0, 0, - 96, 113, 0, 0, - 108, 142, 3, 1, - 119, 175, 17, 1, - 146, 213, 44, 2, - 174, 255, 82, 4, - 188, 255,115, 4, - 202, 255,156, 4, - 218, 255,203, 4, - 234, 255,255, 4, - 244, 255,255, 71, - 255, 255,255,255}; - - -// Gradient palette "fierce_ice_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/fierce-ice.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte fierce_ice_gp[] PROGMEM = { - 0, 0, 0, 0, - 59, 0, 9, 45, - 119, 0, 38,255, - 149, 3,100,255, - 180, 23,199,255, - 217, 100,235,255, - 255, 255,255,255}; - - -// Gradient palette "Colorfull_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Colorfull.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 44 bytes of program space. - -const byte Colorfull_gp[] PROGMEM = { - 0, 10, 85, 5, - 25, 29,109, 18, - 60, 59,138, 42, - 93, 83, 99, 52, - 106, 110, 66, 64, - 109, 123, 49, 65, - 113, 139, 35, 66, - 116, 192,117, 98, - 124, 255,255,137, - 168, 100,180,155, - 255, 22,121,174}; - - -// Gradient palette "Pink_Purple_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Pink_Purple.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 44 bytes of program space. - -const byte Pink_Purple_gp[] PROGMEM = { - 0, 19, 2, 39, - 25, 26, 4, 45, - 51, 33, 6, 52, - 76, 68, 62,125, - 102, 118,187,240, - 109, 163,215,247, - 114, 217,244,255, - 122, 159,149,221, - 149, 113, 78,188, - 183, 128, 57,155, - 255, 146, 40,123}; - - -// Gradient palette "Sunset_Real_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Real.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte Sunset_Real_gp[] PROGMEM = { - 0, 120, 0, 0, - 22, 179, 22, 0, - 51, 255,104, 0, - 85, 167, 22, 18, - 135, 100, 0,103, - 198, 16, 0,130, - 255, 0, 0,160}; - - -// Gradient palette "Sunset_Yellow_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Yellow.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 44 bytes of program space. - -const byte Sunset_Yellow_gp[] PROGMEM = { - 0, 10, 62,123, - 36, 56,130,103, - 87, 153,225, 85, - 100, 199,217, 68, - 107, 255,207, 54, - 115, 247,152, 57, - 120, 239,107, 61, - 128, 247,152, 57, - 180, 255,207, 54, - 223, 255,227, 48, - 255, 255,248, 42}; - - -// Gradient palette "Beech_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Beech.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 60 bytes of program space. - -const byte Beech_gp[] PROGMEM = { - 0, 255,252,214, - 12, 255,252,214, - 22, 255,252,214, - 26, 190,191,115, - 28, 137,141, 52, - 28, 112,255,205, - 50, 51,246,214, - 71, 17,235,226, - 93, 2,193,199, - 120, 0,156,174, - 133, 1,101,115, - 136, 1, 59, 71, - 136, 7,131,170, - 208, 1, 90,151, - 255, 0, 56,133}; - - -// Gradient palette "Another_Sunset_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Another_Sunset.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte Another_Sunset_gp[] PROGMEM = { - 0, 110, 49, 11, - 29, 55, 34, 10, - 68, 22, 22, 9, - 68, 239,124, 8, - 97, 220,156, 27, - 124, 203,193, 61, - 178, 33, 53, 56, - 255, 0, 1, 52}; - - - - - -// Gradient palette "es_autumn_19_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/autumn/tn/es_autumn_19.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 52 bytes of program space. - -const byte es_autumn_19_gp[] PROGMEM = { - 0, 26, 1, 1, - 51, 67, 4, 1, - 84, 118, 14, 1, - 104, 137,152, 52, - 112, 113, 65, 1, - 122, 133,149, 59, - 124, 137,152, 52, - 135, 113, 65, 1, - 142, 139,154, 46, - 163, 113, 13, 1, - 204, 55, 3, 1, - 249, 17, 1, 1, - 255, 17, 1, 1}; - - -// Gradient palette "BlacK_Blue_Magenta_White_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Blue_Magenta_White.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte BlacK_Blue_Magenta_White_gp[] PROGMEM = { - 0, 0, 0, 0, - 42, 0, 0, 45, - 84, 0, 0,255, - 127, 42, 0,255, - 170, 255, 0,255, - 212, 255, 55,255, - 255, 255,255,255}; - - -// Gradient palette "BlacK_Magenta_Red_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Magenta_Red.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte BlacK_Magenta_Red_gp[] PROGMEM = { - 0, 0, 0, 0, - 63, 42, 0, 45, - 127, 255, 0,255, - 191, 255, 0, 45, - 255, 255, 0, 0}; - - -// Gradient palette "BlacK_Red_Magenta_Yellow_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Red_Magenta_Yellow.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte BlacK_Red_Magenta_Yellow_gp[] PROGMEM = { - 0, 0, 0, 0, - 42, 42, 0, 0, - 84, 255, 0, 0, - 127, 255, 0, 45, - 170, 255, 0,255, - 212, 255, 55, 45, - 255, 255,255, 0}; - - -// Gradient palette "Blue_Cyan_Yellow_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/Blue_Cyan_Yellow.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte Blue_Cyan_Yellow_gp[] PROGMEM = { - 0, 0, 0,255, - 63, 0, 55,255, - 127, 0,255,255, - 191, 42,255, 45, - 255, 255,255, 0}; - - -//Custom palette by Aircoookie - -const byte Orange_Teal_gp[] PROGMEM = { - 0, 0,150, 92, - 55, 0,150, 92, - 200, 255, 72, 0, - 255, 255, 72, 0}; - -//Custom palette by Aircoookie - -const byte Tiamat_gp[] PROGMEM = { - 0, 1, 2, 14, //gc - 33, 2, 5, 35, //gc from 47, 61,126 - 100, 13,135, 92, //gc from 88,242,247 - 120, 43,255,193, //gc from 135,255,253 - 140, 247, 7,249, //gc from 252, 69,253 - 160, 193, 17,208, //gc from 231, 96,237 - 180, 39,255,154, //gc from 130, 77,213 - 200, 4,213,236, //gc from 57,122,248 - 220, 39,252,135, //gc from 177,254,255 - 240, 193,213,253, //gc from 203,239,253 - 255, 255,249,255}; - -//Custom palette by Aircoookie - -const byte April_Night_gp[] PROGMEM = { - 0, 1, 5, 45, //deep blue - 10, 1, 5, 45, - 25, 5,169,175, //light blue - 40, 1, 5, 45, - 61, 1, 5, 45, - 76, 45,175, 31, //green - 91, 1, 5, 45, - 112, 1, 5, 45, - 127, 249,150, 5, //yellow - 143, 1, 5, 45, - 162, 1, 5, 45, - 178, 255, 92, 0, //pastel orange - 193, 1, 5, 45, - 214, 1, 5, 45, - 229, 223, 45, 72, //pink - 244, 1, 5, 45, - 255, 1, 5, 45}; - -const byte Orangery_gp[] PROGMEM = { - 0, 255, 95, 23, - 30, 255, 82, 0, - 60, 223, 13, 8, - 90, 144, 44, 2, - 120, 255,110, 17, - 150, 255, 69, 0, - 180, 158, 13, 11, - 210, 241, 82, 17, - 255, 213, 37, 4}; - -//inspired by Mark Kriegsman https://gist.github.com/kriegsman/756ea6dcae8e30845b5a -const byte C9_gp[] PROGMEM = { - 0, 184, 4, 0, //red - 60, 184, 4, 0, - 65, 144, 44, 2, //amber - 125, 144, 44, 2, - 130, 4, 96, 2, //green - 190, 4, 96, 2, - 195, 7, 7, 88, //blue - 255, 7, 7, 88}; - -const byte Sakura_gp[] PROGMEM = { - 0, 196, 19, 10, - 65, 255, 69, 45, - 130, 223, 45, 72, - 195, 255, 82,103, - 255, 223, 13, 17}; - -const byte Aurora_gp[] PROGMEM = { - 0, 1, 5, 45, //deep blue - 64, 0,200, 23, - 128, 0,255, 0, //green - 170, 0,243, 45, - 200, 0,135, 7, - 255, 1, 5, 45};//deep blue - -const byte Atlantica_gp[] PROGMEM = { - 0, 0, 28,112, //#001C70 - 50, 32, 96,255, //#2060FF - 100, 0,243, 45, - 150, 12, 95, 82, //#0C5F52 - 200, 25,190, 95, //#19BE5F - 255, 40,170, 80};//#28AA50 - - const byte C9_2_gp[] PROGMEM = { - 0, 6, 126, 2, //green - 45, 6, 126, 2, - 45, 4, 30, 114, //blue - 90, 4, 30, 114, - 90, 255, 5, 0, //red - 135, 255, 5, 0, - 135, 196, 57, 2, //amber - 180, 196, 57, 2, - 180, 137, 85, 2, //yellow - 255, 137, 85, 2}; - - //C9, but brighter and with a less purple blue - const byte C9_new_gp[] PROGMEM = { - 0, 255, 5, 0, //red - 60, 255, 5, 0, - 60, 196, 57, 2, //amber (start 61?) - 120, 196, 57, 2, - 120, 6, 126, 2, //green (start 126?) - 180, 6, 126, 2, - 180, 4, 30, 114, //blue (start 191?) - 255, 4, 30, 114}; - -// Gradient palette "temperature_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/arendal/tn/temperature.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 144 bytes of program space. - -const byte temperature_gp[] PROGMEM = { - 0, 1, 27,105, - 14, 1, 40,127, - 28, 1, 70,168, - 42, 1, 92,197, - 56, 1,119,221, - 70, 3,130,151, - 84, 23,156,149, - 99, 67,182,112, - 113, 121,201, 52, - 127, 142,203, 11, - 141, 224,223, 1, - 155, 252,187, 2, - 170, 247,147, 1, - 184, 237, 87, 1, - 198, 229, 43, 1, - 226, 171, 2, 2, - 240, 80, 3, 3, - 255, 80, 3, 3}; - - const byte Aurora2_gp[] PROGMEM = { - 0, 17, 177, 13, //Greenish - 64, 121, 242, 5, //Greenish - 128, 25, 173, 121, //Turquoise - 192, 250, 77, 127, //Pink - 255, 171, 101, 221 //Purple - }; - - // Gradient palette "bhw1_01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 12 bytes of program space. - -const byte retro_clown_gp[] PROGMEM = { - 0, 227,101, 3, - 117, 194, 18, 19, - 255, 92, 8,192}; - -// Gradient palette "bhw1_04_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_04.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte candy_gp[] PROGMEM = { - 0, 229,227, 1, - 15, 227,101, 3, - 142, 40, 1, 80, - 198, 17, 1, 79, - 255, 0, 0, 45}; - -// Gradient palette "bhw1_05_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_05.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 8 bytes of program space. - -const byte toxy_reaf_gp[] PROGMEM = { - 0, 1,221, 53, - 255, 73, 3,178}; - -// Gradient palette "bhw1_06_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_06.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -const byte fairy_reaf_gp[] PROGMEM = { - 0, 184, 1,128, - 160, 1,193,182, - 219, 153,227,190, - 255, 255,255,255}; - -// Gradient palette "bhw1_14_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_14.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 36 bytes of program space. - -const byte semi_blue_gp[] PROGMEM = { - 0, 0, 0, 0, - 12, 1, 1, 3, - 53, 8, 1, 22, - 80, 4, 6, 89, - 119, 2, 25,216, - 145, 7, 10, 99, - 186, 15, 2, 31, - 233, 2, 1, 5, - 255, 0, 0, 0}; - -// Gradient palette "bhw1_three_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_three.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte pink_candy_gp[] PROGMEM = { - 0, 255,255,255, - 45, 7, 12,255, - 112, 227, 1,127, - 112, 227, 1,127, - 140, 255,255,255, - 155, 227, 1,127, - 196, 45, 1, 99, - 255, 255,255,255}; - -// Gradient palette "bhw1_w00t_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_w00t.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -const byte red_reaf_gp[] PROGMEM = { - 0, 3, 13, 43, - 104, 78,141,240, - 188, 255, 0, 0, - 255, 28, 1, 1}; - - -// Gradient palette "bhw2_23_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_23.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Red & Flash in SR -// Size: 28 bytes of program space. - -const byte aqua_flash_gp[] PROGMEM = { - 0, 0, 0, 0, - 66, 57,227,233, - 96, 255,255, 8, - 124, 255,255,255, - 153, 255,255, 8, - 188, 57,227,233, - 255, 0, 0, 0}; - -// Gradient palette "bhw2_xc_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_xc.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// YBlue in SR -// Size: 28 bytes of program space. - -const byte yelblu_hot_gp[] PROGMEM = { - 0, 4, 2, 9, - 58, 16, 0, 47, - 122, 24, 0, 16, - 158, 144, 9, 1, - 183, 179, 45, 1, - 219, 220,114, 2, - 255, 234,237, 1}; - - // Gradient palette "bhw2_45_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_45.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte lite_light_gp[] PROGMEM = { - 0, 0, 0, 0, - 9, 1, 1, 1, - 40, 5, 5, 6, - 66, 5, 5, 6, - 101, 10, 1, 12, - 255, 0, 0, 0}; - -// Gradient palette "bhw2_22_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_22.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Pink Plasma in SR -// Size: 20 bytes of program space. - -const byte red_flash_gp[] PROGMEM = { - 0, 0, 0, 0, - 99, 227, 1, 1, - 130, 249,199, 95, - 155, 227, 1, 1, - 255, 0, 0, 0}; - -// Gradient palette "bhw3_40_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw3/tn/bhw3_40.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte blink_red_gp[] PROGMEM = { - 0, 1, 1, 1, - 43, 4, 1, 11, - 76, 10, 1, 3, - 109, 161, 4, 29, - 127, 255, 86,123, - 165, 125, 16,160, - 204, 35, 13,223, - 255, 18, 2, 18}; - -// Gradient palette "bhw3_52_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw3/tn/bhw3_52.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Yellow2Blue in SR -// Size: 28 bytes of program space. - -const byte red_shift_gp[] PROGMEM = { - 0, 31, 1, 27, - 45, 34, 1, 16, - 99, 137, 5, 9, - 132, 213,128, 10, - 175, 199, 22, 1, - 201, 199, 9, 6, - 255, 1, 0, 1}; - -// Gradient palette "bhw4_097_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw4/tn/bhw4_097.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Yellow2Red in SR -// Size: 44 bytes of program space. - -const byte red_tide_gp[] PROGMEM = { - 0, 247, 5, 0, - 28, 255, 67, 1, - 43, 234, 88, 11, - 58, 234,176, 51, - 84, 229, 28, 1, - 114, 113, 12, 1, - 140, 255,225, 44, - 168, 113, 12, 1, - 196, 244,209, 88, - 216, 255, 28, 1, - 255, 53, 1, 1}; - -// Gradient palette "bhw4_017_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw4/tn/bhw4_017.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 40 bytes of program space. - -const byte candy2_gp[] PROGMEM = { - 0, 39, 33, 34, - 25, 4, 6, 15, - 48, 49, 29, 22, - 73, 224,173, 1, - 89, 177, 35, 5, - 130, 4, 6, 15, - 163, 255,114, 6, - 186, 224,173, 1, - 211, 39, 33, 34, - 255, 1, 1, 1}; - -// Single array of defined cpt-city color palettes. -// This will let us programmatically choose one based on -// a number, rather than having to activate each explicitly -// by name every time. -const byte* const gGradientPalettes[] PROGMEM = { - Sunset_Real_gp, //13-00 Sunset - es_rivendell_15_gp, //14-01 Rivendell - es_ocean_breeze_036_gp, //15-02 Breeze - rgi_15_gp, //16-03 Red & Blue - retro2_16_gp, //17-04 Yellowout - Analogous_1_gp, //18-05 Analogous - es_pinksplash_08_gp, //19-06 Splash - Sunset_Yellow_gp, //20-07 Pastel - Another_Sunset_gp, //21-08 Sunset2 - Beech_gp, //22-09 Beech - es_vintage_01_gp, //23-10 Vintage - departure_gp, //24-11 Departure - es_landscape_64_gp, //25-12 Landscape - es_landscape_33_gp, //26-13 Beach - rainbowsherbet_gp, //27-14 Sherbet - gr65_hult_gp, //28-15 Hult - gr64_hult_gp, //29-16 Hult64 - GMT_drywet_gp, //30-17 Drywet - ib_jul01_gp, //31-18 Jul - es_vintage_57_gp, //32-19 Grintage - ib15_gp, //33-20 Rewhi - Tertiary_01_gp, //34-21 Tertiary - lava_gp, //35-22 Fire - fierce_ice_gp, //36-23 Icefire - Colorfull_gp, //37-24 Cyane - Pink_Purple_gp, //38-25 Light Pink - es_autumn_19_gp, //39-26 Autumn - BlacK_Blue_Magenta_White_gp, //40-27 Magenta - BlacK_Magenta_Red_gp, //41-28 Magred - BlacK_Red_Magenta_Yellow_gp, //42-29 Yelmag - Blue_Cyan_Yellow_gp, //43-30 Yelblu - Orange_Teal_gp, //44-31 Orange & Teal - Tiamat_gp, //45-32 Tiamat - April_Night_gp, //46-33 April Night - Orangery_gp, //47-34 Orangery - C9_gp, //48-35 C9 - Sakura_gp, //49-36 Sakura - Aurora_gp, //50-37 Aurora - Atlantica_gp, //51-38 Atlantica - C9_2_gp, //52-39 C9 2 - C9_new_gp, //53-40 C9 New - temperature_gp, //54-41 Temperature - Aurora2_gp, //55-42 Aurora 2 - retro_clown_gp, //56-43 Retro Clown - candy_gp, //57-44 Candy - toxy_reaf_gp, //58-45 Toxy Reaf - fairy_reaf_gp, //59-46 Fairy Reaf - semi_blue_gp, //60-47 Semi Blue - pink_candy_gp, //61-48 Pink Candy - red_reaf_gp, //62-49 Red Reaf - aqua_flash_gp, //63-50 Aqua Flash - yelblu_hot_gp, //64-51 Yelblu Hot - lite_light_gp, //65-52 Lite Light - red_flash_gp, //66-53 Red Flash - blink_red_gp, //67-54 Blink Red - red_shift_gp, //68-55 Red Shift - red_tide_gp, //69-56 Red Tide - candy2_gp //70-57 Candy2 -}; - -#endif diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp index 0d4c2ad5cb..84eaaec1fc 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -1,5 +1,5 @@ -#include "pin_manager.h" #include "wled.h" +#include "pin_manager.h" #ifdef ARDUINO_ARCH_ESP32 #ifdef bitRead @@ -13,6 +13,16 @@ #endif #endif +// Pin management state variables +#ifdef ESP8266 +static uint32_t pinAlloc = 0UL; // 1 bit per pin, we use first 17bits +#else +static uint64_t pinAlloc = 0ULL; // 1 bit per pin, we use 50 bits on ESP32-S3 +static uint16_t ledcAlloc = 0; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) +#endif +static uint8_t i2cAllocCount = 0; // allow multiple allocation of I2C bus pins but keep track of allocations +static uint8_t spiAllocCount = 0; // allow multiple allocation of SPI bus pins but keep track of allocations +static PinOwner ownerTag[WLED_NUM_PINS] = { PinOwner::None }; /// Actual allocation/deallocation routines bool PinManager::deallocatePin(byte gpio, PinOwner tag) @@ -90,7 +100,8 @@ bool PinManager::allocateMultiplePins(const managed_pin_type * mptArray, byte ar // as this can greatly simplify configuration arrays continue; } - if (!isPinOk(gpio, mptArray[i].isOutput)) { + // allow any GPIO for Ethernet (compile time assigned) + if (!(isPinOk(gpio, mptArray[i].isOutput) || tag==PinOwner::Ethernet)) { DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL Invalid pin attempted to be allocated: GPIO %d as %s\n."), gpio, mptArray[i].isOutput ? PSTR("output"): PSTR("input")); shouldFail = true; } @@ -128,10 +139,18 @@ bool PinManager::allocateMultiplePins(const managed_pin_type * mptArray, byte ar return true; } +bool PinManager::allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output) { + PinManagerPinType pins[arrayElementCount]; + for (int i=0; i= WLED_NUM_PINS) || tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI) { + // DMX_INPUT pins have to be allocated using allocateMultiplePins variant since there is always RX/TX/EN triple + if (!isPinOk(gpio, output) || (gpio >= WLED_NUM_PINS) || tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI + || tag==PinOwner::DMX_INPUT) { #ifdef WLED_DEBUG if (gpio < 255) { // 255 (-1) is the "not defined GPIO" if (!isPinOk(gpio, output)) { @@ -214,9 +233,27 @@ bool PinManager::isPinOk(byte gpio, bool output) // JTAG: GPIO39-42 are usually used for inline debugging // GPIO46 is input only and pulled down #else - if (gpio > 5 && gpio < 12) return false; //SPI flash pins - if (strncmp_P(PSTR("ESP32-PICO"), ESP.getChipModel(), 10) == 0 && (gpio == 16 || gpio == 17)) return false; // PICO-D4: gpio16+17 are in use for onboard SPI FLASH - if (gpio == 16 || gpio == 17) return !psramFound(); //PSRAM pins on ESP32 (these are IO) + + if ((strncmp_P(PSTR("ESP32-U4WDH"), ESP.getChipModel(), 11) == 0) || // this is the correct identifier, but.... + (strncmp_P(PSTR("ESP32-PICO-D"), ESP.getChipModel(), 12) == 0)) { // https://github.com/espressif/arduino-esp32/issues/10683 + // this chip has 4 MB of internal Flash and different packaging, so available pins are different! + if ((gpio > 5 && gpio < 9) || gpio == 11) return false; // U4WDH/PICO-D2 & PICO-D4: GPIO 6, 7, 8, 11 are used for SPI flash; 9 & 10 are free + if (gpio == 16 || gpio == 17) return false; // U4WDH/PICO-D?: GPIO 16 and 17 are used for PSRAM + } else if (strncmp_P(PSTR("ESP32-PICO-V3"), ESP.getChipModel(), 13) == 0) { + if (gpio == 6 || gpio == 11) return false; // PICO-V3: uses GPIO 6 and 11 for flash + if (strstr_P(ESP.getChipModel(), PSTR("V3-02")) != nullptr && (gpio == 9 || gpio == 10)) return false; // PICO-V3-02: uses GPIO 9 and 10 for PSRAM; 7, 8 are free + } else { + // for classic ESP32 (non-mini) modules, these are the SPI flash pins + if (gpio > 5 && gpio < 12) return false; //SPI flash pins + } + if (gpio == 16) return !psramFound(); // PSRAM pins on modules with off-package or in-package PSRAM + if (gpio == 17) { + if (strncmp_P(PSTR("ESP32-D0WDR2-V3"), ESP.getChipModel(), 15) == 0) { + return true; + } else { + return !psramFound(); // PSRAM pins on modules with in-package PSRAM + } + } #endif if (output) return digitalPinCanOutput(gpio); else return true; @@ -279,12 +316,65 @@ void PinManager::deallocateLedc(byte pos, byte channels) } #endif -#ifdef ESP8266 -uint32_t PinManager::pinAlloc = 0UL; -#else -uint64_t PinManager::pinAlloc = 0ULL; -uint16_t PinManager::ledcAlloc = 0; -#endif -uint8_t PinManager::i2cAllocCount = 0; -uint8_t PinManager::spiAllocCount = 0; -PinOwner PinManager::ownerTag[WLED_NUM_PINS] = { PinOwner::None }; +// Convert PinOwner enum to string for allocated pins +const char* PinManager::getPinOwnerName(uint8_t gpio) { + PinOwner owner = PinManager::getPinOwner(gpio); // returns "none" if allocated by system, unallocated or unavailable + switch (owner) { + case PinOwner::None: return PinManager::isPinAllocated(gpio) ? "System" : "Unknown"; + case PinOwner::Ethernet: return "Ethernet"; + case PinOwner::BusDigital: return "LED Digital"; + case PinOwner::BusOnOff: return "LED On/Off"; + case PinOwner::BusPwm: return "LED PWM"; + case PinOwner::Button: return "Button"; + case PinOwner::IR: return "IR Receiver"; + case PinOwner::Relay: return "Relay"; + case PinOwner::SPI_RAM: return "SPI RAM"; + case PinOwner::DebugOut: return "Debug"; + case PinOwner::DMX: return "DMX Output"; + case PinOwner::HW_I2C: return "I2C"; + case PinOwner::HW_SPI: return "SPI"; + case PinOwner::DMX_INPUT: return "DMX Input"; + case PinOwner::HUB75: return "HUB75"; + // Usermods - return generic name for now + // TODO: Get actual usermod name from UsermodManager + default: + // Check if it's a usermod (high bit not set) + if (static_cast(owner) > 0 && !(static_cast(owner) & 0x80)) { + return "Usermod"; + } + return "Unknown"; + } +} + +int PinManager::getButtonIndex(byte gpio) { + for (size_t b = 0; b < buttons.size(); b++) { + if (buttons[b].pin == gpio && buttons[b].type != BTN_TYPE_NONE) { + return b; + } + } + return -1; +} + +bool PinManager::isAnalogPin(byte gpio) { + #ifdef ARDUINO_ARCH_ESP32 + // Check ADC capability: only ADC1 channels can be used (ADC2 channels are not usable when WiFi is active) + #if CONFIG_IDF_TARGET_ESP32 + // ESP32: ADC1 channels 0-7 (GPIO 36, 37, 38, 39, 32, 33, 34, 35) + int adc_channel = digitalPinToAnalogChannel(gpio); + if (adc_channel >= 0 && adc_channel <= 7) return true; + #elif CONFIG_IDF_TARGET_ESP32S2 + // ESP32-S2: ADC1 channels 0-9 (GPIO 1-10) + int adc_channel = digitalPinToAnalogChannel(gpio); + if (adc_channel >= 0 && adc_channel <= 9) return true; + #elif CONFIG_IDF_TARGET_ESP32S3 + // ESP32-S3: ADC1 channels 0-9 (GPIO 1-10) + int adc_channel = digitalPinToAnalogChannel(gpio); + if (adc_channel >= 0 && adc_channel <= 9) return true; + #elif CONFIG_IDF_TARGET_ESP32C3 + // ESP32-C3: ADC1 channels 0-4 (GPIO 0-4) + int adc_channel = digitalPinToAnalogChannel(gpio); + if (adc_channel >= 0 && adc_channel <= 4) return true; + #endif + #endif + return false; // not an analog pin if it doesn't have ADC capability, ESP8266 has only one ADC pin (A0) which is handled separately in button.cpp, so return false for all pins here +} diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 73a4a36564..5f774bb47d 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -3,11 +3,19 @@ /* * Registers pins so there is no attempt for two interfaces to use the same pin */ -#include -#ifdef ARDUINO_ARCH_ESP32 -#include "driver/ledc.h" // needed for analog/LEDC channel counts + +#ifdef ESP8266 +#define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) +#else +#define WLED_NUM_PINS (GPIO_PIN_COUNT) #endif -#include "const.h" // for USERMOD_* values + +// Pin capability flags - only "special" capabilities useful for debugging (note: touch capability is provided by appendGPIOinfo() via d.touch) +#define PIN_CAP_ADC 0x02 // has ADC capability (analog input) +#define PIN_CAP_PWM 0x04 // can be used for PWM (analog LED output) -> unused, all pins can use ledc PWM +#define PIN_CAP_BOOT 0x08 // bootloader pin +#define PIN_CAP_BOOTSTRAP 0x10 // bootstrap pin (strapping pin affecting boot mode) +#define PIN_CAP_INPUT_ONLY 0x20 // input only pin (cannot be used as output) typedef struct PinManagerPinType { int8_t pin; @@ -29,15 +37,17 @@ enum struct PinOwner : uint8_t { Ethernet = 0x81, BusDigital = 0x82, BusOnOff = 0x83, - BusPwm = 0x84, // 'BusP' == PWM output using BusPwm - Button = 0x85, // 'Butn' == button from configuration - IR = 0x86, // 'IR' == IR receiver pin from configuration - Relay = 0x87, // 'Rly' == Relay pin from configuration - SPI_RAM = 0x88, // 'SpiR' == SPI RAM - DebugOut = 0x89, // 'Dbg' == debug output always IO1 - DMX = 0x8A, // 'DMX' == hard-coded to IO2 - HW_I2C = 0x8B, // 'I2C' == hardware I2C pins (4&5 on ESP8266, 21&22 on ESP32) - HW_SPI = 0x8C, // 'SPI' == hardware (V)SPI pins (13,14&15 on ESP8266, 5,18&23 on ESP32) + BusPwm = 0x84, // 'BusP' == PWM output using BusPwm + Button = 0x85, // 'Butn' == button from configuration + IR = 0x86, // 'IR' == IR receiver pin from configuration + Relay = 0x87, // 'Rly' == Relay pin from configuration + SPI_RAM = 0x88, // 'SpiR' == SPI RAM + DebugOut = 0x89, // 'Dbg' == debug output always IO1 + DMX = 0x8A, // 'DMX' == hard-coded to IO2 + HW_I2C = 0x8B, // 'I2C' == hardware I2C pins (4&5 on ESP8266, 21&22 on ESP32) + HW_SPI = 0x8C, // 'SPI' == hardware (V)SPI pins (13,14&15 on ESP8266, 5,18&23 on ESP32) + DMX_INPUT = 0x8D, // 'DMX_INPUT' == DMX input via serial + HUB75 = 0x8E, // 'Hub75' == Hub75 driver // Use UserMod IDs from const.h here UM_Unspecified = USERMOD_ID_UNSPECIFIED, // 0x01 UM_Example = USERMOD_ID_EXAMPLE, // 0x02 // Usermod "usermod_v2_example.h" @@ -70,53 +80,43 @@ enum struct PinOwner : uint8_t { }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); -class PinManager { - private: - #ifdef ESP8266 - #define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) - static uint32_t pinAlloc; // 1 bit per pin, we use first 17bits - #else - #define WLED_NUM_PINS (GPIO_PIN_COUNT) - static uint64_t pinAlloc; // 1 bit per pin, we use 50 bits on ESP32-S3 - static uint16_t ledcAlloc; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) - #endif - static uint8_t i2cAllocCount; // allow multiple allocation of I2C bus pins but keep track of allocations - static uint8_t spiAllocCount; // allow multiple allocation of SPI bus pins but keep track of allocations - static PinOwner ownerTag[WLED_NUM_PINS]; +namespace PinManager { + // De-allocates a single pin + bool deallocatePin(byte gpio, PinOwner tag); + // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) + bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag); + bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag); + // Allocates a single pin, with an owner tag. + // De-allocation requires the same owner tag (or override) + bool allocatePin(byte gpio, bool output, PinOwner tag); + // Allocates all the pins, or allocates none of the pins, with owner tag. + // Provided to simplify error condition handling in clients + // using more than one pin, such as I2C, SPI, rotary encoders, + // ethernet, etc.. + bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); + bool allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output); - public: - // De-allocates a single pin - static bool deallocatePin(byte gpio, PinOwner tag); - // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) - static bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag); - static bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag); - // Allocates a single pin, with an owner tag. - // De-allocation requires the same owner tag (or override) - static bool allocatePin(byte gpio, bool output, PinOwner tag); - // Allocates all the pins, or allocates none of the pins, with owner tag. - // Provided to simplify error condition handling in clients - // using more than one pin, such as I2C, SPI, rotary encoders, - // ethernet, etc.. - static bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); + [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] + inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } + [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] + inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } - [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] - static inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } - [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] - static inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } + // will return true for reserved pins + bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None); + // will return false for reserved pins + bool isPinOk(byte gpio, bool output = true); - // will return true for reserved pins - static bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None); - // will return false for reserved pins - static bool isPinOk(byte gpio, bool output = true); - - static bool isReadOnlyPin(byte gpio); + bool isReadOnlyPin(byte gpio); + int getButtonIndex(byte gpio); // returns button index if pin is used for button, otherwise -1 + bool isAnalogPin(byte gpio); // returns true if pin has ADC capability, otherwise false - static PinOwner getPinOwner(byte gpio); + PinOwner getPinOwner(byte gpio); + const char* getPinOwnerName(uint8_t gpio); - #ifdef ARDUINO_ARCH_ESP32 - static byte allocateLedc(byte channels); - static void deallocateLedc(byte pos, byte channels); - #endif + #ifdef ARDUINO_ARCH_ESP32 + byte allocateLedc(byte channels); + void deallocateLedc(byte pos, byte channels); + #endif }; //extern PinManager pinManager; diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index 36235ab9ea..253c980b6e 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -10,19 +10,19 @@ typedef struct PlaylistEntry { uint16_t tr; //Duration of the transition TO this entry (in tenths of seconds) } ple; -byte playlistRepeat = 1; //how many times to repeat the playlist (0 = infinitely) -byte playlistEndPreset = 0; //what preset to apply after playlist end (0 = stay on last preset) -byte playlistOptions = 0; //bit 0: shuffle playlist after each iteration. bits 1-7 TBD +static byte playlistRepeat = 1; //how many times to repeat the playlist (0 = infinitely) +static byte playlistEndPreset = 0; //what preset to apply after playlist end (0 = stay on last preset) +static byte playlistOptions = 0; //bit 0: shuffle playlist after each iteration. bits 1-7 TBD -PlaylistEntry *playlistEntries = nullptr; -byte playlistLen; //number of playlist entries -int8_t playlistIndex = -1; -uint16_t playlistEntryDur = 0; //duration of the current entry in tenths of seconds +static PlaylistEntry *playlistEntries = nullptr; +static byte playlistLen; //number of playlist entries +static int8_t playlistIndex = -1; +static uint16_t playlistEntryDur = 0; //duration of the current entry in tenths of seconds //values we need to keep about the parent playlist while inside sub-playlist -//int8_t parentPlaylistIndex = -1; -//byte parentPlaylistRepeat = 0; -//byte parentPlaylistPresetId = 0; //for re-loading +static int16_t parentPlaylistIndex = -1; +static byte parentPlaylistRepeat = 0; +static byte parentPlaylistPresetId = 0; //for re-loading void shufflePlaylist() { @@ -54,6 +54,12 @@ void unloadPlaylist() { int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { + if (currentPlaylist > 0 && parentPlaylistPresetId > 0) return -1; // we are already in nested playlist, do nothing + if (currentPlaylist > 0) { + parentPlaylistIndex = playlistIndex; + parentPlaylistRepeat = playlistRepeat; + parentPlaylistPresetId = currentPlaylist; + } unloadPlaylist(); JsonArray presets = playlistObj["ps"]; @@ -61,7 +67,7 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { if (playlistLen == 0) return -1; if (playlistLen > 100) playlistLen = 100; - playlistEntries = new PlaylistEntry[playlistLen]; + playlistEntries = new(std::nothrow) PlaylistEntry[playlistLen]; if (playlistEntries == nullptr) return -1; byte it = 0; @@ -79,11 +85,12 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { } else { for (int dur : durations) { if (it >= playlistLen) break; - playlistEntries[it].dur = (dur > 1) ? dur : 100; + playlistEntries[it].dur = constrain(dur, 0, 65530); it++; } } - for (int i = it; i < playlistLen; i++) playlistEntries[i].dur = playlistEntries[it -1].dur; + if (it > 0) // should never happen but just in case + for (int i = it; i < playlistLen; i++) playlistEntries[i].dur = playlistEntries[it -1].dur; it = 0; JsonArray tr = playlistObj[F("transition")]; @@ -117,6 +124,19 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { shuffle = shuffle || playlistObj["r"]; if (shuffle) playlistOptions |= PL_OPTION_SHUFFLE; + if (parentPlaylistPresetId == 0 && parentPlaylistIndex > -1) { + // we are re-loading playlist when returning from nested playlist + playlistIndex = parentPlaylistIndex; + playlistRepeat = parentPlaylistRepeat; + parentPlaylistIndex = -1; + parentPlaylistRepeat = 0; + } else if (rep == 0) { + // endless playlist will never return to parent so erase parent information if it was called from it + parentPlaylistPresetId = 0; + parentPlaylistIndex = -1; + parentPlaylistRepeat = 0; + } + currentPlaylist = presetId; DEBUG_PRINTLN(F("Playlist loaded.")); return currentPlaylist; @@ -127,7 +147,7 @@ void handlePlaylist() { static unsigned long presetCycledTime = 0; if (currentPlaylist < 0 || playlistEntries == nullptr) return; -if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) { + if ((playlistEntryDur < UINT16_MAX && millis() - presetCycledTime > 100 * playlistEntryDur) || doAdvancePlaylist) { presetCycledTime = millis(); if (bri == 0 || nightlightActive) return; @@ -137,7 +157,10 @@ if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) if (!playlistIndex) { if (playlistRepeat == 1) { //stop if all repetitions are done unloadPlaylist(); - if (playlistEndPreset) applyPresetFromPlaylist(playlistEndPreset); + if (parentPlaylistPresetId > 0) { + applyPresetFromPlaylist(parentPlaylistPresetId); // reload previous playlist (unfortunately asynchronous) + parentPlaylistPresetId = 0; // reset previous playlist but do not reset Index or Repeat (they will be loaded & reset in loadPlaylist()) + } else if (playlistEndPreset) applyPresetFromPlaylist(playlistEndPreset); return; } if (playlistRepeat > 1) playlistRepeat--; // decrease repeat count on each index reset if not an endless playlist @@ -146,8 +169,8 @@ if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) } jsonTransitionOnce = true; - strip.setTransition(fadeTransition ? playlistEntries[playlistIndex].tr * 100 : 0); - playlistEntryDur = playlistEntries[playlistIndex].dur; + strip.setTransition(playlistEntries[playlistIndex].tr * 100); + playlistEntryDur = playlistEntries[playlistIndex].dur > 0 ? playlistEntries[playlistIndex].dur : UINT16_MAX; applyPresetFromPlaylist(playlistEntries[playlistIndex].preset); doAdvancePlaylist = false; } diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 04474113d1..9023baf344 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -22,12 +22,17 @@ const char *getPresetsFileName(bool persistent) { return persistent ? presets_json : tmp_json; } +bool presetNeedsSaving() { + return presetToSave; +} + static void doSaveState() { bool persist = (presetToSave < 251); - unsigned long start = millis(); - while (strip.isUpdating() && millis()-start < (2*FRAMETIME_FIXED)+1) yield(); // wait 2 frames - if (!requestJSONBufferLock(10)) return; + unsigned long maxWait = millis() + strip.getFrameTime(); + while (strip.isUpdating() && millis() < maxWait) delay(1); // wait for strip to finish updating, accessing FS during sendout causes glitches + + if (!requestJSONBufferLock(JSON_LOCK_PRESET_SAVE)) return; initPresetsFile(); // just in case if someone deleted presets.json using /edit JsonObject sObj = pDoc->to(); @@ -52,14 +57,10 @@ static void doSaveState() { */ #if defined(ARDUINO_ARCH_ESP32) if (!persist) { - if (tmpRAMbuffer!=nullptr) free(tmpRAMbuffer); + p_free(tmpRAMbuffer); size_t len = measureJson(*pDoc) + 1; - DEBUG_PRINTLN(len); // if possible use SPI RAM on ESP32 - if (psramSafe && psramFound()) - tmpRAMbuffer = (char*) ps_malloc(len); - else - tmpRAMbuffer = (char*) malloc(len); + tmpRAMbuffer = (char*)p_malloc(len); if (tmpRAMbuffer!=nullptr) { serializeJson(*pDoc, tmpRAMbuffer, len); } else { @@ -76,8 +77,8 @@ static void doSaveState() { // clean up saveLedmap = -1; presetToSave = 0; - delete[] saveName; - delete[] quickLoad; + p_free(saveName); + p_free(quickLoad); saveName = nullptr; quickLoad = nullptr; playlistSave = false; @@ -85,7 +86,7 @@ static void doSaveState() { bool getPresetName(byte index, String& name) { - if (!requestJSONBufferLock(19)) return false; + if (!requestJSONBufferLock(JSON_LOCK_PRESET_NAME)) return false; bool presetExists = false; if (readObjectFromFileUsingId(getPresetsFileName(), index, pDoc)) { JsonObject fdo = pDoc->as(); @@ -151,7 +152,7 @@ void handlePresets() return; } - if (presetToApply == 0 || !requestJSONBufferLock(9)) return; // no preset waiting to apply, or JSON buffer is already allocated, return to loop until free + if (presetToApply == 0 || !requestJSONBufferLock(JSON_LOCK_PRESET_LOAD)) return; // no preset waiting to apply, or JSON buffer is already allocated, return to loop until free bool changePreset = false; uint8_t tmpPreset = presetToApply; // store temporary since deserializeState() may call applyPreset() @@ -164,6 +165,11 @@ void handlePresets() DEBUG_PRINTF_P(PSTR("Applying preset: %u\n"), (unsigned)tmpPreset); + #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) + unsigned long maxWait = millis() + strip.getFrameTime(); + while (strip.isUpdating() && millis() < maxWait) delay(1); // wait for strip to finish updating, accessing FS during sendout causes glitches + #endif + #ifdef ARDUINO_ARCH_ESP32 if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { deserializeJson(*pDoc,tmpRAMbuffer); @@ -197,7 +203,7 @@ void handlePresets() #if defined(ARDUINO_ARCH_ESP32) //Aircoookie recommended not to delete buffer if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { - free(tmpRAMbuffer); + p_free(tmpRAMbuffer); tmpRAMbuffer = nullptr; } #endif @@ -211,8 +217,8 @@ void handlePresets() //called from handleSet(PS=) [network callback (sObj is empty), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] void savePreset(byte index, const char* pname, JsonObject sObj) { - if (!saveName) saveName = new char[33]; - if (!quickLoad) quickLoad = new char[9]; + if (!saveName) saveName = static_cast(p_malloc(33)); + if (!quickLoad) quickLoad = static_cast(p_malloc(9)); if (!saveName || !quickLoad) return; if (index == 0 || (index > 250 && index < 255)) return; @@ -233,7 +239,7 @@ void savePreset(byte index, const char* pname, JsonObject sObj) if (!sObj[FPSTR(bootPS)].isNull()) { bootPreset = sObj[FPSTR(bootPS)] | bootPreset; sObj.remove(FPSTR(bootPS)); - doSerializeConfig = true; + configNeedsWrite = true; } if (sObj.size()==0 || sObj["o"].isNull()) { // no "o" means not a playlist or custom API call, saving of state is async (not immediately) @@ -258,13 +264,13 @@ void savePreset(byte index, const char* pname, JsonObject sObj) presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } - delete[] saveName; - delete[] quickLoad; + p_free(saveName); + p_free(quickLoad); saveName = nullptr; quickLoad = nullptr; } else { // store playlist - // WARNING: playlist will be loaded in json.cpp after this call and will have repeat counter increased by 1 + // WARNING: playlist will be loaded in json.cpp after this call and will have repeat counter increased by 1 it will also be randomised if selected includeBri = true; // !sObj["on"].isNull(); playlistSave = true; } diff --git a/wled00/remote.cpp b/wled00/remote.cpp index 9bc5430c01..7b3375fa66 100644 --- a/wled00/remote.cpp +++ b/wled00/remote.cpp @@ -1,6 +1,8 @@ #include "wled.h" #ifndef WLED_DISABLE_ESPNOW +#define ESPNOW_BUSWAIT_TIMEOUT 24 // one frame timeout to wait for bus to finish updating + #define NIGHT_MODE_DEACTIVATED -1 #define NIGHT_MODE_BRIGHTNESS 5 @@ -11,6 +13,9 @@ #define WIZMOTE_BUTTON_TWO 17 #define WIZMOTE_BUTTON_THREE 18 #define WIZMOTE_BUTTON_FOUR 19 +#define WIZMOTE_BUTTON_FIVE 20 +#define WIZMOTE_BUTTON_SIX 21 +#define WIZMOTE_BUTTON_SEVEN 22 #define WIZMOTE_BUTTON_BRIGHT_UP 9 #define WIZMOTE_BUTTON_BRIGHT_DOWN 8 @@ -25,7 +30,7 @@ typedef struct WizMoteMessageStructure { uint8_t program; // 0x91 for ON button, 0x81 for all others uint8_t seq[4]; // Incremetal sequence number 32 bit unsigned integer LSB first - uint8_t dt1; // Button Data Type (0x32) + uint8_t dt1; // Button Data Type (0x20) uint8_t button; // Identifies which button is being pressed uint8_t dt2; // Battery Level Data Type (0x01) uint8_t batLevel; // Battery Level 0-100 @@ -38,6 +43,7 @@ typedef struct WizMoteMessageStructure { static uint32_t last_seq = UINT32_MAX; static int brightnessBeforeNightMode = NIGHT_MODE_DEACTIVATED; +static int16_t ESPNowButton = -1; // set in callback if new button value is received // Pulled from the IR Remote logic but reduced to 10 steps with a constant of 3 static const byte brightnessSteps[] = { @@ -106,7 +112,7 @@ static void setOff() { } } -void presetWithFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { +static void presetWithFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { resetNightMode(); applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID); } @@ -117,10 +123,13 @@ static bool remoteJson(int button) char objKey[10]; bool parsed = false; - if (!requestJSONBufferLock(22)) return false; + if (!requestJSONBufferLock(JSON_LOCK_REMOTE)) return false; sprintf_P(objKey, PSTR("\"%d\":"), button); + unsigned long start = millis(); + while (strip.isUpdating() && millis()-start < ESPNOW_BUSWAIT_TIMEOUT) yield(); // wait for strip to finish updating, accessing FS during sendout causes glitches + // attempt to read command from remote.json readObjectFromFile(PSTR("/remote.json"), objKey, pDoc); JsonObject fdo = pDoc->as(); @@ -146,7 +155,7 @@ static bool remoteJson(int button) parsed = true; } else if (cmdStr.startsWith(F("!presetF"))) { //!presetFallback uint8_t p1 = fdo["PL"] | 1; - uint8_t p2 = fdo["FX"] | random8(strip.getModeCount() -1); + uint8_t p2 = fdo["FX"] | hw_random8(strip.getModeCount() -1); uint8_t p3 = fdo["FP"] | 0; presetWithFallback(p1, p2, p3); parsed = true; @@ -175,16 +184,10 @@ static bool remoteJson(int button) return parsed; } -// Callback function that will be executed when data is received -void handleRemote(uint8_t *incomingData, size_t len) { +// Callback function that will be executed when data is received from a linked remote +void handleWiZdata(uint8_t *incomingData, size_t len) { message_structure_t *incoming = reinterpret_cast(incomingData); - if (strcmp(last_signal_src, linked_remote) != 0) { - DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: ")); - DEBUG_PRINTLN(last_signal_src); - return; - } - if (len != sizeof(message_structure_t)) { DEBUG_PRINTF_P(PSTR("Unknown incoming ESP Now message received of length %u\n"), len); return; @@ -202,14 +205,24 @@ void handleRemote(uint8_t *incomingData, size_t len) { DEBUG_PRINT(F("] button: ")); DEBUG_PRINTLN(incoming->button); - if (!remoteJson(incoming->button)) - switch (incoming->button) { + ESPNowButton = incoming->button; // save state, do not process in callback (can cause glitches) + last_seq = cur_seq; +} + +// process ESPNow button data (acesses FS, should not be called while update to avoid glitches) +void handleRemote() { + if(ESPNowButton >= 0) { + if (!remoteJson(ESPNowButton)) + switch (ESPNowButton) { case WIZMOTE_BUTTON_ON : setOn(); break; case WIZMOTE_BUTTON_OFF : setOff(); break; case WIZMOTE_BUTTON_ONE : presetWithFallback(1, FX_MODE_STATIC, 0); break; case WIZMOTE_BUTTON_TWO : presetWithFallback(2, FX_MODE_BREATH, 0); break; case WIZMOTE_BUTTON_THREE : presetWithFallback(3, FX_MODE_FIRE_FLICKER, 0); break; case WIZMOTE_BUTTON_FOUR : presetWithFallback(4, FX_MODE_RAINBOW, 0); break; + case WIZMOTE_BUTTON_FIVE : presetWithFallback(5, FX_MODE_CANDLE, 0); break; + case WIZMOTE_BUTTON_SIX : presetWithFallback(6, FX_MODE_RANDOM_COLOR, 0); break; + case WIZMOTE_BUTTON_SEVEN : presetWithFallback(7, FX_MODE_FADE, 0); break; case WIZMOTE_BUTTON_NIGHT : activateNightMode(); break; case WIZMOTE_BUTTON_BRIGHT_UP : brightnessUp(); break; case WIZMOTE_BUTTON_BRIGHT_DOWN : brightnessDown(); break; @@ -219,9 +232,10 @@ void handleRemote(uint8_t *incomingData, size_t len) { case WIZ_SMART_BUTTON_BRIGHT_DOWN : brightnessDown(); break; default: break; } - last_seq = cur_seq; + } + ESPNowButton = -1; } #else -void handleRemote(uint8_t *incomingData, size_t len) {} +void handleRemote() {} #endif diff --git a/wled00/set.cpp b/wled00/set.cpp index 2a6fdd3fb0..3c6c72b7b3 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -23,22 +23,33 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) for (size_t n = 0; n < WLED_MAX_WIFI_COUNT; n++) { char cs[4] = "CS"; cs[2] = 48+n; cs[3] = 0; //client SSID char pw[4] = "PW"; pw[2] = 48+n; pw[3] = 0; //client password + char bs[4] = "BS"; bs[2] = 48+n; bs[3] = 0; //BSSID char ip[5] = "IP"; ip[2] = 48+n; ip[4] = 0; //IP address char gw[5] = "GW"; gw[2] = 48+n; gw[4] = 0; //GW address char sn[5] = "SN"; sn[2] = 48+n; sn[4] = 0; //subnet mask +#ifdef WLED_ENABLE_WPA_ENTERPRISE + char et[4] = "ET"; et[2] = 48+n; et[3] = 0; //WiFi encryption type + char ea[4] = "EA"; ea[2] = 48+n; ea[3] = 0; //enterprise anonymous identity + char ei[4] = "EI"; ei[2] = 48+n; ei[3] = 0; //enterprise identity +#endif if (request->hasArg(cs)) { - if (n >= multiWiFi.size()) multiWiFi.push_back(WiFiConfig()); // expand vector by one + if (n >= multiWiFi.size()) multiWiFi.emplace_back(); // expand vector by one char oldSSID[33]; strcpy(oldSSID, multiWiFi[n].clientSSID); char oldPass[65]; strcpy(oldPass, multiWiFi[n].clientPass); + uint8_t oldBSSID[6]; memcpy(oldBSSID, multiWiFi[n].bssid, 6); // save old BSSID strlcpy(multiWiFi[n].clientSSID, request->arg(cs).c_str(), 33); - if (strlen(oldSSID) == 0 || !strncmp(multiWiFi[n].clientSSID, oldSSID, 32)) { + if (strlen(oldSSID) == 0 || strncmp(multiWiFi[n].clientSSID, oldSSID, 32) != 0) { forceReconnect = true; } if (!isAsterisksOnly(request->arg(pw).c_str(), 65)) { strlcpy(multiWiFi[n].clientPass, request->arg(pw).c_str(), 65); forceReconnect = true; } + fillStr2MAC(multiWiFi[n].bssid, request->arg(bs).c_str()); + if (memcmp(oldBSSID, multiWiFi[n].bssid, 6) != 0) { // check if BSSID changed + forceReconnect = true; + } for (size_t i = 0; i < 4; i++) { ip[3] = 48+i; gw[3] = 48+i; @@ -47,6 +58,34 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) multiWiFi[n].staticGW[i] = request->arg(gw).toInt(); multiWiFi[n].staticSN[i] = request->arg(sn).toInt(); } + +#ifdef WLED_ENABLE_WPA_ENTERPRISE + byte oldType = multiWiFi[n].encryptionType; + char oldAnon[65]; strcpy(oldAnon, multiWiFi[n].enterpriseAnonIdentity); + char oldIden[65]; strcpy(oldIden, multiWiFi[n].enterpriseIdentity); + if (request->hasArg(et) && request->hasArg(ea) && request->hasArg(ei)) { + multiWiFi[n].encryptionType = request->arg(et).toInt(); + strlcpy(multiWiFi[n].enterpriseAnonIdentity, request->arg(ea).c_str(), 65); + strlcpy(multiWiFi[n].enterpriseIdentity, request->arg(ei).c_str(), 65); + } else { + // No enterprise settings provided, default to PSK + multiWiFi[n].encryptionType = WIFI_ENCRYPTION_TYPE_PSK; + } + + if (multiWiFi[n].encryptionType == WIFI_ENCRYPTION_TYPE_PSK) { + // PSK - Clear the anonymous identity and identity fields + multiWiFi[n].enterpriseAnonIdentity[0] = '\0'; + multiWiFi[n].enterpriseIdentity[0] = '\0'; + } + forceReconnect |= oldType != multiWiFi[n].encryptionType; + if (strncmp(multiWiFi[n].enterpriseAnonIdentity, oldAnon, 64) != 0) { + forceReconnect = true; + } + if (strncmp(multiWiFi[n].enterpriseIdentity, oldIden, 64) != 0) { + forceReconnect = true; + } +#endif + cnt++; } } @@ -89,13 +128,26 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) bool oldESPNow = enableESPNow; enableESPNow = request->hasArg(F("RE")); if (oldESPNow != enableESPNow) forceReconnect = true; - strlcpy(linked_remote, request->arg(F("RMAC")).c_str(), 13); - strlwr(linked_remote); //Normalize MAC format to lowercase + linked_remotes.clear(); // clear old remotes + for (size_t n = 0; n < 10; n++) { + char rm[4]; + snprintf(rm, sizeof(rm), "RM%d", n); // "RM0" to "RM9" + if (request->hasArg(rm)) { + const String& arg = request->arg(rm); + if (arg.isEmpty()) continue; + std::array mac{}; + strlcpy(mac.data(), request->arg(rm).c_str(), 13); + strlwr(mac.data()); + if (mac[0] != '\0') { + linked_remotes.emplace_back(mac); + } + } + } #endif - #ifdef WLED_USE_ETHERNET + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) ethernetType = request->arg(F("ETH")).toInt(); - WLED::instance().initEthernet(); + initEthernet(); #endif } @@ -113,20 +165,22 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) PinManager::deallocatePin(irPin, PinOwner::IR); } #endif - for (unsigned s=0; s=0 && PinManager::isPinAllocated(btnPin[s], PinOwner::Button)) { - PinManager::deallocatePin(btnPin[s], PinOwner::Button); + for (const auto &button : buttons) { + if (button.pin >= 0 && PinManager::isPinAllocated(button.pin, PinOwner::Button)) { + PinManager::deallocatePin(button.pin, PinOwner::Button); #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt - if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin - touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing + if (digitalPinToTouchChannel(button.pin) >= 0) // if touch capable pin + touchDetachInterrupt(button.pin); // if not assigned previously, this will do nothing #endif } } - unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed; + unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed, driverType; unsigned length, start, maMax; - uint8_t pins[5] = {255, 255, 255, 255, 255}; + uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; + String text; + // this will set global ABL max current used when per-port ABL is not used unsigned ablMilliampsMax = request->arg(F("MA")).toInt(); BusManager::setMilliampsMax(ablMilliampsMax); @@ -134,15 +188,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) strip.correctWB = request->hasArg(F("CCT")); strip.cctFromRgb = request->hasArg(F("CR")); cctICused = request->hasArg(F("IC")); - strip.cctBlending = request->arg(F("CB")).toInt(); - Bus::setCCTBlend(strip.cctBlending); + uint8_t cctBlending = request->arg(F("CB")).toInt(); + Bus::setCCTBlend(cctBlending); Bus::setGlobalAWMode(request->arg(F("AW")).toInt()); strip.setTargetFps(request->arg(F("FR")).toInt()); - useGlobalLedBuffer = request->hasArg(F("LD")); bool busesChanged = false; - for (int s = 0; s < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; s++) { - int offset = s < 10 ? 48 : 55; + for (int s = 0; s < 36; s++) { // theoretical limit is 36 : "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + int offset = s < 10 ? '0' : 'A' - 10; char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin char lc[4] = "LC"; lc[2] = offset+s; lc[3] = 0; //strip length char co[4] = "CO"; co[2] = offset+s; co[3] = 0; //strip color order @@ -156,12 +209,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM) char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED mA char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max mA + char ld[4] = "LD"; ld[2] = offset+s; ld[3] = 0; //driver type (RMT=0, I2S=1) + char hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others) if (!request->hasArg(lp)) { - DEBUG_PRINTF_P(PSTR("No data for %d\n"), s); + DEBUG_PRINTF_P(PSTR("# of buses: %d\n"), s+1); break; } for (int i = 0; i < 5; i++) { - lp[1] = offset+i; + lp[1] = '0'+i; if (!request->hasArg(lp)) break; pins[i] = (request->arg(lp).length() > 0) ? request->arg(lp).toInt() : 255; } @@ -203,13 +258,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) maMax = 0; } else { maPerLed = request->arg(la).toInt(); - maMax = request->arg(ma).toInt(); // if ABL is disabled this will be 0 + maMax = request->arg(ma).toInt() * request->hasArg(F("PPL")); // if PP-ABL is disabled maMax (per bus) must be 0 } type |= request->hasArg(rf) << 7; // off refresh override + driverType = request->arg(ld).toInt(); // 0=RMT (default), 1=I2S + text = request->arg(hs).substring(0,31); // actual finalization is done in WLED::loop() (removing old busses and adding new) // this may happen even before this loop is finished so we do "doInitBusses" after the loop - if (busConfigs[s] != nullptr) delete busConfigs[s]; - busConfigs[s] = new BusConfig(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, useGlobalLedBuffer, maPerLed, maMax); + busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text); busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed @@ -217,7 +273,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // we will not bother with pre-allocating ColorOrderMappings vector BusManager::getColorOrderMap().reset(); for (int s = 0; s < WLED_MAX_COLOR_ORDER_MAPPINGS; s++) { - int offset = s < 10 ? 48 : 55; + int offset = s < 10 ? '0' : 'A' - 10; char xs[4] = "XS"; xs[2] = offset+s; xs[3] = 0; //start LED char xc[4] = "XC"; xc[2] = offset+s; xc[3] = 0; //strip length char xo[4] = "XO"; xo[2] = offset+s; xo[3] = 0; //color order @@ -256,58 +312,60 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) disablePullUp = (bool)request->hasArg(F("IP")); touchThreshold = request->arg(F("TT")).toInt(); for (int i = 0; i < WLED_MAX_BUTTONS; i++) { - int offset = i < 10 ? 48 : 55; + int offset = i < 10 ? '0' : 'A' - 10; char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10) char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10) - int hw_btn_pin = request->arg(bt).toInt(); - if (hw_btn_pin >= 0 && PinManager::allocatePin(hw_btn_pin,false,PinOwner::Button)) { - btnPin[i] = hw_btn_pin; - buttonType[i] = request->arg(be).toInt(); - #ifdef ARDUINO_ARCH_ESP32 + int hw_btn_pin = request->hasArg(bt) ? request->arg(bt).toInt() : -1; + if (i >= buttons.size()) buttons.emplace_back(hw_btn_pin, request->arg(be).toInt()); // add button to vector + else { + buttons[i].pin = hw_btn_pin; + buttons[i].type = request->arg(be).toInt(); + } + if (buttons[i].pin >= 0 && PinManager::allocatePin(buttons[i].pin, false, PinOwner::Button)) { + #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that button pin is a valid gpio - if ((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED)) - { - if (digitalPinToAnalogChannel(btnPin[i]) < 0) { + if ((buttons[i].type == BTN_TYPE_ANALOG) || (buttons[i].type == BTN_TYPE_ANALOG_INVERTED)) { + if (digitalPinToAnalogChannel(buttons[i].pin) < 0) { // not an ADC analog pin - DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i); - btnPin[i] = -1; - PinManager::deallocatePin(hw_btn_pin,PinOwner::Button); + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), buttons[i].pin, i); + PinManager::deallocatePin(buttons[i].pin, PinOwner::Button); + buttons[i].type = BTN_TYPE_NONE; } else { analogReadResolution(12); // see #4040 } - } - else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH)) - { - if (digitalPinToTouchChannel(btnPin[i]) < 0) - { + } else if ((buttons[i].type == BTN_TYPE_TOUCH || buttons[i].type == BTN_TYPE_TOUCH_SWITCH)) { + if (digitalPinToTouchChannel(buttons[i].pin) < 0) { // not a touch pin - DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i); - btnPin[i] = -1; - PinManager::deallocatePin(hw_btn_pin,PinOwner::Button); + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), buttons[i].pin, i); + PinManager::deallocatePin(buttons[i].pin, PinOwner::Button); + buttons[i].type = BTN_TYPE_NONE; } #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so - else - { - touchAttachInterrupt(btnPin[i], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) - } - #endif - } - else - #endif + else touchAttachInterrupt(buttons[i].pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) + #endif + } else + #endif { + // regular buttons and switches if (disablePullUp) { - pinMode(btnPin[i], INPUT); + pinMode(buttons[i].pin, INPUT); } else { #ifdef ESP32 - pinMode(btnPin[i], buttonType[i]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); + pinMode(buttons[i].pin, buttons[i].type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); #else - pinMode(btnPin[i], INPUT_PULLUP); + pinMode(buttons[i].pin, INPUT_PULLUP); #endif } } } else { - btnPin[i] = -1; - buttonType[i] = BTN_TYPE_NONE; + buttons[i].pin = -1; + buttons[i].type = BTN_TYPE_NONE; + } + } + // we should remove all unused buttons from the vector + for (int i = buttons.size()-1; i > 0; i--) { + if (buttons[i].pin < 0 && buttons[i].type == BTN_TYPE_NONE) { + buttons.erase(buttons.begin() + i); // remove button from vector } } @@ -319,19 +377,15 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) gammaCorrectBri = request->hasArg(F("GB")); gammaCorrectCol = request->hasArg(F("GC")); gammaCorrectVal = request->arg(F("GV")).toFloat(); - if (gammaCorrectVal > 1.0f && gammaCorrectVal <= 3) - NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); - else { + if (gammaCorrectVal < 0.1f || gammaCorrectVal > 3) { gammaCorrectVal = 1.0f; // no gamma correction gammaCorrectBri = false; gammaCorrectCol = false; } + NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables - fadeTransition = request->hasArg(F("TF")); - modeBlending = request->hasArg(F("EB")); t = request->arg(F("TD")).toInt(); if (t >= 0) transitionDelayDefault = t; - strip.paletteFade = request->hasArg(F("PF")); t = request->arg(F("TP")).toInt(); randomPaletteChangeTime = MIN(255,MAX(1,t)); useHarmonicRandomPalette = request->hasArg(F("TH")); @@ -343,7 +397,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) nightlightMode = request->arg(F("TW")).toInt(); t = request->arg(F("PB")).toInt(); - if (t >= 0 && t < 4) strip.paletteBlend = t; + if (t >= 0 && t < 4) paletteBlend = t; t = request->arg(F("BF")).toInt(); if (t > 0) briMultiplier = t; @@ -354,12 +408,11 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (subPage == SUBPAGE_UI) { strlcpy(serverDescription, request->arg(F("DS")).c_str(), 33); - //syncToggleReceive = request->hasArg(F("ST")); simplifiedUI = request->hasArg(F("SU")); DEBUG_PRINTLN(F("Enumerating ledmaps")); enumerateLedmaps(); DEBUG_PRINTLN(F("Loading custom palettes")); - strip.loadCustomPalettes(); // (re)load all custom palettes + loadCustomPalettes(); // (re)load all custom palettes } //SYNC @@ -421,6 +474,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) t = request->arg(F("WO")).toInt(); if (t >= -255 && t <= 255) arlsOffset = t; +#ifdef WLED_ENABLE_DMX_INPUT + dmxInputTransmitPin = request->arg(F("IDMT")).toInt(); + dmxInputReceivePin = request->arg(F("IDMR")).toInt(); + dmxInputEnablePin = request->arg(F("IDME")).toInt(); + dmxInputPort = request->arg(F("IDMP")).toInt(); + if(dmxInputPort <= 0 || dmxInputPort > 2) dmxInputPort = 2; +#endif + #ifndef WLED_DISABLE_ALEXA alexaEnabled = request->hasArg(F("AL")); strlcpy(alexaInvocationName, request->arg(F("AI")).c_str(), 33); @@ -507,14 +568,16 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) macroAlexaOff = request->arg(F("A1")).toInt(); macroCountdown = request->arg(F("MC")).toInt(); macroNl = request->arg(F("MN")).toInt(); - for (unsigned i=0; ihasArg(mp)) break; - macroButton[i] = request->arg(mp).toInt(); // these will default to 0 if not present - macroLongPress[i] = request->arg(ml).toInt(); - macroDoublePress[i] = request->arg(md).toInt(); + button.macroButton = request->arg(mp).toInt(); // these will default to 0 if not present + button.macroLongPress = request->arg(ml).toInt(); + button.macroDoublePress = request->arg(md).toInt(); + ii++; } char k[3]; k[2] = 0; @@ -548,9 +611,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (request->hasArg(F("RS"))) //complete factory reset { WLED_FS.format(); - #ifdef WLED_ADD_EEPROM_SUPPORT - clearEEPROM(); - #endif serveMessage(request, 200, F("All Settings erased."), F("Connect to WLED-AP to setup again"),255); doReboot = true; // may reboot immediately on dual-core system (race condition) which is desireable in this case } @@ -586,8 +646,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) { otaLock = request->hasArg(F("NO")); wifiLock = request->hasArg(F("OW")); + #ifndef WLED_DISABLE_OTA aOtaEnabled = request->hasArg(F("AO")); - //createEditHandler(correctPIN && !otaLock); + #endif + otaSameSubnet = request->hasArg(F("SU")); } } @@ -624,7 +686,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) //USERMODS if (subPage == SUBPAGE_UM) { - if (!requestJSONBufferLock(5)) return; + if (!requestJSONBufferLock(JSON_LOCK_SETTINGS)) { + request->deferResponse(); + return; + } // global I2C & SPI pins int8_t hw_sda_pin = !request->arg(F("SDA")).length() ? -1 : (int)request->arg(F("SDA")).toInt(); @@ -761,14 +826,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (subPage == SUBPAGE_2D) { strip.isMatrix = request->arg(F("SOMP")).toInt(); - strip.panel.clear(); // release memory if allocated + strip.panel.clear(); if (strip.isMatrix) { - strip.panels = MAX(1,MIN(WLED_MAX_PANELS,request->arg(F("MPC")).toInt())); - strip.panel.reserve(strip.panels); // pre-allocate memory - for (unsigned i=0; iarg(F("MPC")).toInt(), 1, WLED_MAX_PANELS); + strip.panel.reserve(panels); // pre-allocate memory + for (unsigned i=0; iarg(pO).toInt(); strip.panel.push_back(p); } - strip.setUpMatrix(); // will check limits - strip.makeAutoSegments(true); - strip.deserializeMap(); - } else { - Segment::maxWidth = strip.getLengthTotal(); - Segment::maxHeight = 1; } + strip.panel.shrink_to_fit(); // release unused memory + // we are changing matrix/ledmap geometry which *will* affect existing segments + // since we are not in loop() context we must make sure that effects are not running. credit @blazonchek for properly fixing #4911 + strip.suspend(); + strip.waitForIt(); + strip.deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) + strip.makeAutoSegments(true); // force re-creation of segments + strip.resume(); } #endif lastEditTime = millis(); // do not save if factory reset or LED settings (which are saved after LED re-init) - doSerializeConfig = subPage != SUBPAGE_LEDS && !(subPage == SUBPAGE_SEC && doReboot); - if (subPage == SUBPAGE_UM) doReboot = request->hasArg(F("RBT")); // prevent race condition on dual core system (set reboot here, after doSerializeConfig has been set) + configNeedsWrite = subPage != SUBPAGE_LEDS && !(subPage == SUBPAGE_SEC && doReboot); + if (subPage == SUBPAGE_UM) doReboot = request->hasArg(F("RBT")); // prevent race condition on dual core system (set reboot here, after configNeedsWrite has been set) #ifndef WLED_DISABLE_ALEXA if (subPage == SUBPAGE_SYNC) alexaInit(); #endif @@ -814,7 +881,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //segment select (sets main segment) pos = req.indexOf(F("SM=")); if (pos > 0 && !realtimeMode) { - strip.setMainSegmentId(getNumVal(&req, pos)); + strip.setMainSegmentId(getNumVal(req, pos)); } byte selectedSeg = strip.getFirstSelectedSegId(); @@ -823,7 +890,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SS=")); if (pos > 0) { - unsigned t = getNumVal(&req, pos); + unsigned t = getNumVal(req, pos); if (t < strip.getSegmentsNum()) { selectedSeg = t; singleSegment = true; @@ -833,14 +900,15 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) Segment& selseg = strip.getSegment(selectedSeg); pos = req.indexOf(F("SV=")); //segment selected if (pos > 0) { - unsigned t = getNumVal(&req, pos); + unsigned t = getNumVal(req, pos); if (t == 2) for (unsigned i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).selected = false; // unselect other segments selseg.selected = t; } // temporary values, write directly to segments, globals are updated by setValuesFromFirstSelectedSeg() - uint32_t col0 = selseg.colors[0]; - uint32_t col1 = selseg.colors[1]; + uint32_t col0 = selseg.colors[0]; + uint32_t col1 = selseg.colors[1]; + uint32_t col2 = selseg.colors[2]; byte colIn[4] = {R(col0), G(col0), B(col0), W(col0)}; byte colInSec[4] = {R(col1), G(col1), B(col1), W(col1)}; byte effectIn = selseg.mode; @@ -861,21 +929,23 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) uint16_t spcI = selseg.spacing; pos = req.indexOf(F("&S=")); //segment start if (pos > 0) { - startI = std::abs(getNumVal(&req, pos)); + startI = std::abs(getNumVal(req, pos)); } pos = req.indexOf(F("S2=")); //segment stop if (pos > 0) { - stopI = std::abs(getNumVal(&req, pos)); + stopI = std::abs(getNumVal(req, pos)); } pos = req.indexOf(F("GP=")); //segment grouping if (pos > 0) { - grpI = std::max(1,getNumVal(&req, pos)); + grpI = std::max(1,getNumVal(req, pos)); } pos = req.indexOf(F("SP=")); //segment spacing if (pos > 0) { - spcI = std::max(0,getNumVal(&req, pos)); + spcI = std::max(0,getNumVal(req, pos)); } - strip.setSegment(selectedSeg, startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY); + strip.suspend(); // must suspend strip operations before changing geometry + selseg.setGeometry(startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY, selseg.map1D2D); + strip.resume(); pos = req.indexOf(F("RV=")); //Segment reverse if (pos > 0) selseg.reverse = req.charAt(pos+3) != '0'; @@ -885,7 +955,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SB=")); //Segment brightness/opacity if (pos > 0) { - byte segbri = getNumVal(&req, pos); + byte segbri = getNumVal(req, pos); selseg.setOption(SEG_OPTION_ON, segbri); // use transition if (segbri) { selseg.setOpacity(segbri); @@ -894,7 +964,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SW=")); //segment power if (pos > 0) { - switch (getNumVal(&req, pos)) { + switch (getNumVal(req, pos)) { case 0: selseg.setOption(SEG_OPTION_ON, false); break; // use transition case 1: selseg.setOption(SEG_OPTION_ON, true); break; // use transition default: selseg.setOption(SEG_OPTION_ON, !selseg.on); break; // use transition @@ -902,16 +972,16 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("PS=")); //saves current in preset - if (pos > 0) savePreset(getNumVal(&req, pos)); + if (pos > 0) savePreset(getNumVal(req, pos)); pos = req.indexOf(F("P1=")); //sets first preset for cycle - if (pos > 0) presetCycMin = getNumVal(&req, pos); + if (pos > 0) presetCycMin = getNumVal(req, pos); pos = req.indexOf(F("P2=")); //sets last preset for cycle - if (pos > 0) presetCycMax = getNumVal(&req, pos); + if (pos > 0) presetCycMax = getNumVal(req, pos); //apply preset - if (updateVal(req.c_str(), "PL=", &presetCycCurr, presetCycMin, presetCycMax)) { + if (updateVal(req.c_str(), "PL=", presetCycCurr, presetCycMin, presetCycMax)) { applyPreset(presetCycCurr); } @@ -919,25 +989,25 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) if (pos > 0) doAdvancePlaylist = true; //set brightness - updateVal(req.c_str(), "&A=", &bri); + updateVal(req.c_str(), "&A=", bri); - bool col0Changed = false, col1Changed = false; + bool col0Changed = false, col1Changed = false, col2Changed = false; //set colors - col0Changed |= updateVal(req.c_str(), "&R=", &colIn[0]); - col0Changed |= updateVal(req.c_str(), "&G=", &colIn[1]); - col0Changed |= updateVal(req.c_str(), "&B=", &colIn[2]); - col0Changed |= updateVal(req.c_str(), "&W=", &colIn[3]); + col0Changed |= updateVal(req.c_str(), "&R=", colIn[0]); + col0Changed |= updateVal(req.c_str(), "&G=", colIn[1]); + col0Changed |= updateVal(req.c_str(), "&B=", colIn[2]); + col0Changed |= updateVal(req.c_str(), "&W=", colIn[3]); - col1Changed |= updateVal(req.c_str(), "R2=", &colInSec[0]); - col1Changed |= updateVal(req.c_str(), "G2=", &colInSec[1]); - col1Changed |= updateVal(req.c_str(), "B2=", &colInSec[2]); - col1Changed |= updateVal(req.c_str(), "W2=", &colInSec[3]); + col1Changed |= updateVal(req.c_str(), "R2=", colInSec[0]); + col1Changed |= updateVal(req.c_str(), "G2=", colInSec[1]); + col1Changed |= updateVal(req.c_str(), "B2=", colInSec[2]); + col1Changed |= updateVal(req.c_str(), "W2=", colInSec[3]); #ifdef WLED_ENABLE_LOXONE //lox parser pos = req.indexOf(F("LX=")); // Lox primary color if (pos > 0) { - int lxValue = getNumVal(&req, pos); + int lxValue = getNumVal(req, pos); if (parseLx(lxValue, colIn)) { bri = 255; nightlightActive = false; //always disable nightlight when toggling @@ -946,7 +1016,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("LY=")); // Lox secondary color if (pos > 0) { - int lxValue = getNumVal(&req, pos); + int lxValue = getNumVal(req, pos); if(parseLx(lxValue, colInSec)) { bri = 255; nightlightActive = false; //always disable nightlight when toggling @@ -958,11 +1028,11 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set hue pos = req.indexOf(F("HU=")); if (pos > 0) { - uint16_t temphue = getNumVal(&req, pos); + uint16_t temphue = getNumVal(req, pos); byte tempsat = 255; pos = req.indexOf(F("SA=")); if (pos > 0) { - tempsat = getNumVal(&req, pos); + tempsat = getNumVal(req, pos); } byte sec = req.indexOf(F("H2")); colorHStoRGB(temphue, tempsat, (sec>0) ? colInSec : colIn); @@ -973,12 +1043,11 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("&K=")); if (pos > 0) { byte sec = req.indexOf(F("K2")); - colorKtoRGB(getNumVal(&req, pos), (sec>0) ? colInSec : colIn); + colorKtoRGB(getNumVal(req, pos), (sec>0) ? colInSec : colIn); col0Changed |= (!sec); col1Changed |= sec; } //set color from HEX or 32bit DEC - byte tmpCol[4]; pos = req.indexOf(F("CL=")); if (pos > 0) { colorFromDecOrHexString(colIn, (char*)req.substring(pos + 3).c_str()); @@ -991,61 +1060,55 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("C3=")); if (pos > 0) { + byte tmpCol[4]; colorFromDecOrHexString(tmpCol, (char*)req.substring(pos + 3).c_str()); - uint32_t col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]); + col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]); selseg.setColor(2, col2); // defined above (SS= or main) - if (!singleSegment) strip.setColor(2, col2); // will set color to all active & selected segments + col2Changed = true; } //set to random hue SR=0->1st SR=1->2nd pos = req.indexOf(F("SR")); if (pos > 0) { - byte sec = getNumVal(&req, pos); + byte sec = getNumVal(req, pos); setRandomColor(sec? colInSec : colIn); col0Changed |= (!sec); col1Changed |= sec; } - //swap 2nd & 1st - pos = req.indexOf(F("SC")); - if (pos > 0) { - byte temp; - for (unsigned i=0; i<4; i++) { - temp = colIn[i]; - colIn[i] = colInSec[i]; - colInSec[i] = temp; - } - col0Changed = col1Changed = true; - } - // apply colors to selected segment, and all selected segments if applicable if (col0Changed) { - uint32_t colIn0 = RGBW32(colIn[0], colIn[1], colIn[2], colIn[3]); - selseg.setColor(0, colIn0); - if (!singleSegment) strip.setColor(0, colIn0); // will set color to all active & selected segments + col0 = RGBW32(colIn[0], colIn[1], colIn[2], colIn[3]); + selseg.setColor(0, col0); } if (col1Changed) { - uint32_t colIn1 = RGBW32(colInSec[0], colInSec[1], colInSec[2], colInSec[3]); - selseg.setColor(1, colIn1); - if (!singleSegment) strip.setColor(1, colIn1); // will set color to all active & selected segments + col1 = RGBW32(colInSec[0], colInSec[1], colInSec[2], colInSec[3]); + selseg.setColor(1, col1); + } + + //swap 2nd & 1st + pos = req.indexOf(F("SC")); + if (pos > 0) { + std::swap(col0,col1); + col0Changed = col1Changed = true; } bool fxModeChanged = false, speedChanged = false, intensityChanged = false, paletteChanged = false; bool custom1Changed = false, custom2Changed = false, custom3Changed = false, check1Changed = false, check2Changed = false, check3Changed = false; // set effect parameters - if (updateVal(req.c_str(), "FX=", &effectIn, 0, strip.getModeCount()-1)) { + if (updateVal(req.c_str(), "FX=", effectIn, 0, strip.getModeCount()-1)) { if (request != nullptr) unloadPlaylist(); // unload playlist if changing FX using web request fxModeChanged = true; } - speedChanged = updateVal(req.c_str(), "SX=", &speedIn); - intensityChanged = updateVal(req.c_str(), "IX=", &intensityIn); - paletteChanged = updateVal(req.c_str(), "FP=", &paletteIn, 0, strip.getPaletteCount()-1); - custom1Changed = updateVal(req.c_str(), "X1=", &custom1In); - custom2Changed = updateVal(req.c_str(), "X2=", &custom2In); - custom3Changed = updateVal(req.c_str(), "X3=", &custom3In); - check1Changed = updateVal(req.c_str(), "M1=", &check1In); - check2Changed = updateVal(req.c_str(), "M2=", &check2In); - check3Changed = updateVal(req.c_str(), "M3=", &check3In); + speedChanged = updateVal(req.c_str(), "SX=", speedIn); + intensityChanged = updateVal(req.c_str(), "IX=", intensityIn); + paletteChanged = updateVal(req.c_str(), "FP=", paletteIn, 0, getPaletteCount()-1); + custom1Changed = updateVal(req.c_str(), "X1=", custom1In); + custom2Changed = updateVal(req.c_str(), "X2=", custom2In); + custom3Changed = updateVal(req.c_str(), "X3=", custom3In); + check1Changed = updateVal(req.c_str(), "M1=", check1In); + check2Changed = updateVal(req.c_str(), "M2=", check2In); + check3Changed = updateVal(req.c_str(), "M3=", check3In); stateChanged |= (fxModeChanged || speedChanged || intensityChanged || paletteChanged || custom1Changed || custom2Changed || custom3Changed || check1Changed || check2Changed || check3Changed); @@ -1057,6 +1120,9 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) if (speedChanged) seg.speed = speedIn; if (intensityChanged) seg.intensity = intensityIn; if (paletteChanged) seg.setPalette(paletteIn); + if (col0Changed) seg.setColor(0, col0); + if (col1Changed) seg.setColor(1, col1); + if (col2Changed) seg.setColor(2, col2); if (custom1Changed) seg.custom1 = custom1In; if (custom2Changed) seg.custom2 = custom2In; if (custom3Changed) seg.custom3 = custom3In; @@ -1068,13 +1134,13 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set advanced overlay pos = req.indexOf(F("OL=")); if (pos > 0) { - overlayCurrent = getNumVal(&req, pos); + overlayCurrent = getNumVal(req, pos); } //apply macro (deprecated, added for compatibility with pre-0.11 automations) pos = req.indexOf(F("&M=")); if (pos > 0) { - applyPreset(getNumVal(&req, pos) + 16); + applyPreset(getNumVal(req, pos) + 16); } //toggle send UDP direct notifications @@ -1093,7 +1159,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("&T=")); if (pos > 0) { nightlightActive = false; //always disable nightlight when toggling - switch (getNumVal(&req, pos)) + switch (getNumVal(req, pos)) { case 0: if (bri != 0){briLast = bri; bri = 0;} break; //off, only if it was previously on case 1: if (bri == 0) bri = briLast; break; //on, only if it was previously off @@ -1112,7 +1178,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) nightlightActive = false; } else { nightlightActive = true; - if (!aNlDef) nightlightDelayMins = getNumVal(&req, pos); + if (!aNlDef) nightlightDelayMins = getNumVal(req, pos); else nightlightDelayMins = nightlightDelayMinsDefault; nightlightStartTime = millis(); } @@ -1126,7 +1192,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set nightlight target brightness pos = req.indexOf(F("NT=")); if (pos > 0) { - nightlightTargetBri = getNumVal(&req, pos); + nightlightTargetBri = getNumVal(req, pos); nightlightActiveOld = false; //re-init } @@ -1134,35 +1200,36 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("NF=")); if (pos > 0) { - nightlightMode = getNumVal(&req, pos); + nightlightMode = getNumVal(req, pos); nightlightActiveOld = false; //re-init } if (nightlightMode > NL_MODE_SUN) nightlightMode = NL_MODE_SUN; pos = req.indexOf(F("TT=")); - if (pos > 0) transitionDelay = getNumVal(&req, pos); - if (fadeTransition) strip.setTransition(transitionDelay); + if (pos > 0) transitionDelay = getNumVal(req, pos); + strip.setTransition(transitionDelay); //set time (unix timestamp) pos = req.indexOf(F("ST=")); if (pos > 0) { - setTimeFromAPI(getNumVal(&req, pos)); + setTimeFromAPI(getNumVal(req, pos)); } //set countdown goal (unix timestamp) pos = req.indexOf(F("CT=")); if (pos > 0) { - countdownTime = getNumVal(&req, pos); + countdownTime = getNumVal(req, pos); if (countdownTime - toki.second() > 0) countdownOverTriggered = false; } pos = req.indexOf(F("LO=")); if (pos > 0) { - realtimeOverride = getNumVal(&req, pos); + realtimeOverride = getNumVal(req, pos); if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS; if (realtimeMode && useMainSegmentOnly) { strip.getMainSegment().freeze = !realtimeOverride; + realtimeOverride = REALTIME_OVERRIDE_NONE; // ignore request for override if using main segment only } } @@ -1175,16 +1242,16 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("U0=")); //user var 0 if (pos > 0) { - userVar0 = getNumVal(&req, pos); + userVar0 = getNumVal(req, pos); } pos = req.indexOf(F("U1=")); //user var 1 if (pos > 0) { - userVar1 = getNumVal(&req, pos); + userVar1 = getNumVal(req, pos); } // you can add more if you need - // global col[], effectCurrent, ... are updated in stateChanged() + // global colPri[], effectCurrent, ... are updated in stateChanged() if (!apply) return true; // when called by JSON API, do not call colorUpdated() here pos = req.indexOf(F("&NN")); //do not send UDP notifications this time diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient.cpp deleted file mode 100644 index d0c44cb6ba..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient.cpp +++ /dev/null @@ -1,877 +0,0 @@ -#include "AsyncMqttClient.hpp" - -AsyncMqttClient::AsyncMqttClient() -: _connected(false) -, _connectPacketNotEnoughSpace(false) -, _disconnectFlagged(false) -, _tlsBadFingerprint(false) -, _lastClientActivity(0) -, _lastServerActivity(0) -, _lastPingRequestTime(0) -, _host(nullptr) -, _useIp(false) -#if ASYNC_TCP_SSL_ENABLED -, _secure(false) -#endif -, _port(0) -, _keepAlive(15) -, _cleanSession(true) -, _clientId(nullptr) -, _username(nullptr) -, _password(nullptr) -, _willTopic(nullptr) -, _willPayload(nullptr) -, _willPayloadLength(0) -, _willQos(0) -, _willRetain(false) -, _parsingInformation { .bufferState = AsyncMqttClientInternals::BufferState::NONE } -, _currentParsedPacket(nullptr) -, _remainingLengthBufferPosition(0) -, _nextPacketId(1) { - _client.onConnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onConnect(c); }, this); - _client.onDisconnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onDisconnect(c); }, this); - _client.onError([](void* obj, AsyncClient* c, int8_t error) { (static_cast(obj))->_onError(c, error); }, this); - _client.onTimeout([](void* obj, AsyncClient* c, uint32_t time) { (static_cast(obj))->_onTimeout(c, time); }, this); - _client.onAck([](void* obj, AsyncClient* c, size_t len, uint32_t time) { (static_cast(obj))->_onAck(c, len, time); }, this); - _client.onData([](void* obj, AsyncClient* c, void* data, size_t len) { (static_cast(obj))->_onData(c, static_cast(data), len); }, this); - _client.onPoll([](void* obj, AsyncClient* c) { (static_cast(obj))->_onPoll(c); }, this); - -#ifdef ESP32 - sprintf(_generatedClientId, "esp32%06x", (uint32_t)ESP.getEfuseMac()); - _xSemaphore = xSemaphoreCreateMutex(); -#elif defined(ESP8266) - sprintf(_generatedClientId, "esp8266%06x", (uint32_t)ESP.getChipId()); -#endif - _clientId = _generatedClientId; - - setMaxTopicLength(128); -} - -AsyncMqttClient::~AsyncMqttClient() { - delete _currentParsedPacket; - delete[] _parsingInformation.topicBuffer; -#ifdef ESP32 - vSemaphoreDelete(_xSemaphore); -#endif -} - -AsyncMqttClient& AsyncMqttClient::setKeepAlive(uint16_t keepAlive) { - _keepAlive = keepAlive; - return *this; -} - -AsyncMqttClient& AsyncMqttClient::setClientId(const char* clientId) { - _clientId = clientId; - return *this; -} - -AsyncMqttClient& AsyncMqttClient::setCleanSession(bool cleanSession) { - _cleanSession = cleanSession; - return *this; -} - -AsyncMqttClient& AsyncMqttClient::setMaxTopicLength(uint16_t maxTopicLength) { - _parsingInformation.maxTopicLength = maxTopicLength; - delete[] _parsingInformation.topicBuffer; - _parsingInformation.topicBuffer = new char[maxTopicLength + 1]; - return *this; -} - -AsyncMqttClient& AsyncMqttClient::setCredentials(const char* username, const char* password) { - _username = username; - _password = password; - return *this; -} - -AsyncMqttClient& AsyncMqttClient::setWill(const char* topic, uint8_t qos, bool retain, const char* payload, size_t length) { - _willTopic = topic; - _willQos = qos; - _willRetain = retain; - _willPayload = payload; - _willPayloadLength = length; - return *this; -} - -AsyncMqttClient& AsyncMqttClient::setServer(IPAddress ip, uint16_t port) { - _useIp = true; - _ip = ip; - _port = port; - return *this; -} - -AsyncMqttClient& AsyncMqttClient::setServer(const char* host, uint16_t port) { - _useIp = false; - _host = host; - _port = port; - return *this; -} - -#if ASYNC_TCP_SSL_ENABLED -AsyncMqttClient& AsyncMqttClient::setSecure(bool secure) { - _secure = secure; - return *this; -} - -AsyncMqttClient& AsyncMqttClient::addServerFingerprint(const uint8_t* fingerprint) { - std::array newFingerprint; - memcpy(newFingerprint.data(), fingerprint, SHA1_SIZE); - _secureServerFingerprints.push_back(newFingerprint); - return *this; -} -#endif - -AsyncMqttClient& AsyncMqttClient::onConnect(AsyncMqttClientInternals::OnConnectUserCallback callback) { - _onConnectUserCallbacks.push_back(callback); - return *this; -} - -AsyncMqttClient& AsyncMqttClient::onDisconnect(AsyncMqttClientInternals::OnDisconnectUserCallback callback) { - _onDisconnectUserCallbacks.push_back(callback); - return *this; -} - -AsyncMqttClient& AsyncMqttClient::onSubscribe(AsyncMqttClientInternals::OnSubscribeUserCallback callback) { - _onSubscribeUserCallbacks.push_back(callback); - return *this; -} - -AsyncMqttClient& AsyncMqttClient::onUnsubscribe(AsyncMqttClientInternals::OnUnsubscribeUserCallback callback) { - _onUnsubscribeUserCallbacks.push_back(callback); - return *this; -} - -AsyncMqttClient& AsyncMqttClient::onMessage(AsyncMqttClientInternals::OnMessageUserCallback callback) { - _onMessageUserCallbacks.push_back(callback); - return *this; -} - -AsyncMqttClient& AsyncMqttClient::onPublish(AsyncMqttClientInternals::OnPublishUserCallback callback) { - _onPublishUserCallbacks.push_back(callback); - return *this; -} - -void AsyncMqttClient::_freeCurrentParsedPacket() { - delete _currentParsedPacket; - _currentParsedPacket = nullptr; -} - -void AsyncMqttClient::_clear() { - _lastPingRequestTime = 0; - _connected = false; - _disconnectFlagged = false; - _connectPacketNotEnoughSpace = false; - _tlsBadFingerprint = false; - _freeCurrentParsedPacket(); - - _pendingPubRels.clear(); - _pendingPubRels.shrink_to_fit(); - - _toSendAcks.clear(); - _toSendAcks.shrink_to_fit(); - - _nextPacketId = 1; - _parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::NONE; -} - -/* TCP */ -void AsyncMqttClient::_onConnect(AsyncClient* client) { - (void)client; - -#if ASYNC_TCP_SSL_ENABLED - if (_secure && _secureServerFingerprints.size() > 0) { - SSL* clientSsl = _client.getSSL(); - - bool sslFoundFingerprint = false; - for (std::array fingerprint : _secureServerFingerprints) { - if (ssl_match_fingerprint(clientSsl, fingerprint.data()) == SSL_OK) { - sslFoundFingerprint = true; - break; - } - } - - if (!sslFoundFingerprint) { - _tlsBadFingerprint = true; - _client.close(true); - return; - } - } -#endif - - char fixedHeader[5]; - fixedHeader[0] = AsyncMqttClientInternals::PacketType.CONNECT; - fixedHeader[0] = fixedHeader[0] << 4; - fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.CONNECT_RESERVED; - - uint16_t protocolNameLength = 4; - char protocolNameLengthBytes[2]; - protocolNameLengthBytes[0] = protocolNameLength >> 8; - protocolNameLengthBytes[1] = protocolNameLength & 0xFF; - - char protocolLevel[1]; - protocolLevel[0] = 0x04; - - char connectFlags[1]; - connectFlags[0] = 0; - if (_cleanSession) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.CLEAN_SESSION; - if (_username != nullptr) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.USERNAME; - if (_password != nullptr) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.PASSWORD; - if (_willTopic != nullptr) { - connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL; - if (_willRetain) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_RETAIN; - switch (_willQos) { - case 0: - connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_QOS0; - break; - case 1: - connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_QOS1; - break; - case 2: - connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_QOS2; - break; - } - } - - char keepAliveBytes[2]; - keepAliveBytes[0] = _keepAlive >> 8; - keepAliveBytes[1] = _keepAlive & 0xFF; - - uint16_t clientIdLength = strlen(_clientId); - char clientIdLengthBytes[2]; - clientIdLengthBytes[0] = clientIdLength >> 8; - clientIdLengthBytes[1] = clientIdLength & 0xFF; - - // Optional fields - uint16_t willTopicLength = 0; - char willTopicLengthBytes[2]; - uint16_t willPayloadLength = _willPayloadLength; - char willPayloadLengthBytes[2]; - if (_willTopic != nullptr) { - willTopicLength = strlen(_willTopic); - willTopicLengthBytes[0] = willTopicLength >> 8; - willTopicLengthBytes[1] = willTopicLength & 0xFF; - - if (_willPayload != nullptr && willPayloadLength == 0) willPayloadLength = strlen(_willPayload); - - willPayloadLengthBytes[0] = willPayloadLength >> 8; - willPayloadLengthBytes[1] = willPayloadLength & 0xFF; - } - - uint16_t usernameLength = 0; - char usernameLengthBytes[2]; - if (_username != nullptr) { - usernameLength = strlen(_username); - usernameLengthBytes[0] = usernameLength >> 8; - usernameLengthBytes[1] = usernameLength & 0xFF; - } - - uint16_t passwordLength = 0; - char passwordLengthBytes[2]; - if (_password != nullptr) { - passwordLength = strlen(_password); - passwordLengthBytes[0] = passwordLength >> 8; - passwordLengthBytes[1] = passwordLength & 0xFF; - } - - uint32_t remainingLength = 2 + protocolNameLength + 1 + 1 + 2 + 2 + clientIdLength; // always present - if (_willTopic != nullptr) remainingLength += 2 + willTopicLength + 2 + willPayloadLength; - if (_username != nullptr) remainingLength += 2 + usernameLength; - if (_password != nullptr) remainingLength += 2 + passwordLength; - uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(remainingLength, fixedHeader + 1); - - uint32_t neededSpace = 1 + remainingLengthLength; - neededSpace += 2; - neededSpace += protocolNameLength; - neededSpace += 1; - neededSpace += 1; - neededSpace += 2; - neededSpace += 2; - neededSpace += clientIdLength; - if (_willTopic != nullptr) { - neededSpace += 2; - neededSpace += willTopicLength; - - neededSpace += 2; - if (_willPayload != nullptr) neededSpace += willPayloadLength; - } - if (_username != nullptr) { - neededSpace += 2; - neededSpace += usernameLength; - } - if (_password != nullptr) { - neededSpace += 2; - neededSpace += passwordLength; - } - - SEMAPHORE_TAKE(); - if (_client.space() < neededSpace) { - _connectPacketNotEnoughSpace = true; - _client.close(true); - SEMAPHORE_GIVE(); - return; - } - - _client.add(fixedHeader, 1 + remainingLengthLength); - _client.add(protocolNameLengthBytes, 2); - _client.add("MQTT", protocolNameLength); - _client.add(protocolLevel, 1); - _client.add(connectFlags, 1); - _client.add(keepAliveBytes, 2); - _client.add(clientIdLengthBytes, 2); - _client.add(_clientId, clientIdLength); - if (_willTopic != nullptr) { - _client.add(willTopicLengthBytes, 2); - _client.add(_willTopic, willTopicLength); - - _client.add(willPayloadLengthBytes, 2); - if (_willPayload != nullptr) _client.add(_willPayload, willPayloadLength); - } - if (_username != nullptr) { - _client.add(usernameLengthBytes, 2); - _client.add(_username, usernameLength); - } - if (_password != nullptr) { - _client.add(passwordLengthBytes, 2); - _client.add(_password, passwordLength); - } - _client.send(); - _lastClientActivity = millis(); - SEMAPHORE_GIVE(); -} - -void AsyncMqttClient::_onDisconnect(AsyncClient* client) { - (void)client; - if (!_disconnectFlagged) { - AsyncMqttClientDisconnectReason reason; - - if (_connectPacketNotEnoughSpace) { - reason = AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE; - } else if (_tlsBadFingerprint) { - reason = AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT; - } else { - reason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED; - } - for (auto callback : _onDisconnectUserCallbacks) callback(reason); - } - _clear(); -} - -void AsyncMqttClient::_onError(AsyncClient* client, int8_t error) { - (void)client; - (void)error; - // _onDisconnect called anyway -} - -void AsyncMqttClient::_onTimeout(AsyncClient* client, uint32_t time) { - (void)client; - (void)time; - // disconnection will be handled by ping/pong management -} - -void AsyncMqttClient::_onAck(AsyncClient* client, size_t len, uint32_t time) { - (void)client; - (void)len; - (void)time; -} - -void AsyncMqttClient::_onData(AsyncClient* client, char* data, size_t len) { - (void)client; - size_t currentBytePosition = 0; - char currentByte; - do { - switch (_parsingInformation.bufferState) { - case AsyncMqttClientInternals::BufferState::NONE: - currentByte = data[currentBytePosition++]; - _parsingInformation.packetType = currentByte >> 4; - _parsingInformation.packetFlags = (currentByte << 4) >> 4; - _parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::REMAINING_LENGTH; - _lastServerActivity = millis(); - switch (_parsingInformation.packetType) { - case AsyncMqttClientInternals::PacketType.CONNACK: - _currentParsedPacket = new AsyncMqttClientInternals::ConnAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onConnAck, this, std::placeholders::_1, std::placeholders::_2)); - break; - case AsyncMqttClientInternals::PacketType.PINGRESP: - _currentParsedPacket = new AsyncMqttClientInternals::PingRespPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPingResp, this)); - break; - case AsyncMqttClientInternals::PacketType.SUBACK: - _currentParsedPacket = new AsyncMqttClientInternals::SubAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onSubAck, this, std::placeholders::_1, std::placeholders::_2)); - break; - case AsyncMqttClientInternals::PacketType.UNSUBACK: - _currentParsedPacket = new AsyncMqttClientInternals::UnsubAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onUnsubAck, this, std::placeholders::_1)); - break; - case AsyncMqttClientInternals::PacketType.PUBLISH: - _currentParsedPacket = new AsyncMqttClientInternals::PublishPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6, std::placeholders::_7, std::placeholders::_8, std::placeholders::_9), std::bind(&AsyncMqttClient::_onPublish, this, std::placeholders::_1, std::placeholders::_2)); - break; - case AsyncMqttClientInternals::PacketType.PUBREL: - _currentParsedPacket = new AsyncMqttClientInternals::PubRelPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubRel, this, std::placeholders::_1)); - break; - case AsyncMqttClientInternals::PacketType.PUBACK: - _currentParsedPacket = new AsyncMqttClientInternals::PubAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubAck, this, std::placeholders::_1)); - break; - case AsyncMqttClientInternals::PacketType.PUBREC: - _currentParsedPacket = new AsyncMqttClientInternals::PubRecPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubRec, this, std::placeholders::_1)); - break; - case AsyncMqttClientInternals::PacketType.PUBCOMP: - _currentParsedPacket = new AsyncMqttClientInternals::PubCompPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubComp, this, std::placeholders::_1)); - break; - default: - break; - } - break; - case AsyncMqttClientInternals::BufferState::REMAINING_LENGTH: - currentByte = data[currentBytePosition++]; - _remainingLengthBuffer[_remainingLengthBufferPosition++] = currentByte; - if (currentByte >> 7 == 0) { - _parsingInformation.remainingLength = AsyncMqttClientInternals::Helpers::decodeRemainingLength(_remainingLengthBuffer); - _remainingLengthBufferPosition = 0; - if (_parsingInformation.remainingLength > 0) { - _parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::VARIABLE_HEADER; - } else { - // PINGRESP is a special case where it has no variable header, so the packet ends right here - _parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::NONE; - _onPingResp(); - } - } - break; - case AsyncMqttClientInternals::BufferState::VARIABLE_HEADER: - _currentParsedPacket->parseVariableHeader(data, len, ¤tBytePosition); - break; - case AsyncMqttClientInternals::BufferState::PAYLOAD: - _currentParsedPacket->parsePayload(data, len, ¤tBytePosition); - break; - default: - currentBytePosition = len; - } - } while (currentBytePosition != len); -} - -void AsyncMqttClient::_onPoll(AsyncClient* client) { - if (!_connected) return; - - // if there is too much time the client has sent a ping request without a response, disconnect client to avoid half open connections - if (_lastPingRequestTime != 0 && (millis() - _lastPingRequestTime) >= (_keepAlive * 1000 * 2)) { - disconnect(); - return; - // send ping to ensure the server will receive at least one message inside keepalive window - } else if (_lastPingRequestTime == 0 && (millis() - _lastClientActivity) >= (_keepAlive * 1000 * 0.7)) { - _sendPing(); - - // send ping to verify if the server is still there (ensure this is not a half connection) - } else if (_connected && _lastPingRequestTime == 0 && (millis() - _lastServerActivity) >= (_keepAlive * 1000 * 0.7)) { - _sendPing(); - } - - // handle to send ack packets - - _sendAcks(); - - // handle disconnect - - if (_disconnectFlagged) { - _sendDisconnect(); - } -} - -/* MQTT */ -void AsyncMqttClient::_onPingResp() { - _freeCurrentParsedPacket(); - _lastPingRequestTime = 0; -} - -void AsyncMqttClient::_onConnAck(bool sessionPresent, uint8_t connectReturnCode) { - (void)sessionPresent; - _freeCurrentParsedPacket(); - - if (connectReturnCode == 0) { - _connected = true; - for (auto callback : _onConnectUserCallbacks) callback(sessionPresent); - } else { - for (auto callback : _onDisconnectUserCallbacks) callback(static_cast(connectReturnCode)); - _disconnectFlagged = true; - } -} - -void AsyncMqttClient::_onSubAck(uint16_t packetId, char status) { - _freeCurrentParsedPacket(); - - for (auto callback : _onSubscribeUserCallbacks) callback(packetId, status); -} - -void AsyncMqttClient::_onUnsubAck(uint16_t packetId) { - _freeCurrentParsedPacket(); - - for (auto callback : _onUnsubscribeUserCallbacks) callback(packetId); -} - -void AsyncMqttClient::_onMessage(char* topic, char* payload, uint8_t qos, bool dup, bool retain, size_t len, size_t index, size_t total, uint16_t packetId) { - bool notifyPublish = true; - - if (qos == 2) { - for (AsyncMqttClientInternals::PendingPubRel pendingPubRel : _pendingPubRels) { - if (pendingPubRel.packetId == packetId) { - notifyPublish = false; - break; - } - } - } - - if (notifyPublish) { - AsyncMqttClientMessageProperties properties; - properties.qos = qos; - properties.dup = dup; - properties.retain = retain; - - for (auto callback : _onMessageUserCallbacks) callback(topic, payload, properties, len, index, total); - } -} - -void AsyncMqttClient::_onPublish(uint16_t packetId, uint8_t qos) { - AsyncMqttClientInternals::PendingAck pendingAck; - - if (qos == 1) { - pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBACK; - pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBACK_RESERVED; - pendingAck.packetId = packetId; - _toSendAcks.push_back(pendingAck); - } else if (qos == 2) { - pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBREC; - pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBREC_RESERVED; - pendingAck.packetId = packetId; - _toSendAcks.push_back(pendingAck); - - bool pubRelAwaiting = false; - for (AsyncMqttClientInternals::PendingPubRel pendingPubRel : _pendingPubRels) { - if (pendingPubRel.packetId == packetId) { - pubRelAwaiting = true; - break; - } - } - - if (!pubRelAwaiting) { - AsyncMqttClientInternals::PendingPubRel pendingPubRel; - pendingPubRel.packetId = packetId; - _pendingPubRels.push_back(pendingPubRel); - } - - _sendAcks(); - } - - _freeCurrentParsedPacket(); -} - -void AsyncMqttClient::_onPubRel(uint16_t packetId) { - _freeCurrentParsedPacket(); - - AsyncMqttClientInternals::PendingAck pendingAck; - pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBCOMP; - pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBCOMP_RESERVED; - pendingAck.packetId = packetId; - _toSendAcks.push_back(pendingAck); - - for (size_t i = 0; i < _pendingPubRels.size(); i++) { - if (_pendingPubRels[i].packetId == packetId) { - _pendingPubRels.erase(_pendingPubRels.begin() + i); - _pendingPubRels.shrink_to_fit(); - } - } - - _sendAcks(); -} - -void AsyncMqttClient::_onPubAck(uint16_t packetId) { - _freeCurrentParsedPacket(); - - for (auto callback : _onPublishUserCallbacks) callback(packetId); -} - -void AsyncMqttClient::_onPubRec(uint16_t packetId) { - _freeCurrentParsedPacket(); - - AsyncMqttClientInternals::PendingAck pendingAck; - pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBREL; - pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBREL_RESERVED; - pendingAck.packetId = packetId; - _toSendAcks.push_back(pendingAck); - - _sendAcks(); -} - -void AsyncMqttClient::_onPubComp(uint16_t packetId) { - _freeCurrentParsedPacket(); - - for (auto callback : _onPublishUserCallbacks) callback(packetId); -} - -bool AsyncMqttClient::_sendPing() { - char fixedHeader[2]; - fixedHeader[0] = AsyncMqttClientInternals::PacketType.PINGREQ; - fixedHeader[0] = fixedHeader[0] << 4; - fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.PINGREQ_RESERVED; - fixedHeader[1] = 0; - - size_t neededSpace = 2; - - SEMAPHORE_TAKE(false); - if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return false; } - - _client.add(fixedHeader, 2); - _client.send(); - _lastClientActivity = millis(); - _lastPingRequestTime = millis(); - - SEMAPHORE_GIVE(); - return true; -} - -void AsyncMqttClient::_sendAcks() { - uint8_t neededAckSpace = 2 + 2; - - SEMAPHORE_TAKE(); - for (size_t i = 0; i < _toSendAcks.size(); i++) { - if (_client.space() < neededAckSpace) break; - - AsyncMqttClientInternals::PendingAck pendingAck = _toSendAcks[i]; - - char fixedHeader[2]; - fixedHeader[0] = pendingAck.packetType; - fixedHeader[0] = fixedHeader[0] << 4; - fixedHeader[0] = fixedHeader[0] | pendingAck.headerFlag; - fixedHeader[1] = 2; - - char packetIdBytes[2]; - packetIdBytes[0] = pendingAck.packetId >> 8; - packetIdBytes[1] = pendingAck.packetId & 0xFF; - - _client.add(fixedHeader, 2); - _client.add(packetIdBytes, 2); - _client.send(); - - _toSendAcks.erase(_toSendAcks.begin() + i); - _toSendAcks.shrink_to_fit(); - - _lastClientActivity = millis(); - } - SEMAPHORE_GIVE(); -} - -bool AsyncMqttClient::_sendDisconnect() { - if (!_connected) return true; - - const uint8_t neededSpace = 2; - - SEMAPHORE_TAKE(false); - - if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return false; } - - char fixedHeader[2]; - fixedHeader[0] = AsyncMqttClientInternals::PacketType.DISCONNECT; - fixedHeader[0] = fixedHeader[0] << 4; - fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.DISCONNECT_RESERVED; - fixedHeader[1] = 0; - - _client.add(fixedHeader, 2); - _client.send(); - _client.close(true); - - _disconnectFlagged = false; - - SEMAPHORE_GIVE(); - return true; -} - -uint16_t AsyncMqttClient::_getNextPacketId() { - uint16_t nextPacketId = _nextPacketId; - - if (_nextPacketId == 65535) _nextPacketId = 0; // 0 is forbidden - _nextPacketId++; - - return nextPacketId; -} - -bool AsyncMqttClient::connected() const { - return _connected; -} - -void AsyncMqttClient::connect() { - if (_connected) return; - -#if ASYNC_TCP_SSL_ENABLED - if (_useIp) { - _client.connect(_ip, _port, _secure); - } else { - _client.connect(_host, _port, _secure); - } -#else - if (_useIp) { - _client.connect(_ip, _port); - } else { - _client.connect(_host, _port); - } -#endif -} - -void AsyncMqttClient::disconnect(bool force) { - if (!_connected) return; - - if (force) { - _client.close(true); - } else { - _disconnectFlagged = true; - _sendDisconnect(); - } -} - -uint16_t AsyncMqttClient::subscribe(const char* topic, uint8_t qos) { - if (!_connected) return 0; - - char fixedHeader[5]; - fixedHeader[0] = AsyncMqttClientInternals::PacketType.SUBSCRIBE; - fixedHeader[0] = fixedHeader[0] << 4; - fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.SUBSCRIBE_RESERVED; - - uint16_t topicLength = strlen(topic); - char topicLengthBytes[2]; - topicLengthBytes[0] = topicLength >> 8; - topicLengthBytes[1] = topicLength & 0xFF; - - char qosByte[1]; - qosByte[0] = qos; - - uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(2 + 2 + topicLength + 1, fixedHeader + 1); - - size_t neededSpace = 0; - neededSpace += 1 + remainingLengthLength; - neededSpace += 2; - neededSpace += 2; - neededSpace += topicLength; - neededSpace += 1; - - SEMAPHORE_TAKE(0); - if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return 0; } - - uint16_t packetId = _getNextPacketId(); - char packetIdBytes[2]; - packetIdBytes[0] = packetId >> 8; - packetIdBytes[1] = packetId & 0xFF; - - _client.add(fixedHeader, 1 + remainingLengthLength); - _client.add(packetIdBytes, 2); - _client.add(topicLengthBytes, 2); - _client.add(topic, topicLength); - _client.add(qosByte, 1); - _client.send(); - _lastClientActivity = millis(); - - SEMAPHORE_GIVE(); - return packetId; -} - -uint16_t AsyncMqttClient::unsubscribe(const char* topic) { - if (!_connected) return 0; - - char fixedHeader[5]; - fixedHeader[0] = AsyncMqttClientInternals::PacketType.UNSUBSCRIBE; - fixedHeader[0] = fixedHeader[0] << 4; - fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.UNSUBSCRIBE_RESERVED; - - uint16_t topicLength = strlen(topic); - char topicLengthBytes[2]; - topicLengthBytes[0] = topicLength >> 8; - topicLengthBytes[1] = topicLength & 0xFF; - - uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(2 + 2 + topicLength, fixedHeader + 1); - - size_t neededSpace = 0; - neededSpace += 1 + remainingLengthLength; - neededSpace += 2; - neededSpace += 2; - neededSpace += topicLength; - - SEMAPHORE_TAKE(0); - if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return 0; } - - uint16_t packetId = _getNextPacketId(); - char packetIdBytes[2]; - packetIdBytes[0] = packetId >> 8; - packetIdBytes[1] = packetId & 0xFF; - - _client.add(fixedHeader, 1 + remainingLengthLength); - _client.add(packetIdBytes, 2); - _client.add(topicLengthBytes, 2); - _client.add(topic, topicLength); - _client.send(); - _lastClientActivity = millis(); - - SEMAPHORE_GIVE(); - return packetId; -} - -uint16_t AsyncMqttClient::publish(const char* topic, uint8_t qos, bool retain, const char* payload, size_t length, bool dup, uint16_t message_id) { - if (!_connected) return 0; - - char fixedHeader[5]; - fixedHeader[0] = AsyncMqttClientInternals::PacketType.PUBLISH; - fixedHeader[0] = fixedHeader[0] << 4; - if (dup) fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_DUP; - if (retain) fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_RETAIN; - switch (qos) { - case 0: - fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_QOS0; - break; - case 1: - fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_QOS1; - break; - case 2: - fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_QOS2; - break; - } - - uint16_t topicLength = strlen(topic); - char topicLengthBytes[2]; - topicLengthBytes[0] = topicLength >> 8; - topicLengthBytes[1] = topicLength & 0xFF; - - uint32_t payloadLength = length; - if (payload != nullptr && payloadLength == 0) payloadLength = strlen(payload); - - uint32_t remainingLength = 2 + topicLength + payloadLength; - if (qos != 0) remainingLength += 2; - uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(remainingLength, fixedHeader + 1); - - size_t neededSpace = 0; - neededSpace += 1 + remainingLengthLength; - neededSpace += 2; - neededSpace += topicLength; - if (qos != 0) neededSpace += 2; - if (payload != nullptr) neededSpace += payloadLength; - - SEMAPHORE_TAKE(0); - if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return 0; } - - uint16_t packetId = 0; - char packetIdBytes[2]; - if (qos != 0) { - if (dup && message_id > 0) { - packetId = message_id; - } else { - packetId = _getNextPacketId(); - } - - packetIdBytes[0] = packetId >> 8; - packetIdBytes[1] = packetId & 0xFF; - } - - _client.add(fixedHeader, 1 + remainingLengthLength); - _client.add(topicLengthBytes, 2); - _client.add(topic, topicLength); - if (qos != 0) _client.add(packetIdBytes, 2); - if (payload != nullptr) _client.add(payload, payloadLength); - _client.send(); - _lastClientActivity = millis(); - - SEMAPHORE_GIVE(); - if (qos != 0) { - return packetId; - } else { - return 1; - } -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient.h b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient.h deleted file mode 100644 index 23d30554d8..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef SRC_ASYNCMQTTCLIENT_H_ -#define SRC_ASYNCMQTTCLIENT_H_ - -#include "AsyncMqttClient.hpp" - -#endif // SRC_ASYNCMQTTCLIENT_H_ diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient.hpp deleted file mode 100644 index af8332b24e..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient.hpp +++ /dev/null @@ -1,166 +0,0 @@ -#pragma once - -#include -#include - -#include "Arduino.h" - -#ifdef ESP32 -#include -#include -#elif defined(ESP8266) -#include -#else -#error Platform not supported -#endif - -#if ASYNC_TCP_SSL_ENABLED -#include -#define SHA1_SIZE 20 -#endif - -#include "AsyncMqttClient/Flags.hpp" -#include "AsyncMqttClient/ParsingInformation.hpp" -#include "AsyncMqttClient/MessageProperties.hpp" -#include "AsyncMqttClient/Helpers.hpp" -#include "AsyncMqttClient/Callbacks.hpp" -#include "AsyncMqttClient/DisconnectReasons.hpp" -#include "AsyncMqttClient/Storage.hpp" - -#include "AsyncMqttClient/Packets/Packet.hpp" -#include "AsyncMqttClient/Packets/ConnAckPacket.hpp" -#include "AsyncMqttClient/Packets/PingRespPacket.hpp" -#include "AsyncMqttClient/Packets/SubAckPacket.hpp" -#include "AsyncMqttClient/Packets/UnsubAckPacket.hpp" -#include "AsyncMqttClient/Packets/PublishPacket.hpp" -#include "AsyncMqttClient/Packets/PubRelPacket.hpp" -#include "AsyncMqttClient/Packets/PubAckPacket.hpp" -#include "AsyncMqttClient/Packets/PubRecPacket.hpp" -#include "AsyncMqttClient/Packets/PubCompPacket.hpp" - -#if ESP32 -#define SEMAPHORE_TAKE(X) if (xSemaphoreTake(_xSemaphore, 1000 / portTICK_PERIOD_MS) != pdTRUE) { return X; } // Waits max 1000ms -#define SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore); -#elif defined(ESP8266) -#define SEMAPHORE_TAKE(X) void() -#define SEMAPHORE_GIVE() void() -#endif - -class AsyncMqttClient { - public: - AsyncMqttClient(); - ~AsyncMqttClient(); - - AsyncMqttClient& setKeepAlive(uint16_t keepAlive); - AsyncMqttClient& setClientId(const char* clientId); - AsyncMqttClient& setCleanSession(bool cleanSession); - AsyncMqttClient& setMaxTopicLength(uint16_t maxTopicLength); - AsyncMqttClient& setCredentials(const char* username, const char* password = nullptr); - AsyncMqttClient& setWill(const char* topic, uint8_t qos, bool retain, const char* payload = nullptr, size_t length = 0); - AsyncMqttClient& setServer(IPAddress ip, uint16_t port); - AsyncMqttClient& setServer(const char* host, uint16_t port); -#if ASYNC_TCP_SSL_ENABLED - AsyncMqttClient& setSecure(bool secure); - AsyncMqttClient& addServerFingerprint(const uint8_t* fingerprint); -#endif - - AsyncMqttClient& onConnect(AsyncMqttClientInternals::OnConnectUserCallback callback); - AsyncMqttClient& onDisconnect(AsyncMqttClientInternals::OnDisconnectUserCallback callback); - AsyncMqttClient& onSubscribe(AsyncMqttClientInternals::OnSubscribeUserCallback callback); - AsyncMqttClient& onUnsubscribe(AsyncMqttClientInternals::OnUnsubscribeUserCallback callback); - AsyncMqttClient& onMessage(AsyncMqttClientInternals::OnMessageUserCallback callback); - AsyncMqttClient& onPublish(AsyncMqttClientInternals::OnPublishUserCallback callback); - - bool connected() const; - void connect(); - void disconnect(bool force = false); - uint16_t subscribe(const char* topic, uint8_t qos); - uint16_t unsubscribe(const char* topic); - uint16_t publish(const char* topic, uint8_t qos, bool retain, const char* payload = nullptr, size_t length = 0, bool dup = false, uint16_t message_id = 0); - - private: - AsyncClient _client; - - bool _connected; - bool _connectPacketNotEnoughSpace; - bool _disconnectFlagged; - bool _tlsBadFingerprint; - uint32_t _lastClientActivity; - uint32_t _lastServerActivity; - uint32_t _lastPingRequestTime; - - char _generatedClientId[13 + 1]; // esp8266abc123 - IPAddress _ip; - const char* _host; - bool _useIp; -#if ASYNC_TCP_SSL_ENABLED - bool _secure; -#endif - uint16_t _port; - uint16_t _keepAlive; - bool _cleanSession; - const char* _clientId; - const char* _username; - const char* _password; - const char* _willTopic; - const char* _willPayload; - uint16_t _willPayloadLength; - uint8_t _willQos; - bool _willRetain; - -#if ASYNC_TCP_SSL_ENABLED - std::vector> _secureServerFingerprints; -#endif - - std::vector _onConnectUserCallbacks; - std::vector _onDisconnectUserCallbacks; - std::vector _onSubscribeUserCallbacks; - std::vector _onUnsubscribeUserCallbacks; - std::vector _onMessageUserCallbacks; - std::vector _onPublishUserCallbacks; - - AsyncMqttClientInternals::ParsingInformation _parsingInformation; - AsyncMqttClientInternals::Packet* _currentParsedPacket; - uint8_t _remainingLengthBufferPosition; - char _remainingLengthBuffer[4]; - - uint16_t _nextPacketId; - - std::vector _pendingPubRels; - - std::vector _toSendAcks; - -#ifdef ESP32 - SemaphoreHandle_t _xSemaphore = nullptr; -#endif - - void _clear(); - void _freeCurrentParsedPacket(); - - // TCP - void _onConnect(AsyncClient* client); - void _onDisconnect(AsyncClient* client); - static void _onError(AsyncClient* client, int8_t error); - void _onTimeout(AsyncClient* client, uint32_t time); - static void _onAck(AsyncClient* client, size_t len, uint32_t time); - void _onData(AsyncClient* client, char* data, size_t len); - void _onPoll(AsyncClient* client); - - // MQTT - void _onPingResp(); - void _onConnAck(bool sessionPresent, uint8_t connectReturnCode); - void _onSubAck(uint16_t packetId, char status); - void _onUnsubAck(uint16_t packetId); - void _onMessage(char* topic, char* payload, uint8_t qos, bool dup, bool retain, size_t len, size_t index, size_t total, uint16_t packetId); - void _onPublish(uint16_t packetId, uint8_t qos); - void _onPubRel(uint16_t packetId); - void _onPubAck(uint16_t packetId); - void _onPubRec(uint16_t packetId); - void _onPubComp(uint16_t packetId); - - bool _sendPing(); - void _sendAcks(); - bool _sendDisconnect(); - - uint16_t _getNextPacketId(); -}; diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Callbacks.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Callbacks.hpp deleted file mode 100644 index 7c3d63dd04..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Callbacks.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include - -#include "DisconnectReasons.hpp" -#include "MessageProperties.hpp" - -namespace AsyncMqttClientInternals { -// user callbacks -typedef std::function OnConnectUserCallback; -typedef std::function OnDisconnectUserCallback; -typedef std::function OnSubscribeUserCallback; -typedef std::function OnUnsubscribeUserCallback; -typedef std::function OnMessageUserCallback; -typedef std::function OnPublishUserCallback; - -// internal callbacks -typedef std::function OnConnAckInternalCallback; -typedef std::function OnPingRespInternalCallback; -typedef std::function OnSubAckInternalCallback; -typedef std::function OnUnsubAckInternalCallback; -typedef std::function OnMessageInternalCallback; -typedef std::function OnPublishInternalCallback; -typedef std::function OnPubRelInternalCallback; -typedef std::function OnPubAckInternalCallback; -typedef std::function OnPubRecInternalCallback; -typedef std::function OnPubCompInternalCallback; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/DisconnectReasons.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/DisconnectReasons.hpp deleted file mode 100644 index f4cbda8af4..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/DisconnectReasons.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -enum class AsyncMqttClientDisconnectReason : int8_t { - TCP_DISCONNECTED = 0, - - MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1, - MQTT_IDENTIFIER_REJECTED = 2, - MQTT_SERVER_UNAVAILABLE = 3, - MQTT_MALFORMED_CREDENTIALS = 4, - MQTT_NOT_AUTHORIZED = 5, - - ESP8266_NOT_ENOUGH_SPACE = 6, - - TLS_BAD_FINGERPRINT = 7 -}; diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Flags.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Flags.hpp deleted file mode 100644 index a1fb3e3c83..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Flags.hpp +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -namespace AsyncMqttClientInternals { -constexpr struct { - const uint8_t RESERVED = 0; - const uint8_t CONNECT = 1; - const uint8_t CONNACK = 2; - const uint8_t PUBLISH = 3; - const uint8_t PUBACK = 4; - const uint8_t PUBREC = 5; - const uint8_t PUBREL = 6; - const uint8_t PUBCOMP = 7; - const uint8_t SUBSCRIBE = 8; - const uint8_t SUBACK = 9; - const uint8_t UNSUBSCRIBE = 10; - const uint8_t UNSUBACK = 11; - const uint8_t PINGREQ = 12; - const uint8_t PINGRESP = 13; - const uint8_t DISCONNECT = 14; - const uint8_t RESERVED2 = 1; -} PacketType; - -constexpr struct { - const uint8_t CONNECT_RESERVED = 0x00; - const uint8_t CONNACK_RESERVED = 0x00; - const uint8_t PUBLISH_DUP = 0x08; - const uint8_t PUBLISH_QOS0 = 0x00; - const uint8_t PUBLISH_QOS1 = 0x02; - const uint8_t PUBLISH_QOS2 = 0x04; - const uint8_t PUBLISH_QOSRESERVED = 0x06; - const uint8_t PUBLISH_RETAIN = 0x01; - const uint8_t PUBACK_RESERVED = 0x00; - const uint8_t PUBREC_RESERVED = 0x00; - const uint8_t PUBREL_RESERVED = 0x02; - const uint8_t PUBCOMP_RESERVED = 0x00; - const uint8_t SUBSCRIBE_RESERVED = 0x02; - const uint8_t SUBACK_RESERVED = 0x00; - const uint8_t UNSUBSCRIBE_RESERVED = 0x02; - const uint8_t UNSUBACK_RESERVED = 0x00; - const uint8_t PINGREQ_RESERVED = 0x00; - const uint8_t PINGRESP_RESERVED = 0x00; - const uint8_t DISCONNECT_RESERVED = 0x00; - const uint8_t RESERVED2_RESERVED = 0x00; -} HeaderFlag; - -constexpr struct { - const uint8_t USERNAME = 0x80; - const uint8_t PASSWORD = 0x40; - const uint8_t WILL_RETAIN = 0x20; - const uint8_t WILL_QOS0 = 0x00; - const uint8_t WILL_QOS1 = 0x08; - const uint8_t WILL_QOS2 = 0x10; - const uint8_t WILL = 0x04; - const uint8_t CLEAN_SESSION = 0x02; - const uint8_t RESERVED = 0x00; -} ConnectFlag; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Helpers.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Helpers.hpp deleted file mode 100644 index 5737c02ef0..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Helpers.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -namespace AsyncMqttClientInternals { -class Helpers { - public: - static uint32_t decodeRemainingLength(char* bytes) { - uint32_t multiplier = 1; - uint32_t value = 0; - uint8_t currentByte = 0; - uint8_t encodedByte; - do { - encodedByte = bytes[currentByte++]; - value += (encodedByte & 127) * multiplier; - multiplier *= 128; - } while ((encodedByte & 128) != 0); - - return value; - } - - static uint8_t encodeRemainingLength(uint32_t remainingLength, char* destination) { - uint8_t currentByte = 0; - uint8_t bytesNeeded = 0; - - do { - uint8_t encodedByte = remainingLength % 128; - remainingLength /= 128; - if (remainingLength > 0) { - encodedByte = encodedByte | 128; - } - - destination[currentByte++] = encodedByte; - bytesNeeded++; - } while (remainingLength > 0); - - return bytesNeeded; - } -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/MessageProperties.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/MessageProperties.hpp deleted file mode 100644 index c04b59664f..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/MessageProperties.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -struct AsyncMqttClientMessageProperties { - uint8_t qos; - bool dup; - bool retain; -}; diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.cpp deleted file mode 100644 index f9091c2152..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "ConnAckPacket.hpp" - -using AsyncMqttClientInternals::ConnAckPacket; - -ConnAckPacket::ConnAckPacket(ParsingInformation* parsingInformation, OnConnAckInternalCallback callback) -: _parsingInformation(parsingInformation) -, _callback(callback) -, _bytePosition(0) -, _sessionPresent(false) -, _connectReturnCode(0) { -} - -ConnAckPacket::~ConnAckPacket() { -} - -void ConnAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { - char currentByte = data[(*currentBytePosition)++]; - if (_bytePosition++ == 0) { - _sessionPresent = (currentByte << 7) >> 7; - } else { - _connectReturnCode = currentByte; - _parsingInformation->bufferState = BufferState::NONE; - _callback(_sessionPresent, _connectReturnCode); - } -} - -void ConnAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { - (void)data; - (void)currentBytePosition; -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.hpp deleted file mode 100644 index 8be9ab1921..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "Packet.hpp" -#include "../ParsingInformation.hpp" -#include "../Callbacks.hpp" - -namespace AsyncMqttClientInternals { -class ConnAckPacket : public Packet { - public: - explicit ConnAckPacket(ParsingInformation* parsingInformation, OnConnAckInternalCallback callback); - ~ConnAckPacket(); - - void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); - void parsePayload(char* data, size_t len, size_t* currentBytePosition); - - private: - ParsingInformation* _parsingInformation; - OnConnAckInternalCallback _callback; - - uint8_t _bytePosition; - bool _sessionPresent; - uint8_t _connectReturnCode; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/Packet.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/Packet.hpp deleted file mode 100644 index 9552cf06a9..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/Packet.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -namespace AsyncMqttClientInternals { -class Packet { - public: - virtual ~Packet() {} - - virtual void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) = 0; - virtual void parsePayload(char* data, size_t len, size_t* currentBytePosition) = 0; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.cpp deleted file mode 100644 index 2a939aac96..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "PingRespPacket.hpp" - -using AsyncMqttClientInternals::PingRespPacket; - -PingRespPacket::PingRespPacket(ParsingInformation* parsingInformation, OnPingRespInternalCallback callback) -: _parsingInformation(parsingInformation) -, _callback(callback) { -} - -PingRespPacket::~PingRespPacket() { -} - -void PingRespPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { - (void)data; - (void)currentBytePosition; -} - -void PingRespPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { - (void)data; - (void)currentBytePosition; -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.hpp deleted file mode 100644 index 043a730323..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "Packet.hpp" -#include "../ParsingInformation.hpp" -#include "../Callbacks.hpp" - -namespace AsyncMqttClientInternals { -class PingRespPacket : public Packet { - public: - explicit PingRespPacket(ParsingInformation* parsingInformation, OnPingRespInternalCallback callback); - ~PingRespPacket(); - - void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); - void parsePayload(char* data, size_t len, size_t* currentBytePosition); - - private: - ParsingInformation* _parsingInformation; - OnPingRespInternalCallback _callback; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.cpp deleted file mode 100644 index efa5fa4102..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "PubAckPacket.hpp" - -using AsyncMqttClientInternals::PubAckPacket; - -PubAckPacket::PubAckPacket(ParsingInformation* parsingInformation, OnPubAckInternalCallback callback) -: _parsingInformation(parsingInformation) -, _callback(callback) -, _bytePosition(0) -, _packetIdMsb(0) -, _packetId(0) { -} - -PubAckPacket::~PubAckPacket() { -} - -void PubAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { - char currentByte = data[(*currentBytePosition)++]; - if (_bytePosition++ == 0) { - _packetIdMsb = currentByte; - } else { - _packetId = currentByte | _packetIdMsb << 8; - _parsingInformation->bufferState = BufferState::NONE; - _callback(_packetId); - } -} - -void PubAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { - (void)data; - (void)currentBytePosition; -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.hpp deleted file mode 100644 index bd00142d0f..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "Packet.hpp" -#include "../ParsingInformation.hpp" -#include "../Callbacks.hpp" - -namespace AsyncMqttClientInternals { -class PubAckPacket : public Packet { - public: - explicit PubAckPacket(ParsingInformation* parsingInformation, OnPubAckInternalCallback callback); - ~PubAckPacket(); - - void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); - void parsePayload(char* data, size_t len, size_t* currentBytePosition); - - private: - ParsingInformation* _parsingInformation; - OnPubAckInternalCallback _callback; - - uint8_t _bytePosition; - char _packetIdMsb; - uint16_t _packetId; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.cpp deleted file mode 100644 index 2b3e00ded1..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "PubCompPacket.hpp" - -using AsyncMqttClientInternals::PubCompPacket; - -PubCompPacket::PubCompPacket(ParsingInformation* parsingInformation, OnPubCompInternalCallback callback) -: _parsingInformation(parsingInformation) -, _callback(callback) -, _bytePosition(0) -, _packetIdMsb(0) -, _packetId(0) { -} - -PubCompPacket::~PubCompPacket() { -} - -void PubCompPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { - char currentByte = data[(*currentBytePosition)++]; - if (_bytePosition++ == 0) { - _packetIdMsb = currentByte; - } else { - _packetId = currentByte | _packetIdMsb << 8; - _parsingInformation->bufferState = BufferState::NONE; - _callback(_packetId); - } -} - -void PubCompPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { - (void)data; - (void)currentBytePosition; -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.hpp deleted file mode 100644 index 17c1db4888..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "Packet.hpp" -#include "../ParsingInformation.hpp" -#include "../Callbacks.hpp" - -namespace AsyncMqttClientInternals { -class PubCompPacket : public Packet { - public: - explicit PubCompPacket(ParsingInformation* parsingInformation, OnPubCompInternalCallback callback); - ~PubCompPacket(); - - void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); - void parsePayload(char* data, size_t len, size_t* currentBytePosition); - - private: - ParsingInformation* _parsingInformation; - OnPubCompInternalCallback _callback; - - uint8_t _bytePosition; - char _packetIdMsb; - uint16_t _packetId; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.cpp deleted file mode 100644 index ec535c613f..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "PubRecPacket.hpp" - -using AsyncMqttClientInternals::PubRecPacket; - -PubRecPacket::PubRecPacket(ParsingInformation* parsingInformation, OnPubRecInternalCallback callback) -: _parsingInformation(parsingInformation) -, _callback(callback) -, _bytePosition(0) -, _packetIdMsb(0) -, _packetId(0) { -} - -PubRecPacket::~PubRecPacket() { -} - -void PubRecPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { - char currentByte = data[(*currentBytePosition)++]; - if (_bytePosition++ == 0) { - _packetIdMsb = currentByte; - } else { - _packetId = currentByte | _packetIdMsb << 8; - _parsingInformation->bufferState = BufferState::NONE; - _callback(_packetId); - } -} - -void PubRecPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { - (void)data; - (void)currentBytePosition; -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.hpp deleted file mode 100644 index 910130a2b5..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "Packet.hpp" -#include "../ParsingInformation.hpp" -#include "../Callbacks.hpp" - -namespace AsyncMqttClientInternals { -class PubRecPacket : public Packet { - public: - explicit PubRecPacket(ParsingInformation* parsingInformation, OnPubRecInternalCallback callback); - ~PubRecPacket(); - - void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); - void parsePayload(char* data, size_t len, size_t* currentBytePosition); - - private: - ParsingInformation* _parsingInformation; - OnPubRecInternalCallback _callback; - - uint8_t _bytePosition; - char _packetIdMsb; - uint16_t _packetId; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.cpp deleted file mode 100644 index 2d5abde0e8..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "PubRelPacket.hpp" - -using AsyncMqttClientInternals::PubRelPacket; - -PubRelPacket::PubRelPacket(ParsingInformation* parsingInformation, OnPubRelInternalCallback callback) -: _parsingInformation(parsingInformation) -, _callback(callback) -, _bytePosition(0) -, _packetIdMsb(0) -, _packetId(0) { -} - -PubRelPacket::~PubRelPacket() { -} - -void PubRelPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { - char currentByte = data[(*currentBytePosition)++]; - if (_bytePosition++ == 0) { - _packetIdMsb = currentByte; - } else { - _packetId = currentByte | _packetIdMsb << 8; - _parsingInformation->bufferState = BufferState::NONE; - _callback(_packetId); - } -} - -void PubRelPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { - (void)data; - (void)currentBytePosition; -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.hpp deleted file mode 100644 index edea3d53b1..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "Packet.hpp" -#include "../ParsingInformation.hpp" -#include "../Callbacks.hpp" - -namespace AsyncMqttClientInternals { -class PubRelPacket : public Packet { - public: - explicit PubRelPacket(ParsingInformation* parsingInformation, OnPubRelInternalCallback callback); - ~PubRelPacket(); - - void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); - void parsePayload(char* data, size_t len, size_t* currentBytePosition); - - private: - ParsingInformation* _parsingInformation; - OnPubRelInternalCallback _callback; - - uint8_t _bytePosition; - char _packetIdMsb; - uint16_t _packetId; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.cpp deleted file mode 100644 index dab30a1542..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "PublishPacket.hpp" - -using AsyncMqttClientInternals::PublishPacket; - -PublishPacket::PublishPacket(ParsingInformation* parsingInformation, OnMessageInternalCallback dataCallback, OnPublishInternalCallback completeCallback) -: _parsingInformation(parsingInformation) -, _dataCallback(dataCallback) -, _completeCallback(completeCallback) -, _dup(false) -, _qos(0) -, _retain(0) -, _bytePosition(0) -, _topicLengthMsb(0) -, _topicLength(0) -, _ignore(false) -, _packetIdMsb(0) -, _packetId(0) -, _payloadLength(0) -, _payloadBytesRead(0) { - _dup = _parsingInformation->packetFlags & HeaderFlag.PUBLISH_DUP; - _retain = _parsingInformation->packetFlags & HeaderFlag.PUBLISH_RETAIN; - char qosMasked = _parsingInformation->packetFlags & 0x06; - switch (qosMasked) { - case HeaderFlag.PUBLISH_QOS0: - _qos = 0; - break; - case HeaderFlag.PUBLISH_QOS1: - _qos = 1; - break; - case HeaderFlag.PUBLISH_QOS2: - _qos = 2; - break; - } -} - -PublishPacket::~PublishPacket() { -} - -void PublishPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { - char currentByte = data[(*currentBytePosition)++]; - if (_bytePosition == 0) { - _topicLengthMsb = currentByte; - } else if (_bytePosition == 1) { - _topicLength = currentByte | _topicLengthMsb << 8; - if (_topicLength > _parsingInformation->maxTopicLength) { - _ignore = true; - } else { - _parsingInformation->topicBuffer[_topicLength] = '\0'; - } - } else if (_bytePosition >= 2 && _bytePosition < 2 + _topicLength) { - // Starting from here, _ignore might be true - if (!_ignore) _parsingInformation->topicBuffer[_bytePosition - 2] = currentByte; - if (_bytePosition == 2 + _topicLength - 1 && _qos == 0) { - _preparePayloadHandling(_parsingInformation->remainingLength - (_bytePosition + 1)); - return; - } - } else if (_bytePosition == 2 + _topicLength) { - _packetIdMsb = currentByte; - } else { - _packetId = currentByte | _packetIdMsb << 8; - _preparePayloadHandling(_parsingInformation->remainingLength - (_bytePosition + 1)); - } - _bytePosition++; -} - -void PublishPacket::_preparePayloadHandling(uint32_t payloadLength) { - _payloadLength = payloadLength; - if (payloadLength == 0) { - _parsingInformation->bufferState = BufferState::NONE; - if (!_ignore) { - _dataCallback(_parsingInformation->topicBuffer, nullptr, _qos, _dup, _retain, 0, 0, 0, _packetId); - _completeCallback(_packetId, _qos); - } - } else { - _parsingInformation->bufferState = BufferState::PAYLOAD; - } -} - -void PublishPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { - size_t remainToRead = len - (*currentBytePosition); - if (_payloadBytesRead + remainToRead > _payloadLength) remainToRead = _payloadLength - _payloadBytesRead; - - if (!_ignore) _dataCallback(_parsingInformation->topicBuffer, data + (*currentBytePosition), _qos, _dup, _retain, remainToRead, _payloadBytesRead, _payloadLength, _packetId); - _payloadBytesRead += remainToRead; - (*currentBytePosition) += remainToRead; - - if (_payloadBytesRead == _payloadLength) { - _parsingInformation->bufferState = BufferState::NONE; - if (!_ignore) _completeCallback(_packetId, _qos); - } -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.hpp deleted file mode 100644 index 9dc19f0aa2..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "Packet.hpp" -#include "../Flags.hpp" -#include "../ParsingInformation.hpp" -#include "../Callbacks.hpp" - -namespace AsyncMqttClientInternals { -class PublishPacket : public Packet { - public: - explicit PublishPacket(ParsingInformation* parsingInformation, OnMessageInternalCallback dataCallback, OnPublishInternalCallback completeCallback); - ~PublishPacket(); - - void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); - void parsePayload(char* data, size_t len, size_t* currentBytePosition); - - private: - ParsingInformation* _parsingInformation; - OnMessageInternalCallback _dataCallback; - OnPublishInternalCallback _completeCallback; - - void _preparePayloadHandling(uint32_t payloadLength); - - bool _dup; - uint8_t _qos; - bool _retain; - - uint8_t _bytePosition; - char _topicLengthMsb; - uint16_t _topicLength; - bool _ignore; - char _packetIdMsb; - uint16_t _packetId; - uint32_t _payloadLength; - uint32_t _payloadBytesRead; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.cpp deleted file mode 100644 index ab89965506..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "SubAckPacket.hpp" - -using AsyncMqttClientInternals::SubAckPacket; - -SubAckPacket::SubAckPacket(ParsingInformation* parsingInformation, OnSubAckInternalCallback callback) -: _parsingInformation(parsingInformation) -, _callback(callback) -, _bytePosition(0) -, _packetIdMsb(0) -, _packetId(0) { -} - -SubAckPacket::~SubAckPacket() { -} - -void SubAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { - char currentByte = data[(*currentBytePosition)++]; - if (_bytePosition++ == 0) { - _packetIdMsb = currentByte; - } else { - _packetId = currentByte | _packetIdMsb << 8; - _parsingInformation->bufferState = BufferState::PAYLOAD; - } -} - -void SubAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { - char status = data[(*currentBytePosition)++]; - - /* switch (status) { - case 0: - Serial.println("Success QoS 0"); - break; - case 1: - Serial.println("Success QoS 1"); - break; - case 2: - Serial.println("Success QoS 2"); - break; - case 0x80: - Serial.println("Failure"); - break; - } */ - - _parsingInformation->bufferState = BufferState::NONE; - _callback(_packetId, status); -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.hpp deleted file mode 100644 index 011b800a04..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "Packet.hpp" -#include "../ParsingInformation.hpp" -#include "../Callbacks.hpp" - -namespace AsyncMqttClientInternals { -class SubAckPacket : public Packet { - public: - explicit SubAckPacket(ParsingInformation* parsingInformation, OnSubAckInternalCallback callback); - ~SubAckPacket(); - - void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); - void parsePayload(char* data, size_t len, size_t* currentBytePosition); - - private: - ParsingInformation* _parsingInformation; - OnSubAckInternalCallback _callback; - - uint8_t _bytePosition; - char _packetIdMsb; - uint16_t _packetId; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.cpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.cpp deleted file mode 100644 index a44943d171..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "UnsubAckPacket.hpp" - -using AsyncMqttClientInternals::UnsubAckPacket; - -UnsubAckPacket::UnsubAckPacket(ParsingInformation* parsingInformation, OnUnsubAckInternalCallback callback) -: _parsingInformation(parsingInformation) -, _callback(callback) -, _bytePosition(0) -, _packetIdMsb(0) -, _packetId(0) { -} - -UnsubAckPacket::~UnsubAckPacket() { -} - -void UnsubAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { - char currentByte = data[(*currentBytePosition)++]; - if (_bytePosition++ == 0) { - _packetIdMsb = currentByte; - } else { - _packetId = currentByte | _packetIdMsb << 8; - _parsingInformation->bufferState = BufferState::NONE; - _callback(_packetId); - } -} - -void UnsubAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { - (void)data; - (void)currentBytePosition; -} diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.hpp deleted file mode 100644 index ab5b9c5cb3..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "Packet.hpp" -#include "../ParsingInformation.hpp" -#include "../Callbacks.hpp" - -namespace AsyncMqttClientInternals { -class UnsubAckPacket : public Packet { - public: - explicit UnsubAckPacket(ParsingInformation* parsingInformation, OnUnsubAckInternalCallback callback); - ~UnsubAckPacket(); - - void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); - void parsePayload(char* data, size_t len, size_t* currentBytePosition); - - private: - ParsingInformation* _parsingInformation; - OnUnsubAckInternalCallback _callback; - - uint8_t _bytePosition; - char _packetIdMsb; - uint16_t _packetId; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/ParsingInformation.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/ParsingInformation.hpp deleted file mode 100644 index 2d46f27f89..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/ParsingInformation.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -namespace AsyncMqttClientInternals { -enum class BufferState : uint8_t { - NONE = 0, - REMAINING_LENGTH = 2, - VARIABLE_HEADER = 3, - PAYLOAD = 4 -}; - -struct ParsingInformation { - BufferState bufferState; - - uint16_t maxTopicLength; - char* topicBuffer; - - uint8_t packetType; - uint16_t packetFlags; - uint32_t remainingLength; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Storage.hpp b/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Storage.hpp deleted file mode 100644 index 725307b7f2..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/AsyncMqttClient/Storage.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -namespace AsyncMqttClientInternals { -struct PendingPubRel { - uint16_t packetId; -}; - -struct PendingAck { - uint8_t packetType; - uint8_t headerFlag; - uint16_t packetId; -}; -} // namespace AsyncMqttClientInternals diff --git a/wled00/src/dependencies/async-mqtt-client/LICENSE b/wled00/src/dependencies/async-mqtt-client/LICENSE deleted file mode 100644 index a6183c687c..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Marvin Roger - -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. diff --git a/wled00/src/dependencies/async-mqtt-client/README.md b/wled00/src/dependencies/async-mqtt-client/README.md deleted file mode 100644 index 7ea6db1a7e..0000000000 --- a/wled00/src/dependencies/async-mqtt-client/README.md +++ /dev/null @@ -1,18 +0,0 @@ -Async MQTT client for ESP8266 and ESP32 (Github: https://github.com/marvinroger/async-mqtt-client) -============================= - -[![Build Status](https://img.shields.io/travis/marvinroger/async-mqtt-client/master.svg?style=flat-square)](https://travis-ci.org/marvinroger/async-mqtt-client) - -An Arduino for ESP8266 and ESP32 asynchronous [MQTT](http://mqtt.org/) client implementation, built on [me-no-dev/ESPAsyncTCP (ESP8266)](https://github.com/me-no-dev/ESPAsyncTCP) | [me-no-dev/AsyncTCP (ESP32)](https://github.com/me-no-dev/AsyncTCP) . -## Features - -* Compliant with the 3.1.1 version of the protocol -* Fully asynchronous -* Subscribe at QoS 0, 1 and 2 -* Publish at QoS 0, 1 and 2 -* SSL/TLS support -* Available in the [PlatformIO registry](http://platformio.org/lib/show/346/AsyncMqttClient) - -## Requirements, installation and usage - -The project is documented in the [/docs folder](docs). diff --git a/wled00/src/dependencies/network/Network.cpp b/wled00/src/dependencies/network/Network.cpp index d86bf127fd..dbc2707887 100644 --- a/wled00/src/dependencies/network/Network.cpp +++ b/wled00/src/dependencies/network/Network.cpp @@ -73,17 +73,13 @@ void NetworkClass::localMAC(uint8_t* MAC) bool NetworkClass::isConnected() { -#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED) || ETH.localIP()[0] != 0; -#else - return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED); -#endif + return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED) || isEthernet(); } bool NetworkClass::isEthernet() { #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - return (ETH.localIP()[0] != 0); + return (ETH.localIP()[0] != 0) && ETH.linkUp(); #endif return false; } diff --git a/wled00/src/dependencies/time/DS1307RTC.h b/wled00/src/dependencies/time/DS1307RTC.h index 551ae99658..bc272701f2 100644 --- a/wled00/src/dependencies/time/DS1307RTC.h +++ b/wled00/src/dependencies/time/DS1307RTC.h @@ -7,6 +7,7 @@ #define DS1307RTC_h #include "TimeLib.h" +#include "Wire.h" // library interface description class DS1307RTC diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 60774d7010..fab67bc600 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -6,7 +6,7 @@ #define UDP_SEG_SIZE 36 #define SEG_OFFSET (41) -#define WLEDPACKETSIZE (41+(MAX_NUM_SEGMENTS*UDP_SEG_SIZE)+0) +#define WLEDPACKETSIZE (41+(WS2812FX::getMaxSegments()*UDP_SEG_SIZE)+0) #define UDP_IN_MAXSIZE 1472 #define PRESUMED_NETWORK_DELAY 3 //how many ms could it take on avg to reach the receiver? This will be added to transmitted times @@ -55,7 +55,7 @@ void notify(byte callMode, bool followUp) //0: old 1: supports white 2: supports secondary color //3: supports FX intensity, 24 byte packet 4: supports transitionDelay 5: sup palette //6: supports timebase syncing, 29 byte packet 7: supports tertiary color 8: supports sys time sync, 36 byte packet - //9: supports sync groups, 37 byte packet 10: supports CCT, 39 byte packet 11: per segment options, variable packet length (40+MAX_NUM_SEGMENTS*3) + //9: supports sync groups, 37 byte packet 10: supports CCT, 39 byte packet 11: per segment options, variable packet length (40+WS2812FX::getMaxSegments()*3) //12: enhanced effect sliders, 2D & mapping options udpOut[11] = 12; col = mainseg.colors[1]; @@ -104,7 +104,7 @@ void notify(byte callMode, bool followUp) udpOut[40] = UDP_SEG_SIZE; //size of each loop iteration (one segment) size_t s = 0, nsegs = strip.getSegmentsNum(); for (size_t i = 0; i < nsegs; i++) { - Segment &selseg = strip.getSegment(i); + const Segment &selseg = strip.getSegment(i); if (!selseg.isActive()) continue; unsigned ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte udpOut[0 +ofs] = s; @@ -177,7 +177,7 @@ void notify(byte callMode, bool followUp) memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE); packetSize += UDP_SEG_SIZE; if (packetSize + UDP_SEG_SIZE < bufferSize) continue; - DEBUG_PRINTF_P(PSTR("ESP-NOW sending packet: %d (%d)\n"), (int)buffer.packet, packetSize+3); + DEBUG_PRINTF_P(PSTR("ESP-NOW sending packet: %d (%u)\n"), (int)buffer.packet, packetSize+3); err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize+3); buffer.packet++; packetSize = 0; @@ -206,7 +206,7 @@ void notify(byte callMode, bool followUp) notificationCount = followUp ? notificationCount + 1 : 0; } -void parseNotifyPacket(uint8_t *udpIn) { +static void parseNotifyPacket(const uint8_t *udpIn) { //ignore notification if received within a second after sending a notification ourselves if (millis() - notificationSentTime < 1000) return; if (udpIn[1] > 199) return; //do not receive custom versions @@ -225,21 +225,19 @@ void parseNotifyPacket(uint8_t *udpIn) { // set transition time before making any segment changes if (version > 3) { - if (fadeTransition) { - jsonTransitionOnce = true; - strip.setTransition(((udpIn[17] << 0) & 0xFF) + ((udpIn[18] << 8) & 0xFF00)); - } + jsonTransitionOnce = true; + strip.setTransition(((udpIn[17] << 0) & 0xFF) + ((udpIn[18] << 8) & 0xFF00)); } //apply colors from notification to main segment, only if not syncing full segments if ((receiveNotificationColor || !someSel) && (version < 11 || !receiveSegmentOptions)) { // primary color, only apply white if intented (version > 0) - strip.setColor(0, RGBW32(udpIn[3], udpIn[4], udpIn[5], (version > 0) ? udpIn[10] : 0)); + strip.getMainSegment().setColor(0, RGBW32(udpIn[3], udpIn[4], udpIn[5], (version > 0) ? udpIn[10] : 0)); if (version > 1) { - strip.setColor(1, RGBW32(udpIn[12], udpIn[13], udpIn[14], udpIn[15])); // secondary color + strip.getMainSegment().setColor(1, RGBW32(udpIn[12], udpIn[13], udpIn[14], udpIn[15])); // secondary color } if (version > 6) { - strip.setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color + strip.getMainSegment().setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color if (version > 9 && udpIn[37] < 255) { // valid CCT/Kelvin value unsigned cct = udpIn[38]; if (udpIn[37] > 0) { //Kelvin @@ -260,20 +258,21 @@ void parseNotifyPacket(uint8_t *udpIn) { // are we syncing bounds and slave has more active segments than master? if (receiveSegmentBounds && numSrcSegs < strip.getActiveSegmentsNum()) { DEBUG_PRINTLN(F("Removing excessive segments.")); - for (size_t i=strip.getSegmentsNum(); i>numSrcSegs; i--) { - if (strip.getSegment(i).isActive()) { - strip.setSegment(i-1,0,0); // delete segment - } + strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" + for (size_t i=strip.getSegmentsNum(); i>numSrcSegs && i>0; i--) { + Segment &seg = strip.getSegment(i-1); + if (seg.isActive()) seg.deactivate(); // delete segment } + strip.resume(); } size_t inactiveSegs = 0; - for (size_t i = 0; i < numSrcSegs && i < strip.getMaxSegments(); i++) { + for (size_t i = 0; i < numSrcSegs && i < WS2812FX::getMaxSegments(); i++) { unsigned ofs = 41 + i*udpIn[40]; //start of segment offset byte unsigned id = udpIn[0 +ofs]; DEBUG_PRINTF_P(PSTR("UDP segment received: %u\n"), id); if (id > strip.getSegmentsNum()) break; else if (id == strip.getSegmentsNum()) { - if (receiveSegmentBounds && id < strip.getMaxSegments()) strip.appendSegment(); + if (receiveSegmentBounds && id < WS2812FX::getMaxSegments()) strip.appendSegment(); else break; } DEBUG_PRINTF_P(PSTR("UDP segment check: %u\n"), id); @@ -300,7 +299,7 @@ void parseNotifyPacket(uint8_t *udpIn) { if (!receiveSegmentOptions) { DEBUG_PRINTF_P(PSTR("Set segment w/o options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setUp(start, stop, selseg.grouping, selseg.spacing, offset, startY, stopY); + selseg.setGeometry(start, stop, selseg.grouping, selseg.spacing, offset, startY, stopY, selseg.map1D2D); strip.resume(); continue; // we do receive bounds, but not options } @@ -328,26 +327,26 @@ void parseNotifyPacket(uint8_t *udpIn) { // freeze, reset should never be synced // LSB to MSB: select, reverse, on, mirror, freeze, reset, reverse_y, mirror_y, transpose, map1d2d (3), ssim (2), set (2) DEBUG_PRINTF_P(PSTR("Apply options: %u\n"), id); - selseg.options = (selseg.options & 0b0000000000110001U) | (udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset + selseg.options = (selseg.options & 0b0000000000110001U) | ((uint16_t)udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset if (applyEffects) { DEBUG_PRINTF_P(PSTR("Apply sliders: %u\n"), id); selseg.custom1 = udpIn[29+ofs]; selseg.custom2 = udpIn[30+ofs]; selseg.custom3 = udpIn[31+ofs] & 0x1F; selseg.check1 = (udpIn[31+ofs]>>5) & 0x1; - selseg.check1 = (udpIn[31+ofs]>>6) & 0x1; - selseg.check1 = (udpIn[31+ofs]>>7) & 0x1; + selseg.check2 = (udpIn[31+ofs]>>6) & 0x1; + selseg.check3 = (udpIn[31+ofs]>>7) & 0x1; } } if (receiveSegmentBounds) { DEBUG_PRINTF_P(PSTR("Set segment w/ options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setUp(start, stop, udpIn[5+ofs], udpIn[6+ofs], offset, startY, stopY); + selseg.setGeometry(start, stop, udpIn[5+ofs], udpIn[6+ofs], offset, startY, stopY, selseg.map1D2D); strip.resume(); } else { DEBUG_PRINTF_P(PSTR("Set segment grouping: %d [%d,%d]\n"), id, (int)udpIn[5+ofs], (int)udpIn[6+ofs]); strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setUp(selseg.start, selseg.stop, udpIn[5+ofs], udpIn[6+ofs], selseg.offset, selseg.startY, selseg.stopY); + selseg.setGeometry(selseg.start, selseg.stop, udpIn[5+ofs], udpIn[6+ofs], selseg.offset, selseg.startY, selseg.stopY, selseg.map1D2D); strip.resume(); } } @@ -407,32 +406,27 @@ void parseNotifyPacket(uint8_t *udpIn) { stateUpdated(CALL_MODE_NOTIFICATION); } +// realtimeLock() is called from UDP notifications, JSON API or serial Ada void realtimeLock(uint32_t timeoutMs, byte md) { if (!realtimeMode && !realtimeOverride) { - unsigned stop, start; if (useMainSegmentOnly) { Segment& mainseg = strip.getMainSegment(); - start = mainseg.start; - stop = mainseg.stop; + mainseg.clear(); // clear entire segment (in case sender transmits less pixels) mainseg.freeze = true; + // if WLED was off and using main segment only, freeze non-main segments so they stay off + if (bri == 0) { + for (size_t s = 0; s < strip.getSegmentsNum(); s++) strip.getSegment(s).freeze = true; + } } else { - start = 0; - stop = strip.getLengthTotal(); + // clear entire strip + strip.fill(BLACK); } - // clear strip/segment - for (size_t i = start; i < stop; i++) strip.setPixelColor(i,BLACK); - // if WLED was off and using main segment only, freeze non-main segments so they stay off - if (useMainSegmentOnly && bri == 0) { - for (size_t s=0; s < strip.getSegmentsNum(); s++) { - strip.getSegment(s).freeze = true; - } + // if strip is off (bri==0) and not already in RTM + if (briT == 0) { + strip.setBrightness(briLast, true); } } - // if strip is off (bri==0) and not already in RTM - if (briT == 0 && !realtimeMode && !realtimeOverride) { - strip.setBrightness(scaledBri(briLast), true); - } if (realtimeTimeout != UINT32_MAX) { realtimeTimeout = (timeoutMs == 255001 || timeoutMs == 65000) ? UINT32_MAX : millis() + timeoutMs; @@ -440,19 +434,20 @@ void realtimeLock(uint32_t timeoutMs, byte md) realtimeMode = md; if (realtimeOverride) return; - if (arlsForceMaxBri) strip.setBrightness(scaledBri(255), true); + if (arlsForceMaxBri) strip.setBrightness(255, true); if (briT > 0 && md == REALTIME_MODE_GENERIC) strip.show(); } void exitRealtime() { if (!realtimeMode) return; if (realtimeOverride == REALTIME_OVERRIDE_ONCE) realtimeOverride = REALTIME_OVERRIDE_NONE; - strip.setBrightness(scaledBri(bri), true); + strip.setBrightness(bri, true); realtimeTimeout = 0; // cancel realtime mode immediately realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately realtimeIP[0] = 0; if (useMainSegmentOnly) { // unfreeze live segment again strip.getMainSegment().freeze = false; + strip.trigger(); } else { strip.show(); // possible fix for #3589 } @@ -462,7 +457,7 @@ void exitRealtime() { #define TMP2NET_OUT_PORT 65442 -void sendTPM2Ack() { +static void sendTPM2Ack() { notifierUdp.beginPacket(notifierUdp.remoteIP(), TMP2NET_OUT_PORT); uint8_t response_ack = 0xac; notifierUdp.write(&response_ack, 1); @@ -482,7 +477,8 @@ void handleNotifications() if (e131NewData && millis() - strip.getLastShow() > 15) { e131NewData = false; - strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); } //unlock strip when realtime UDP times out @@ -509,15 +505,13 @@ void handleNotifications() uint8_t lbuf[packetSize]; rgbUdp.read(lbuf, packetSize); realtimeLock(realtimeTimeoutMs, REALTIME_MODE_HYPERION); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; - unsigned id = 0; + if (realtimeOverride) return; unsigned totalLen = strip.getLengthTotal(); - for (size_t i = 0; i < packetSize -2; i += 3) - { + for (size_t i = 0, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) { setRealtimePixel(id, lbuf[i], lbuf[i+1], lbuf[i+2], 0); - id++; if (id >= totalLen) break; } - if (!(realtimeMode && useMainSegmentOnly)) strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); return; } } @@ -571,115 +565,88 @@ void handleNotifications() return; } - if (!receiveDirect) return; - - //TPM2.NET - if (udpIn[0] == 0x9c) - { - //WARNING: this code assumes that the final TMP2.NET payload is evenly distributed if using multiple packets (ie. frame size is constant) - //if the number of LEDs in your installation doesn't allow that, please include padding bytes at the end of the last packet - byte tpmType = udpIn[1]; - if (tpmType == 0xaa) { //TPM2.NET polling, expect answer - sendTPM2Ack(); return; - } - if (tpmType != 0xda) return; //return if notTPM2.NET data + if (receiveDirect) { + //TPM2.NET + if (udpIn[0] == 0x9c) { + //WARNING: this code assumes that the final TMP2.NET payload is evenly distributed if using multiple packets (ie. frame size is constant) + //if the number of LEDs in your installation doesn't allow that, please include padding bytes at the end of the last packet + byte tpmType = udpIn[1]; + if (tpmType == 0xaa) { //TPM2.NET polling, expect answer + sendTPM2Ack(); return; + } + if (tpmType != 0xda) return; //return if notTPM2.NET data - realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); - realtimeLock(realtimeTimeoutMs, REALTIME_MODE_TPM2NET); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); + realtimeLock(realtimeTimeoutMs, REALTIME_MODE_TPM2NET); + if (realtimeOverride) return; - tpmPacketCount++; //increment the packet count - if (tpmPacketCount == 1) tpmPayloadFrameSize = (udpIn[2] << 8) + udpIn[3]; //save frame size for the whole payload if this is the first packet - byte packetNum = udpIn[4]; //starts with 1! - byte numPackets = udpIn[5]; + tpmPacketCount++; //increment the packet count + if (tpmPacketCount == 1) tpmPayloadFrameSize = (udpIn[2] << 8) + udpIn[3]; //save frame size for the whole payload if this is the first packet + byte packetNum = udpIn[4]; //starts with 1! + byte numPackets = udpIn[5]; - unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED - unsigned totalLen = strip.getLengthTotal(); - for (size_t i = 6; i < tpmPayloadFrameSize + 4U; i += 3) - { - if (id < totalLen) - { + unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED + unsigned totalLen = strip.getLengthTotal(); + for (size_t i = 6; i < tpmPayloadFrameSize + 4U && id < totalLen; i += 3, id++) { setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - id++; } - else break; - } - if (tpmPacketCount == numPackets) //reset packet count and show if all packets were received - { - tpmPacketCount = 0; - strip.show(); - } - return; - } - - //UDP realtime: 1 warls 2 drgb 3 drgbw - if (udpIn[0] > 0 && udpIn[0] < 5) - { - realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); - DEBUG_PRINTLN(realtimeIP); - if (packetSize < 2) return; - - if (udpIn[1] == 0) - { - realtimeTimeout = 0; + if (tpmPacketCount == numPackets) { //reset packet count and show if all packets were received + tpmPacketCount = 0; + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); + } return; - } else { - realtimeLock(udpIn[1]*1000 +1, REALTIME_MODE_UDP); } - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; - unsigned totalLen = strip.getLengthTotal(); - if (udpIn[0] == 1 && packetSize > 5) //warls - { - for (size_t i = 2; i < packetSize -3; i += 4) - { - setRealtimePixel(udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3], 0); - } - } else if (udpIn[0] == 2 && packetSize > 4) //drgb - { - unsigned id = 0; - for (size_t i = 2; i < packetSize -2; i += 3) - { - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); + //UDP realtime: 1 warls 2 drgb 3 drgbw 4 dnrgb 5 dnrgbw + if (udpIn[0] > 0 && udpIn[0] < 6) { + realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); + DEBUG_PRINTLN(realtimeIP); + if (packetSize < 2) return; - id++; if (id >= totalLen) break; + if (udpIn[1] == 0) { + realtimeTimeout = 0; // cancel realtime mode immediately + return; + } else { + realtimeLock(udpIn[1]*1000 +1, REALTIME_MODE_UDP); } - } else if (udpIn[0] == 3 && packetSize > 6) //drgbw - { - unsigned id = 0; - for (size_t i = 2; i < packetSize -3; i += 4) - { - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); + if (realtimeOverride) return; - id++; if (id >= totalLen) break; - } - } else if (udpIn[0] == 4 && packetSize > 7) //dnrgb - { - unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); - for (size_t i = 4; i < packetSize -2; i += 3) - { - if (id >= totalLen) break; - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - id++; - } - } else if (udpIn[0] == 5 && packetSize > 8) //dnrgbw - { - unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); - for (size_t i = 4; i < packetSize -2; i += 4) - { - if (id >= totalLen) break; - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); - id++; + unsigned totalLen = strip.getLengthTotal(); + if (udpIn[0] == 1 && packetSize > 5) { //warls + for (size_t i = 2; i < packetSize -3; i += 4) { + setRealtimePixel(udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3], 0); + } + } else if (udpIn[0] == 2 && packetSize > 4) { //drgb + for (size_t i = 2, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) + { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); + } + } else if (udpIn[0] == 3 && packetSize > 6) { //drgbw + for (size_t i = 2, id = 0; i < packetSize -3 && id < totalLen; i += 4, id++) { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); + } + } else if (udpIn[0] == 4 && packetSize > 7) { //dnrgb + unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); + for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 3, id++) { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); + } + } else if (udpIn[0] == 5 && packetSize > 8) { //dnrgbw + unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); + for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 4, id++) { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); + } } + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); + return; } - strip.show(); - return; } // API over UDP udpIn[packetSize] = '\0'; - if (requestJSONBufferLock(18)) { + if (requestJSONBufferLock(JSON_LOCK_NOTIFY)) { if (udpIn[0] >= 'A' && udpIn[0] <= 'Z') { //HTTP API String apireq = "win"; apireq += '&'; // reduce flash string usage apireq += (char*)udpIn; @@ -691,26 +658,15 @@ void handleNotifications() } releaseJSONBufferLock(); } + + UsermodManager::onUdpPacket(udpIn, packetSize); } void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w) { unsigned pix = i + arlsOffset; - if (pix < strip.getLengthTotal()) { - if (!arlsDisableGammaCorrection && gammaCorrectCol) { - r = gamma8(r); - g = gamma8(g); - b = gamma8(b); - w = gamma8(w); - } - if (useMainSegmentOnly) { - Segment &seg = strip.getMainSegment(); - if (pixsetup(); } -void UsermodManager::connected() { for (unsigned i = 0; i < numMods; i++) ums[i]->connected(); } -void UsermodManager::loop() { for (unsigned i = 0; i < numMods; i++) ums[i]->loop(); } -void UsermodManager::handleOverlayDraw() { for (unsigned i = 0; i < numMods; i++) ums[i]->handleOverlayDraw(); } -void UsermodManager::appendConfigData(Print& dest) { for (unsigned i = 0; i < numMods; i++) ums[i]->appendConfigData(dest); } +void UsermodManager::setup() { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->setup(); } +void UsermodManager::connected() { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->connected(); } +void UsermodManager::loop() { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->loop(); } +void UsermodManager::handleOverlayDraw() { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->handleOverlayDraw(); } +void UsermodManager::appendConfigData(Print& dest) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->appendConfigData(dest); } bool UsermodManager::handleButton(uint8_t b) { bool overrideIO = false; - for (unsigned i = 0; i < numMods; i++) { - if (ums[i]->handleButton(b)) overrideIO = true; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + if ((*mod)->handleButton(b)) overrideIO = true; } return overrideIO; } bool UsermodManager::getUMData(um_data_t **data, uint8_t mod_id) { - for (unsigned i = 0; i < numMods; i++) { - if (mod_id > 0 && ums[i]->getId() != mod_id) continue; // only get data form requested usermod if provided - if (ums[i]->getUMData(data)) return true; // if usermod does provide data return immediately (only one usermod can provide data at one time) + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + if (mod_id > 0 && (*mod)->getId() != mod_id) continue; // only get data form requested usermod if provided + if ((*mod)->getUMData(data)) return true; // if usermod does provide data return immediately (only one usermod can provide data at one time) } return false; } -void UsermodManager::addToJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonState(obj); } -void UsermodManager::addToJsonInfo(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonInfo(obj); } -void UsermodManager::readFromJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } -void UsermodManager::addToConfig(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } +void UsermodManager::addToJsonState(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->addToJsonState(obj); } +void UsermodManager::addToJsonInfo(JsonObject& obj) { + auto um_id_list = obj.createNestedArray("um"); + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + um_id_list.add((*mod)->getId()); + (*mod)->addToJsonInfo(obj); + } +} +void UsermodManager::readFromJsonState(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->readFromJsonState(obj); } +void UsermodManager::addToConfig(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->addToConfig(obj); } bool UsermodManager::readFromConfig(JsonObject& obj) { bool allComplete = true; - for (unsigned i = 0; i < numMods; i++) { - if (!ums[i]->readFromConfig(obj)) allComplete = false; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + if (!(*mod)->readFromConfig(obj)) allComplete = false; } return allComplete; } #ifndef WLED_DISABLE_MQTT -void UsermodManager::onMqttConnect(bool sessionPresent) { for (unsigned i = 0; i < numMods; i++) ums[i]->onMqttConnect(sessionPresent); } +void UsermodManager::onMqttConnect(bool sessionPresent) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->onMqttConnect(sessionPresent); } bool UsermodManager::onMqttMessage(char* topic, char* payload) { - for (unsigned i = 0; i < numMods; i++) if (ums[i]->onMqttMessage(topic, payload)) return true; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) if ((*mod)->onMqttMessage(topic, payload)) return true; return false; } #endif #ifndef WLED_DISABLE_ESPNOW bool UsermodManager::onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { - for (unsigned i = 0; i < numMods; i++) if (ums[i]->onEspNowMessage(sender, payload, len)) return true; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) if ((*mod)->onEspNowMessage(sender, payload, len)) return true; return false; } #endif -void UsermodManager::onUpdateBegin(bool init) { for (unsigned i = 0; i < numMods; i++) ums[i]->onUpdateBegin(init); } // notify usermods that update is to begin -void UsermodManager::onStateChange(uint8_t mode) { for (unsigned i = 0; i < numMods; i++) ums[i]->onStateChange(mode); } // notify usermods that WLED state changed +bool UsermodManager::onUdpPacket(uint8_t* payload, size_t len) { + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) if ((*mod)->onUdpPacket(payload, len)) return true; + return false; +} +void UsermodManager::onUpdateBegin(bool init) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->onUpdateBegin(init); } // notify usermods that update is to begin +void UsermodManager::onStateChange(uint8_t mode) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->onStateChange(mode); } // notify usermods that WLED state changed /* * Enables usermods to lookup another Usermod. */ Usermod* UsermodManager::lookup(uint16_t mod_id) { - for (unsigned i = 0; i < numMods; i++) { - if (ums[i]->getId() == mod_id) { - return ums[i]; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + if ((*mod)->getId() == mod_id) { + return *mod; } } return nullptr; } -bool UsermodManager::add(Usermod* um) -{ - if (numMods >= WLED_MAX_USERMODS || um == nullptr) return false; - ums[numMods++] = um; - return true; -} - -Usermod* UsermodManager::ums[WLED_MAX_USERMODS] = {nullptr}; -byte UsermodManager::numMods = 0; +size_t UsermodManager::getModCount() { return getCount(); }; /* Usermod v2 interface shim for oappend */ Print* Usermod::oappend_shim = nullptr; diff --git a/wled00/usermod.cpp b/wled00/usermod.cpp index da9738234e..40fda83b07 100644 --- a/wled00/usermod.cpp +++ b/wled00/usermod.cpp @@ -1,7 +1,7 @@ #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 + * See: https://github.com/wled-dev/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) * diff --git a/wled00/usermod_v2_empty.h b/wled00/usermod_v2_empty.h deleted file mode 100644 index 6537b56bc5..0000000000 --- a/wled00/usermod_v2_empty.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "wled.h" - -//This is an empty v2 usermod template. Please see the file usermod_v2_example.h in the EXAMPLE_v2 usermod folder for documentation on the functions you can use! - -class UsermodRenameMe : public Usermod { - private: - - public: - void setup() { - - } - - void loop() { - - } -}; \ No newline at end of file diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp deleted file mode 100644 index 36bd122a51..0000000000 --- a/wled00/usermods_list.cpp +++ /dev/null @@ -1,473 +0,0 @@ -#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 "../usermods/EXAMPLE_v2/usermod_v2_example.h" - -#ifdef USERMOD_BATTERY - #include "../usermods/Battery/usermod_v2_Battery.h" -#endif - -#ifdef USERMOD_DALLASTEMPERATURE - #include "../usermods/Temperature/usermod_temperature.h" -#endif - -#ifdef USERMOD_SHT -#include "../usermods/sht/usermod_sht.h" -#endif - -#ifdef USERMOD_SN_PHOTORESISTOR - #include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h" -#endif - -#ifdef USERMOD_PWM_FAN - // requires DALLASTEMPERATURE or SHT included before it - #include "../usermods/PWM_fan/usermod_PWM_fan.h" -#endif - -#ifdef USERMOD_BUZZER - #include "../usermods/buzzer/usermod_v2_buzzer.h" -#endif - -#ifdef USERMOD_SENSORSTOMQTT - #include "../usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h" -#endif - -#ifdef USERMOD_PIRSWITCH - #include "../usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h" -#endif - -#ifdef USERMOD_BH1750 - #include "../usermods/BH1750_v2/usermod_BH1750.h" -#endif - -// BME280 v2 usermod. Define "USERMOD_BME280" in my_config.h -#ifdef USERMOD_BME280 - #include "../usermods/BME280_v2/usermod_bme280.h" -#endif - -#ifdef USERMOD_BME68X - #include "../usermods/BME68X_v2/usermod_bme68x.h" -#endif - - -#ifdef USERMOD_FOUR_LINE_DISPLAY - #include "../usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h" -#endif - -#ifdef USERMOD_ROTARY_ENCODER_UI - #include "../usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h" -#endif - -#ifdef USERMOD_AUTO_SAVE - #include "../usermods/usermod_v2_auto_save/usermod_v2_auto_save.h" -#endif - -#ifdef USERMOD_DHT - #include "../usermods/DHT/usermod_dht.h" -#endif - -#ifdef USERMOD_VL53L0X_GESTURES - #include "../usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h" -#endif - -#ifdef USERMOD_ANIMATED_STAIRCASE - #include "../usermods/Animated_Staircase/Animated_Staircase.h" -#endif - -#ifdef USERMOD_MULTI_RELAY - #include "../usermods/multi_relay/usermod_multi_relay.h" -#endif - -#ifdef USERMOD_RTC - #include "../usermods/RTC/usermod_rtc.h" -#endif - -#ifdef USERMOD_ELEKSTUBE_IPS - #include "../usermods/EleksTube_IPS/usermod_elekstube_ips.h" -#endif - -#ifdef USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR - #include "../usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h" -#endif - -#ifdef RGB_ROTARY_ENCODER - #include "../usermods/rgb-rotary-encoder/rgb-rotary-encoder.h" -#endif - -#ifdef USERMOD_ST7789_DISPLAY - #include "../usermods/ST7789_display/ST7789_Display.h" -#endif - -#ifdef USERMOD_PIXELS_DICE_TRAY - #include "../usermods/pixels_dice_tray/pixels_dice_tray.h" -#endif - -#ifdef USERMOD_SEVEN_SEGMENT - #include "../usermods/seven_segment_display/usermod_v2_seven_segment_display.h" -#endif - -#ifdef USERMOD_SSDR - #include "../usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h" -#endif - -#ifdef USERMOD_CRONIXIE - #include "../usermods/Cronixie/usermod_cronixie.h" -#endif - -#ifdef QUINLED_AN_PENTA - #include "../usermods/quinled-an-penta/quinled-an-penta.h" -#endif - -#ifdef USERMOD_WIZLIGHTS - #include "../usermods/wizlights/wizlights.h" -#endif - -#ifdef USERMOD_WIREGUARD - #include "../usermods/wireguard/wireguard.h" -#endif - -#ifdef USERMOD_WORDCLOCK - #include "../usermods/usermod_v2_word_clock/usermod_v2_word_clock.h" -#endif - -#ifdef USERMOD_MY9291 - #include "../usermods/MY9291/usermode_MY9291.h" -#endif - -#ifdef USERMOD_SI7021_MQTT_HA - #include "../usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h" -#endif - -#ifdef USERMOD_SMARTNEST - #include "../usermods/smartnest/usermod_smartnest.h" -#endif - -#ifdef USERMOD_AUDIOREACTIVE - #include "../usermods/audioreactive/audio_reactive.h" -#endif - -#ifdef USERMOD_ANALOG_CLOCK - #include "../usermods/Analog_Clock/Analog_Clock.h" -#endif - -#ifdef USERMOD_PING_PONG_CLOCK - #include "../usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h" -#endif - -#ifdef USERMOD_ADS1115 - #include "../usermods/ADS1115_v2/usermod_ads1115.h" -#endif - -#ifdef USERMOD_KLIPPER_PERCENTAGE - #include "../usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h" -#endif - -#ifdef USERMOD_BOBLIGHT - #include "../usermods/boblight/boblight.h" -#endif - -#ifdef USERMOD_ANIMARTRIX - #include "../usermods/usermod_v2_animartrix/usermod_v2_animartrix.h" -#endif - -#ifdef USERMOD_INTERNAL_TEMPERATURE - #include "../usermods/Internal_Temperature_v2/usermod_internal_temperature.h" -#endif - -#if defined(WLED_USE_SD_MMC) || defined(WLED_USE_SD_SPI) -// This include of SD.h and SD_MMC.h must happen here, else they won't be -// resolved correctly (when included in mod's header only) - #ifdef WLED_USE_SD_MMC - #include "SD_MMC.h" - #elif defined(WLED_USE_SD_SPI) - #include "SD.h" - #include "SPI.h" - #endif - #include "../usermods/sd_card/usermod_sd_card.h" -#endif - -#ifdef USERMOD_PWM_OUTPUTS - #include "../usermods/pwm_outputs/usermod_pwm_outputs.h" -#endif - -#ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL - #include "../usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h" -#endif - -#ifdef USERMOD_MPU6050_IMU - #include "../usermods/mpu6050_imu/usermod_mpu6050_imu.h" -#endif - -#ifdef USERMOD_MPU6050_IMU - #include "../usermods/mpu6050_imu/usermod_gyro_surge.h" -#endif - -#ifdef USERMOD_LDR_DUSK_DAWN - #include "../usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h" -#endif - -#ifdef USERMOD_POV_DISPLAY - #include "../usermods/pov_display/usermod_pov_display.h" -#endif - -#ifdef USERMOD_STAIRCASE_WIPE - #include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h" -#endif - -#ifdef USERMOD_MAX17048 - #include "../usermods/MAX17048_v2/usermod_max17048.h" -#endif - -#ifdef USERMOD_TETRISAI - #include "../usermods/TetrisAI_v2/usermod_v2_tetrisai.h" -#endif - -#ifdef USERMOD_AHT10 - #include "../usermods/AHT10_v2/usermod_aht10.h" -#endif - -#ifdef USERMOD_INA226 - #include "../usermods/INA226_v2/usermod_ina226.h" -#endif - -#ifdef USERMOD_LD2410 -#include "../usermods/LD2410_v2/usermod_ld2410.h" -#endif - -void registerUsermods() -{ -/* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //UsermodManager::add(new MyExampleUsermod()); - - #ifdef USERMOD_BATTERY - UsermodManager::add(new UsermodBattery()); - #endif - - #ifdef USERMOD_DALLASTEMPERATURE - UsermodManager::add(new UsermodTemperature()); - #endif - - #ifdef USERMOD_SN_PHOTORESISTOR - UsermodManager::add(new Usermod_SN_Photoresistor()); - #endif - - #ifdef USERMOD_PWM_FAN - UsermodManager::add(new PWMFanUsermod()); - #endif - - #ifdef USERMOD_BUZZER - UsermodManager::add(new BuzzerUsermod()); - #endif - - #ifdef USERMOD_BH1750 - UsermodManager::add(new Usermod_BH1750()); - #endif - - #ifdef USERMOD_BME280 - UsermodManager::add(new UsermodBME280()); - #endif - - #ifdef USERMOD_BME68X - UsermodManager::add(new UsermodBME68X()); - #endif - - #ifdef USERMOD_SENSORSTOMQTT - UsermodManager::add(new UserMod_SensorsToMQTT()); - #endif - - #ifdef USERMOD_PIRSWITCH - UsermodManager::add(new PIRsensorSwitch()); - #endif - - #ifdef USERMOD_FOUR_LINE_DISPLAY - UsermodManager::add(new FourLineDisplayUsermod()); - #endif - - #ifdef USERMOD_ROTARY_ENCODER_UI - UsermodManager::add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY - #endif - - #ifdef USERMOD_AUTO_SAVE - UsermodManager::add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY - #endif - - #ifdef USERMOD_DHT - UsermodManager::add(new UsermodDHT()); - #endif - - #ifdef USERMOD_VL53L0X_GESTURES - UsermodManager::add(new UsermodVL53L0XGestures()); - #endif - - #ifdef USERMOD_ANIMATED_STAIRCASE - UsermodManager::add(new Animated_Staircase()); - #endif - - #ifdef USERMOD_MULTI_RELAY - UsermodManager::add(new MultiRelay()); - #endif - - #ifdef USERMOD_RTC - UsermodManager::add(new RTCUsermod()); - #endif - - #ifdef USERMOD_ELEKSTUBE_IPS - UsermodManager::add(new ElekstubeIPSUsermod()); - #endif - - #ifdef USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR - UsermodManager::add(new RotaryEncoderBrightnessColor()); - #endif - - #ifdef RGB_ROTARY_ENCODER - UsermodManager::add(new RgbRotaryEncoderUsermod()); - #endif - - #ifdef USERMOD_ST7789_DISPLAY - UsermodManager::add(new St7789DisplayUsermod()); - #endif - - #ifdef USERMOD_PIXELS_DICE_TRAY - UsermodManager::add(new PixelsDiceTrayUsermod()); - #endif - - #ifdef USERMOD_SEVEN_SEGMENT - UsermodManager::add(new SevenSegmentDisplay()); - #endif - - #ifdef USERMOD_SSDR - UsermodManager::add(new UsermodSSDR()); - #endif - - #ifdef USERMOD_CRONIXIE - UsermodManager::add(new UsermodCronixie()); - #endif - - #ifdef QUINLED_AN_PENTA - UsermodManager::add(new QuinLEDAnPentaUsermod()); - #endif - - #ifdef USERMOD_WIZLIGHTS - UsermodManager::add(new WizLightsUsermod()); - #endif - - #ifdef USERMOD_WIREGUARD - UsermodManager::add(new WireguardUsermod()); - #endif - - #ifdef USERMOD_WORDCLOCK - UsermodManager::add(new WordClockUsermod()); - #endif - - #ifdef USERMOD_MY9291 - UsermodManager::add(new MY9291Usermod()); - #endif - - #ifdef USERMOD_SI7021_MQTT_HA - UsermodManager::add(new Si7021_MQTT_HA()); - #endif - - #ifdef USERMOD_SMARTNEST - UsermodManager::add(new Smartnest()); - #endif - - #ifdef USERMOD_AUDIOREACTIVE - UsermodManager::add(new AudioReactive()); - #endif - - #ifdef USERMOD_ANALOG_CLOCK - UsermodManager::add(new AnalogClockUsermod()); - #endif - - #ifdef USERMOD_PING_PONG_CLOCK - UsermodManager::add(new PingPongClockUsermod()); - #endif - - #ifdef USERMOD_ADS1115 - UsermodManager::add(new ADS1115Usermod()); - #endif - - #ifdef USERMOD_KLIPPER_PERCENTAGE - UsermodManager::add(new klipper_percentage()); - #endif - - #ifdef USERMOD_BOBLIGHT - UsermodManager::add(new BobLightUsermod()); - #endif - - #ifdef SD_ADAPTER - UsermodManager::add(new UsermodSdCard()); - #endif - - #ifdef USERMOD_PWM_OUTPUTS - UsermodManager::add(new PwmOutputsUsermod()); - #endif - - #ifdef USERMOD_SHT - UsermodManager::add(new ShtUsermod()); - #endif - - #ifdef USERMOD_ANIMARTRIX - UsermodManager::add(new AnimartrixUsermod("Animartrix", false)); - #endif - - #ifdef USERMOD_INTERNAL_TEMPERATURE - UsermodManager::add(new InternalTemperatureUsermod()); - #endif - - #ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL - UsermodManager::add(new HttpPullLightControl()); - #endif - - #ifdef USERMOD_MPU6050_IMU - static MPU6050Driver mpu6050; UsermodManager::add(&mpu6050); - #endif - - #ifdef USERMOD_GYRO_SURGE - static GyroSurge gyro_surge; UsermodManager::add(&gyro_surge); - #endif - - #ifdef USERMOD_LDR_DUSK_DAWN - UsermodManager::add(new LDR_Dusk_Dawn_v2()); - #endif - - #ifdef USERMOD_STAIRCASE_WIPE - UsermodManager::add(new StairwayWipeUsermod()); - #endif - - #ifdef USERMOD_MAX17048 - UsermodManager::add(new Usermod_MAX17048()); - #endif - - #ifdef USERMOD_TETRISAI - UsermodManager::add(new TetrisAIUsermod()); - #endif - - #ifdef USERMOD_AHT10 - UsermodManager::add(new UsermodAHT10()); - #endif - - #ifdef USERMOD_INA226 - UsermodManager::add(new UsermodINA226()); - #endif - - #ifdef USERMOD_LD2410 - UsermodManager::add(new LD2410Usermod()); - #endif - - #ifdef USERMOD_POV_DISPLAY - UsermodManager::add(new PovDisplayUsermod()); - #endif -} diff --git a/wled00/util.cpp b/wled00/util.cpp index c43fdeabf0..c41cf22884 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -1,20 +1,33 @@ #include "wled.h" #include "fcn_declare.h" #include "const.h" +#ifdef ESP8266 +#include "user_interface.h" // for bootloop detection +#include // for SHA1 on ESP8266 +#else +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + #include "esp32/rtc.h" // for bootloop detection +#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0) + #include "soc/rtc.h" +#endif +#include "mbedtls/sha1.h" // for SHA1 on ESP32 +#include "esp_efuse.h" +#endif //helper to get int value at a position in string -int getNumVal(const String* req, uint16_t pos) +int getNumVal(const String &req, uint16_t pos) { - return req->substring(pos+3).toInt(); + return req.substring(pos+3).toInt(); } //helper to get int value with in/decrementing support via ~ syntax -void parseNumber(const char* str, byte* val, byte minv, byte maxv) +void parseNumber(const char* str, byte &val, byte minv, byte maxv) { if (str == nullptr || str[0] == '\0') return; - if (str[0] == 'r') {*val = random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0 + if (str[0] == 'r') {val = hw_random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0 bool wrap = false; if (str[0] == 'w' && strlen(str) > 1) {str++; wrap = true;} if (str[0] == '~') { @@ -22,19 +35,19 @@ void parseNumber(const char* str, byte* val, byte minv, byte maxv) if (out == 0) { if (str[1] == '0') return; if (str[1] == '-') { - *val = (int)(*val -1) < (int)minv ? maxv : min((int)maxv,(*val -1)); //-1, wrap around + val = (int)(val -1) < (int)minv ? maxv : min((int)maxv,(val -1)); //-1, wrap around } else { - *val = (int)(*val +1) > (int)maxv ? minv : max((int)minv,(*val +1)); //+1, wrap around + val = (int)(val +1) > (int)maxv ? minv : max((int)minv,(val +1)); //+1, wrap around } } else { - if (wrap && *val == maxv && out > 0) out = minv; - else if (wrap && *val == minv && out < 0) out = maxv; + if (wrap && val == maxv && out > 0) out = minv; + else if (wrap && val == minv && out < 0) out = maxv; else { - out += *val; + out += val; if (out > maxv) out = maxv; if (out < minv) out = minv; } - *val = out; + val = out; } return; } else if (minv == maxv && minv == 0) { // limits "unset" i.e. both 0 @@ -49,19 +62,23 @@ void parseNumber(const char* str, byte* val, byte minv, byte maxv) } } } - *val = atoi(str); + val = atoi(str); } - -bool getVal(JsonVariant elem, byte* val, byte vmin, byte vmax) { +//getVal supports inc/decrementing and random ("X~Y(r|~[w][-][Z])" form) +bool getVal(JsonVariant elem, byte &val, byte vmin, byte vmax) { if (elem.is()) { if (elem < 0) return false; //ignore e.g. {"ps":-1} - *val = elem; + val = elem; return true; } else if (elem.is()) { const char* str = elem; - size_t len = strnlen(str, 12); - if (len == 0 || len > 10) return false; + size_t len = strnlen(str, 14); + if (len == 0 || len > 12) return false; + // fix for #3605 & #4346 + // ignore vmin and vmax and use as specified in API + if (len > 3 && (strchr(str,'r') || strchr(str,'~') != strrchr(str,'~'))) vmax = vmin = 0; // we have "X~Y(r|~[w][-][Z])" form + // end fix parseNumber(str, val, vmin, vmax); return true; } @@ -69,7 +86,7 @@ bool getVal(JsonVariant elem, byte* val, byte vmin, byte vmax) { } -bool getBoolVal(JsonVariant elem, bool dflt) { +bool getBoolVal(const JsonVariant &elem, bool dflt) { if (elem.is() && elem.as()[0] == 't') { return !dflt; } else { @@ -78,7 +95,7 @@ bool getBoolVal(JsonVariant elem, bool dflt) { } -bool updateVal(const char* req, const char* key, byte* val, byte minv, byte maxv) +bool updateVal(const char* req, const char* key, byte &val, byte minv, byte maxv) { const char *v = strstr(req, key); if (v) v += strlen(key); @@ -146,8 +163,8 @@ bool isAsterisksOnly(const char* str, byte maxLen) } -//threading/network callback details: https://github.com/Aircoookie/WLED/pull/2336#discussion_r762276994 -bool requestJSONBufferLock(uint8_t module) +//threading/network callback details: https://github.com/wled-dev/WLED/pull/2336#discussion_r762276994 +bool requestJSONBufferLock(uint8_t moduleID) { if (pDoc == nullptr) { DEBUG_PRINTLN(F("ERROR: JSON buffer not allocated!")); @@ -171,14 +188,14 @@ bool requestJSONBufferLock(uint8_t module) #endif // If the lock is still held - by us, or by another task if (jsonBufferLock) { - DEBUG_PRINTF_P(PSTR("ERROR: Locking JSON buffer (%d) failed! (still locked by %d)\n"), module, jsonBufferLock); + DEBUG_PRINTF_P(PSTR("ERROR: Locking JSON buffer (%d) failed! (still locked by %d)\n"), moduleID, jsonBufferLock); #ifdef ARDUINO_ARCH_ESP32 xSemaphoreGiveRecursive(jsonBufferLockMutex); #endif return false; } - jsonBufferLock = module ? module : 255; + jsonBufferLock = moduleID ? moduleID : 255; DEBUG_PRINTF_P(PSTR("JSON buffer locked. (%d)\n"), jsonBufferLock); pDoc->clear(); return true; @@ -196,7 +213,7 @@ void releaseJSONBufferLock() // extracts effect mode (or palette) name from names serialized string -// caller must provide large enough buffer for name (including SR extensions)! +// caller must provide large enough buffer for name (including SR extensions)! maxLen is (buffersize - 1) uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen) { if (src == JSON_mode_names || src == nullptr) { @@ -216,9 +233,9 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe } else return 0; } - if (src == JSON_palette_names && mode > (GRADIENT_PALETTE_COUNT + 13)) { + if (src == JSON_palette_names && mode > 255-customPalettes.size()) { snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode); - dest[maxLen-1] = '\0'; + dest[maxLen] = '\0'; return strlen(dest); } @@ -261,16 +278,16 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL if (mode < strip.getModeCount()) { String lineBuffer = FPSTR(strip.getModeData(mode)); if (lineBuffer.length() > 0) { - unsigned start = lineBuffer.indexOf('@'); - unsigned stop = lineBuffer.indexOf(';', start); + int start = lineBuffer.indexOf('@'); // String::indexOf() returns an int, not an unsigned; -1 means "not found" + int stop = lineBuffer.indexOf(';', start); if (start>0 && stop>0) { String names = lineBuffer.substring(start, stop); // include @ - unsigned nameBegin = 1, nameEnd, nameDefault; + int nameBegin = 1, nameEnd, nameDefault; if (slider < 10) { for (size_t i=0; i<=slider; i++) { const char *tmpstr; dest[0] = '\0'; //clear dest buffer - if (nameBegin == 0) break; // there are no more names + if (nameBegin <= 0) break; // there are no more names nameEnd = names.indexOf(',', nameBegin); if (i == slider) { nameDefault = names.indexOf('=', nameBegin); // find default value @@ -319,7 +336,7 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL case 0: strncpy_P(dest, PSTR("FX Speed"), maxLen); break; case 1: strncpy_P(dest, PSTR("FX Intensity"), maxLen); break; } - dest[maxLen] = '\0'; // strncpy does not necessarily null terminate string + dest[maxLen-1] = '\0'; // strncpy does not necessarily null terminate string } } return strlen(dest); @@ -353,9 +370,8 @@ int16_t extractModeDefaults(uint8_t mode, const char *segVar) void checkSettingsPIN(const char* pin) { if (!pin) return; if (!correctPIN && millis() - lastEditTime < PIN_RETRY_COOLDOWN) return; // guard against PIN brute force - bool correctBefore = correctPIN; + //bool correctBefore = correctPIN; // unused correctPIN = (strlen(settingsPIN) == 0 || strncmp(settingsPIN, pin, 4) == 0); - if (correctBefore != correctPIN) createEditHandler(correctPIN); lastEditTime = millis(); } @@ -466,13 +482,13 @@ um_data_t* simulateSound(uint8_t simulationId) for (int i = 0; i<16; i++) fftResult[i] = beatsin8_t(120 / (i+1), 0, 255); // fftResult[i] = (beatsin8_t(120, 0, 255) + (256/16 * i)) % 256; - volumeSmth = fftResult[8]; + volumeSmth = fftResult[8]; break; case UMS_WeWillRockYou: if (ms%2000 < 200) { - volumeSmth = random8(255); + volumeSmth = hw_random8(); for (int i = 0; i<5; i++) - fftResult[i] = random8(255); + fftResult[i] = hw_random8(); } else if (ms%2000 < 400) { volumeSmth = 0; @@ -480,9 +496,9 @@ um_data_t* simulateSound(uint8_t simulationId) fftResult[i] = 0; } else if (ms%2000 < 600) { - volumeSmth = random8(255); + volumeSmth = hw_random8(); for (int i = 5; i<11; i++) - fftResult[i] = random8(255); + fftResult[i] = hw_random8(); } else if (ms%2000 < 800) { volumeSmth = 0; @@ -490,9 +506,9 @@ um_data_t* simulateSound(uint8_t simulationId) fftResult[i] = 0; } else if (ms%2000 < 1000) { - volumeSmth = random8(255); + volumeSmth = hw_random8(); for (int i = 11; i<16; i++) - fftResult[i] = random8(255); + fftResult[i] = hw_random8(); } else { volumeSmth = 0; @@ -502,17 +518,17 @@ um_data_t* simulateSound(uint8_t simulationId) break; case UMS_10_13: for (int i = 0; i<16; i++) - fftResult[i] = inoise8(beatsin8_t(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3); - volumeSmth = fftResult[8]; + fftResult[i] = perlin8(beatsin8_t(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3); + volumeSmth = fftResult[8]; break; case UMS_14_3: for (int i = 0; i<16; i++) - fftResult[i] = inoise8(beatsin8_t(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3); + fftResult[i] = perlin8(beatsin8_t(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3); volumeSmth = fftResult[8]; break; } - samplePeak = random8() > 250; + samplePeak = hw_random8() > 250; FFT_MajorPeak = 21 + (volumeSmth*volumeSmth) / 8.0f; // walk thru full range of 21hz...8200hz maxVol = 31; // this gets feedback fro UI binNum = 8; // this gets feedback fro UI @@ -526,6 +542,8 @@ um_data_t* simulateSound(uint8_t simulationId) static const char s_ledmap_tmpl[] PROGMEM = "ledmap%d.json"; // enumerate all ledmapX.json files on FS and extract ledmap names if existing void enumerateLedmaps() { + StaticJsonDocument<64> filter; + filter["n"] = true; ledMaps = 1; for (size_t i=1; ias(); if (!root["n"].isNull()) { @@ -552,7 +570,7 @@ void enumerateLedmaps() { const char *name = root["n"].as(); if (name != nullptr) len = strlen(name); if (len > 0 && len < 33) { - ledmapNames[i-1] = new char[len+1]; + ledmapNames[i-1] = static_cast(malloc(len+1)); if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], name, 33); } } @@ -560,7 +578,7 @@ void enumerateLedmaps() { char tmp[33]; snprintf_P(tmp, 32, s_ledmap_tmpl, i); len = strlen(tmp); - ledmapNames[i-1] = new char[len+1]; + ledmapNames[i-1] = static_cast(malloc(len+1)); if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], tmp, 33); } } @@ -578,7 +596,7 @@ void enumerateLedmaps() { uint8_t get_random_wheel_index(uint8_t pos) { uint8_t r = 0, x = 0, y = 0, d = 0; while (d < 42) { - r = random8(); + r = hw_random8(); x = abs(pos - r); y = 255 - x; d = MIN(x, y); @@ -590,3 +608,626 @@ uint8_t get_random_wheel_index(uint8_t pos) { 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; } + +uint32_t hashInt(uint32_t s) { + // borrowed from https://stackoverflow.com/questions/664014/what-integer-hash-function-are-good-that-accepts-an-integer-hash-key + s = ((s >> 16) ^ s) * 0x45d9f3b; + s = ((s >> 16) ^ s) * 0x45d9f3b; + return (s >> 16) ^ s; +} + +// 32 bit random number generator, inlining uses more code, use hw_random16() if speed is critical (see fcn_declare.h) +uint32_t hw_random(uint32_t upperlimit) { + uint32_t rnd = hw_random(); + uint64_t scaled = uint64_t(rnd) * uint64_t(upperlimit); + return scaled >> 32; +} + +int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { + if(lowerlimit >= upperlimit) { + return lowerlimit; + } + uint32_t diff = upperlimit - lowerlimit; + return hw_random(diff) + lowerlimit; +} + +// PSRAM compile time checks to provide info for misconfigured env +#if defined(BOARD_HAS_PSRAM) + #if defined(IDF_TARGET_ESP32C3) || defined(ESP8266) + #error "ESP32-C3 and ESP8266 with PSRAM is not supported, please remove BOARD_HAS_PSRAM definition" + #else + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) // PSRAM fix only needed for classic esp32 + // BOARD_HAS_PSRAM also means that compiler flag "-mfix-esp32-psram-cache-issue" has to be used for old "rev.1" esp32 + #warning "BOARD_HAS_PSRAM defined, make sure to use -mfix-esp32-psram-cache-issue to prevent issues on rev.1 ESP32 boards \ + see https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html#esp32-rev-v1-0" + #endif + #endif +#else + #if !defined(IDF_TARGET_ESP32C3) && !defined(ESP8266) + #pragma message("BOARD_HAS_PSRAM not defined, not using PSRAM.") + #endif +#endif + +// memory allocation functions with minimum free heap size check +#ifdef ESP8266 +static void *validateFreeHeap(void *buffer) { + // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not + // note: ESP826 needs very little contiguous heap for webserver, checking total free heap works better + if (getFreeHeapSize() < MIN_HEAP_SIZE) { + free(buffer); + return nullptr; + } + return buffer; +} + +void *d_malloc(size_t size) { + // note: using "if (getContiguousFreeHeap() > MIN_HEAP_SIZE + size)" did perform worse in tests with regards to keeping heap healthy and UI working + void *buffer = malloc(size); + return validateFreeHeap(buffer); +} + +void *d_calloc(size_t count, size_t size) { + void *buffer = calloc(count, size); + return validateFreeHeap(buffer); +} + +// realloc with malloc fallback, note: on ESPS8266 there is no safe way to ensure MIN_HEAP_SIZE during realloc()s, free buffer and allocate new one +void *d_realloc_malloc(void *ptr, size_t size) { + //void *buffer = realloc(ptr, size); + //buffer = validateFreeHeap(buffer); + //if (buffer) return buffer; // realloc successful + //d_free(ptr); // free old buffer if realloc failed (or min heap was exceeded) + //return d_malloc(size); // fallback to malloc + free(ptr); + return d_malloc(size); +} +#else +static void *validateFreeHeap(void *buffer) { + // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not + // TODO: between allocate and free, heap can run low (async web access), only IDF V5 allows for a pre-allocation-check of all free blocks + if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH && getContiguousFreeHeap() < MIN_HEAP_SIZE) { + free(buffer); + return nullptr; + } + return buffer; +} + +#ifdef BOARD_HAS_PSRAM +#define RTC_RAM_THRESHOLD 1024 // use RTC RAM for allocations smaller than this size +#else +#define RTC_RAM_THRESHOLD 65535 // without PSRAM, allow any size into RTC RAM (useful especially on S2 without PSRAM) +#endif + +void *d_malloc(size_t size) { + void *buffer = nullptr; + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + // the newer ESP32 variants have byte-accessible fast RTC memory that can be used as heap, access speed is on-par with DRAM + // the system does prefer normal DRAM until full, since free RTC memory is ~7.5k only, its below the minimum heap threshold and needs to be allocated explicitly + // use RTC RAM for small allocations or if DRAM is running low to improve fragmentation + if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size) + buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + if (buffer == nullptr) // no RTC RAM allocation: use DRAM + #endif + buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory + buffer = validateFreeHeap(buffer); // make sure there is enough free heap left + #ifdef BOARD_HAS_PSRAM + if (!buffer) + return heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); // DRAM failed, use PSRAM if available + #endif + return buffer; +} + +void *d_calloc(size_t count, size_t size) { + void *buffer = d_malloc(count * size); + if (buffer) memset(buffer, 0, count * size); // clear allocated buffer + return buffer; +} + +// realloc with malloc fallback, original buffer is freed if realloc fails but not copied! +void *d_realloc_malloc(void *ptr, size_t size) { + void *buffer = heap_caps_realloc(ptr, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + buffer = validateFreeHeap(buffer); + if (buffer) return buffer; // realloc successful + d_free(ptr); // free old buffer if realloc failed (or min heap was exceeded) + return d_malloc(size); // fallback to malloc +} + +#ifdef BOARD_HAS_PSRAM +// p_xalloc: prefer PSRAM, use DRAM as fallback +void *p_malloc(size_t size) { + void *buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + return validateFreeHeap(buffer); +} + +void *p_calloc(size_t count, size_t size) { + void *buffer = p_malloc(count * size); + if (buffer) memset(buffer, 0, count * size); // clear allocated buffer + return buffer; +} + +// realloc with malloc fallback, original buffer is freed if realloc fails but not copied! +void *p_realloc_malloc(void *ptr, size_t size) { + void *buffer = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (buffer) return buffer; // realloc successful + p_free(ptr); // free old buffer if realloc failed + return p_malloc(size); // fallback to malloc +} +#endif +#endif + +// allocation function for buffers like pixel-buffers and segment data +// optimises the use of memory types to balance speed and heap availability, always favours DRAM if possible +// if multiple conflicting types are defined, the lowest bits of "type" take priority (see fcn_declare.h for types) +void *allocate_buffer(size_t size, uint32_t type) { + void *buffer = nullptr; + #ifdef CONFIG_IDF_TARGET_ESP32 + // only classic ESP32 has "32bit accessible only" aka IRAM type. Using it frees up normal DRAM for other purposes + // this memory region is used for IRAM_ATTR functions, whatever is left is unused and can be used for pixel buffers + // prefer this type over PSRAM as it is slightly faster, except for _pixels where it is on-par as PSRAM-caching does a good job for mostly sequential access + if (type & BFRALLOC_NOBYTEACCESS) { + // prefer 32bit region, then PSRAM, fallback to any heap. Note: if adding "INTERNAL"-flag this wont work + buffer = heap_caps_malloc_prefer(size, 3, MALLOC_CAP_32BIT, MALLOC_CAP_SPIRAM, MALLOC_CAP_8BIT); + buffer = validateFreeHeap(buffer); + } + else + #endif + #if !defined(BOARD_HAS_PSRAM) + buffer = d_malloc(size); + #else + if (type & BFRALLOC_PREFER_DRAM) { + if (getContiguousFreeHeap() < 3*(MIN_HEAP_SIZE/2) + size && size > PSRAM_THRESHOLD) + buffer = p_malloc(size); // prefer PSRAM for large allocations & when DRAM is low + else + buffer = d_malloc(size); // allocate in DRAM if enough free heap is available, PSRAM as fallback + } + else if (type & BFRALLOC_ENFORCE_DRAM) + buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // use DRAM only, otherwise return nullptr + else if (type & BFRALLOC_PREFER_PSRAM) { + // if DRAM is plenty, prefer it over PSRAM for speed, reserve enough DRAM for segment data: if MAX_SEGMENT_DATA is exceeded, always uses PSRAM + if (getContiguousFreeHeap() > 4*MIN_HEAP_SIZE + size + ((uint32_t)(MAX_SEGMENT_DATA - Segment::getUsedSegmentData()))) + buffer = d_malloc(size); + else + buffer = p_malloc(size); // prefer PSRAM + } + else if (type & BFRALLOC_ENFORCE_PSRAM) + buffer = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); // use PSRAM only, otherwise return nullptr + buffer = validateFreeHeap(buffer); + #endif + if (buffer && (type & BFRALLOC_CLEAR)) + memset(buffer, 0, size); // clear allocated buffer + /* + #if !defined(ESP8266) && defined(WLED_DEBUG) + if (buffer) { + DEBUG_PRINTF_P(PSTR("*Buffer allocated: size:%d, address:%p"), size, (uintptr_t)buffer); + if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH) + DEBUG_PRINTLN(F(" in DRAM")); + #ifndef CONFIG_IDF_TARGET_ESP32C3 + else if ((uintptr_t)buffer > SOC_EXTRAM_DATA_LOW && (uintptr_t)buffer < SOC_EXTRAM_DATA_HIGH) + DEBUG_PRINTLN(F(" in PSRAM")); + #endif + #ifdef CONFIG_IDF_TARGET_ESP32 + else if ((uintptr_t)buffer > SOC_IRAM_LOW && (uintptr_t)buffer < SOC_IRAM_HIGH) + DEBUG_PRINTLN(F(" in IRAM")); // only used on ESP32 (MALLOC_CAP_32BIT) + #else + else if ((uintptr_t)buffer > SOC_RTC_DRAM_LOW && (uintptr_t)buffer < SOC_RTC_DRAM_HIGH) + DEBUG_PRINTLN(F(" in RTCRAM")); // not available on ESP32 + #endif + else + DEBUG_PRINTLN(F(" in ???")); // unknown (check soc.h for other memory regions) + } else + DEBUG_PRINTF_P(PSTR("Buffer allocation failed: size:%d\n"), size); + #endif + */ + return buffer; +} + +// bootloop detection and handling +// checks if the ESP reboots multiple times due to a crash or watchdog timeout +// if a bootloop is detected: restore settings from backup, then reset settings, then switch boot image (and repeat) + +#define BOOTLOOP_INTERVAL_MILLIS 120000 // time limit between crashes: 120 seconds (2 minutes) +#define BOOTLOOP_THRESHOLD 5 // number of consecutive crashes to trigger bootloop detection +#define BOOTLOOP_ACTION_RESTORE 0 // default action: restore config from /bkp.cfg.json +#define BOOTLOOP_ACTION_RESET 1 // if restore does not work, reset config (rename /cfg.json to /rst.cfg.json) +#define BOOTLOOP_ACTION_OTA 2 // swap the boot partition +#define BOOTLOOP_ACTION_DUMP 3 // nothing seems to help, dump files to serial and reboot (until hardware reset) + +// Platform-agnostic abstraction +enum class ResetReason { + Power, + Software, + Crash, + Brownout +}; + +#ifdef ESP8266 +// Place variables in RTC memory via references, since RTC memory is not exposed via the linker in the Non-OS SDK +// Use an offset of 32 as there's some hints that the first 128 bytes of "user" memory are used by the OTA system +// Ref: https://github.com/esp8266/Arduino/blob/78d0d0aceacc1553f45ad8154592b0af22d1eede/cores/esp8266/Esp.cpp#L168 +static volatile uint32_t& bl_last_boottime = *(RTC_USER_MEM + 32); +static volatile uint32_t& bl_crashcounter = *(RTC_USER_MEM + 33); +static volatile uint32_t& bl_actiontracker = *(RTC_USER_MEM + 34); + +static inline ResetReason rebootReason() { + uint32_t resetReason = system_get_rst_info()->reason; + if (resetReason == REASON_EXCEPTION_RST + || resetReason == REASON_WDT_RST + || resetReason == REASON_SOFT_WDT_RST) + return ResetReason::Crash; + if (resetReason == REASON_SOFT_RESTART) + return ResetReason::Software; + return ResetReason::Power; +} + +static inline uint32_t getRtcMillis() { return system_get_rtc_time() / 160; }; // rtc ticks ~160000Hz + +#else +// variables in RTC_NOINIT memory persist between reboots (but not on hardware reset) +RTC_NOINIT_ATTR static uint32_t bl_last_boottime; +RTC_NOINIT_ATTR static uint32_t bl_crashcounter; +RTC_NOINIT_ATTR static uint32_t bl_actiontracker; + +static inline ResetReason rebootReason() { + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_BROWNOUT) return ResetReason::Brownout; + if (reason == ESP_RST_SW) return ResetReason::Software; + if (reason == ESP_RST_PANIC || reason == ESP_RST_WDT || reason == ESP_RST_INT_WDT || reason == ESP_RST_TASK_WDT) return ResetReason::Crash; + return ResetReason::Power; +} + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) +static inline uint32_t getRtcMillis() { return esp_rtc_get_time_us() / 1000; } +#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0) +static inline uint32_t getRtcMillis() { return rtc_time_slowclk_to_us(rtc_time_get(), rtc_clk_slow_freq_get_hz()) / 1000; } +#endif + +void bootloopCheckOTA() { bl_actiontracker = BOOTLOOP_ACTION_OTA; } // swap boot image if bootloop is detected instead of restoring config + +#endif + +// detect bootloop by checking the reset reason and the time since last boot +static bool detectBootLoop() { + uint32_t rtctime = getRtcMillis(); + bool result = false; + + switch(rebootReason()) { + case ResetReason::Power: + bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // init action tracker if not an intentional reboot (e.g. from OTA or bootloop handler) + // fall through + case ResetReason::Software: + // no crash detected, reset counter + bl_crashcounter = 0; + break; + + case ResetReason::Crash: + { + DEBUG_PRINTLN(F("crash detected!")); + uint32_t rebootinterval = rtctime - bl_last_boottime; + if (rebootinterval < BOOTLOOP_INTERVAL_MILLIS) { + bl_crashcounter++; + if (bl_crashcounter >= BOOTLOOP_THRESHOLD) { + DEBUG_PRINTLN(F("!BOOTLOOP DETECTED!")); + bl_crashcounter = 0; + if(bl_actiontracker > BOOTLOOP_ACTION_DUMP) bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // reset action tracker if out of bounds + result = true; + } + } else { + // Reset counter on long intervals to track only consecutive short-interval crashes + bl_crashcounter = 0; + // TODO: crash reporting goes here + } + break; + } + + case ResetReason::Brownout: + // crash due to brownout can't be detected unless using flash memory to store bootloop variables + DEBUG_PRINTLN(F("brownout detected")); + //restoreConfig(); // TODO: blindly restoring config if brownout detected is a bad idea, need a better way (if at all) + break; + } + + bl_last_boottime = rtctime; // store current runtime for next reboot + + return result; +} + +void handleBootLoop() { + DEBUG_PRINTF_P(PSTR("checking for bootloop: time %d, counter %d, action %d\n"), bl_last_boottime, bl_crashcounter, bl_actiontracker); + if (!detectBootLoop()) return; // no bootloop detected + + switch(bl_actiontracker) { + case BOOTLOOP_ACTION_RESTORE: + restoreConfig(); + ++bl_actiontracker; + break; + case BOOTLOOP_ACTION_RESET: + resetConfig(); + ++bl_actiontracker; + break; + case BOOTLOOP_ACTION_OTA: +#ifndef ESP8266 + if(Update.canRollBack()) { + DEBUG_PRINTLN(F("Swapping boot partition...")); + Update.rollBack(); // swap boot partition + } + ++bl_actiontracker; + break; +#else + // fall through +#endif + case BOOTLOOP_ACTION_DUMP: + dumpFilesToSerial(); + break; + } + + ESP.restart(); // restart cleanly and don't wait for another crash +} + +/* + * Fixed point integer based Perlin noise functions by @dedehai + * Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness + */ +#define PERLIN_SHIFT 1 + +// calculate gradient for corner from hash value +static inline __attribute__((always_inline)) int32_t hashToGradient(uint32_t h) { + // using more steps yields more "detailed" perlin noise but looks less like the original fastled version (adjust PERLIN_SHIFT to compensate, also changes range and needs proper adustment) + // return (h & 0xFF) - 128; // use PERLIN_SHIFT 7 + // return (h & 0x0F) - 8; // use PERLIN_SHIFT 3 + // return (h & 0x07) - 4; // use PERLIN_SHIFT 2 + return (h & 0x03) - 2; // use PERLIN_SHIFT 1 -> closest to original fastled version +} + +// Gradient functions for 1D, 2D and 3D Perlin noise note: forcing inline produces smaller code and makes it 3x faster! +static inline __attribute__((always_inline)) int32_t gradient1D(uint32_t x0, int32_t dx) { + uint32_t h = x0 * 0x27D4EB2D; + h ^= h >> 15; + h *= 0x92C3412B; + h ^= h >> 13; + h ^= h >> 7; + return (hashToGradient(h) * dx) >> PERLIN_SHIFT; +} + +static inline __attribute__((always_inline)) int32_t gradient2D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy) { + uint32_t h = (x0 * 0x27D4EB2D) ^ (y0 * 0xB5297A4D); + h ^= h >> 15; + h *= 0x92C3412B; + h ^= h >> 13; + return (hashToGradient(h) * dx + hashToGradient(h>>PERLIN_SHIFT) * dy) >> (1 + PERLIN_SHIFT); +} + +static inline __attribute__((always_inline)) int32_t gradient3D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy, uint32_t z0, int32_t dz) { + // fast and good entropy hash from corner coordinates + uint32_t h = (x0 * 0x27D4EB2D) ^ (y0 * 0xB5297A4D) ^ (z0 * 0x1B56C4E9); + h ^= h >> 15; + h *= 0x92C3412B; + h ^= h >> 13; + return ((hashToGradient(h) * dx + hashToGradient(h>>(1+PERLIN_SHIFT)) * dy + hashToGradient(h>>(1 + 2*PERLIN_SHIFT)) * dz) * 85) >> (8 + PERLIN_SHIFT); // scale to 16bit, x*85 >> 8 = x/3 +} + +// fast cubic smoothstep: t*(3 - 2t²), optimized for fixed point, scaled to avoid overflows +static uint32_t smoothstep(const uint32_t t) { + uint32_t t_squared = (t * t) >> 16; + uint32_t factor = (3 << 16) - ((t << 1)); + return (t_squared * factor) >> 18; // scale to avoid overflows and give best resolution +} + +// simple linear interpolation for fixed-point values, scaled for perlin noise use +static inline int32_t lerpPerlin(int32_t a, int32_t b, int32_t t) { + return a + (((b - a) * t) >> 14); // match scaling with smoothstep to yield 16.16bit values +} + +// 1D Perlin noise function that returns a value in range of -24691 to 24689 +int32_t perlin1D_raw(uint32_t x, bool is16bit) { + // integer and fractional part coordinates + int32_t x0 = x >> 16; + int32_t x1 = x0 + 1; + if(is16bit) x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF + + int32_t dx0 = x & 0xFFFF; + int32_t dx1 = dx0 - 0x10000; + // gradient values for the two corners + int32_t g0 = gradient1D(x0, dx0); + int32_t g1 = gradient1D(x1, dx1); + // interpolate and smooth function + int32_t tx = smoothstep(dx0); + int32_t noise = lerpPerlin(g0, g1, tx); + return noise; +} + +// 2D Perlin noise function that returns a value in range of -20633 to 20629 +int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit) { + int32_t x0 = x >> 16; + int32_t y0 = y >> 16; + int32_t x1 = x0 + 1; + int32_t y1 = y0 + 1; + + if(is16bit) { + x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF + y1 = y1 & 0xFF; + } + + int32_t dx0 = x & 0xFFFF; + int32_t dy0 = y & 0xFFFF; + int32_t dx1 = dx0 - 0x10000; + int32_t dy1 = dy0 - 0x10000; + + int32_t g00 = gradient2D(x0, dx0, y0, dy0); + int32_t g10 = gradient2D(x1, dx1, y0, dy0); + int32_t g01 = gradient2D(x0, dx0, y1, dy1); + int32_t g11 = gradient2D(x1, dx1, y1, dy1); + + uint32_t tx = smoothstep(dx0); + uint32_t ty = smoothstep(dy0); + + int32_t nx0 = lerpPerlin(g00, g10, tx); + int32_t nx1 = lerpPerlin(g01, g11, tx); + + int32_t noise = lerpPerlin(nx0, nx1, ty); + return noise; +} + +// 3D Perlin noise function that returns a value in range of -16788 to 16381 +int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit) { + int32_t x0 = x >> 16; + int32_t y0 = y >> 16; + int32_t z0 = z >> 16; + int32_t x1 = x0 + 1; + int32_t y1 = y0 + 1; + int32_t z1 = z0 + 1; + + if(is16bit) { + x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF + y1 = y1 & 0xFF; + z1 = z1 & 0xFF; + } + + int32_t dx0 = x & 0xFFFF; + int32_t dy0 = y & 0xFFFF; + int32_t dz0 = z & 0xFFFF; + int32_t dx1 = dx0 - 0x10000; + int32_t dy1 = dy0 - 0x10000; + int32_t dz1 = dz0 - 0x10000; + + int32_t g000 = gradient3D(x0, dx0, y0, dy0, z0, dz0); + int32_t g001 = gradient3D(x0, dx0, y0, dy0, z1, dz1); + int32_t g010 = gradient3D(x0, dx0, y1, dy1, z0, dz0); + int32_t g011 = gradient3D(x0, dx0, y1, dy1, z1, dz1); + int32_t g100 = gradient3D(x1, dx1, y0, dy0, z0, dz0); + int32_t g101 = gradient3D(x1, dx1, y0, dy0, z1, dz1); + int32_t g110 = gradient3D(x1, dx1, y1, dy1, z0, dz0); + int32_t g111 = gradient3D(x1, dx1, y1, dy1, z1, dz1); + + uint32_t tx = smoothstep(dx0); + uint32_t ty = smoothstep(dy0); + uint32_t tz = smoothstep(dz0); + + int32_t nx0 = lerpPerlin(g000, g100, tx); + int32_t nx1 = lerpPerlin(g010, g110, tx); + int32_t nx2 = lerpPerlin(g001, g101, tx); + int32_t nx3 = lerpPerlin(g011, g111, tx); + int32_t ny0 = lerpPerlin(nx0, nx1, ty); + int32_t ny1 = lerpPerlin(nx2, nx3, ty); + + int32_t noise = lerpPerlin(ny0, ny1, tz); + return noise; +} + +// scaling functions for fastled replacement +uint16_t perlin16(uint32_t x) { + return ((perlin1D_raw(x) * 1159) >> 10) + 32803; //scale to 16bit and offset (fastled range: about 4838 to 60766) +} + +uint16_t perlin16(uint32_t x, uint32_t y) { + return ((perlin2D_raw(x, y) * 1537) >> 10) + 32725; //scale to 16bit and offset (fastled range: about 1748 to 63697) +} + +uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z) { + return ((perlin3D_raw(x, y, z) * 1731) >> 10) + 33147; //scale to 16bit and offset (fastled range: about 4766 to 60840) +} + +uint8_t perlin8(uint16_t x) { + return (((perlin1D_raw((uint32_t)x << 8, true) * 1353) >> 10) + 32769) >> 8; //scale to 16 bit, offset, then scale to 8bit +} + +uint8_t perlin8(uint16_t x, uint16_t y) { + return (((perlin2D_raw((uint32_t)x << 8, (uint32_t)y << 8, true) * 1620) >> 10) + 32771) >> 8; //scale to 16 bit, offset, then scale to 8bit +} + +uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) { + return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 2015) >> 10) + 33168) >> 8; //scale to 16 bit, offset, then scale to 8bit +} + +// Platform-agnostic SHA1 computation from String input +String computeSHA1(const String& input) { + #ifdef ESP8266 + return sha1(input); // ESP8266 has built-in sha1() function + #else + // ESP32: Compute SHA1 hash using mbedtls + unsigned char shaResult[20]; // SHA1 produces 20 bytes + mbedtls_sha1_context ctx; + + mbedtls_sha1_init(&ctx); + mbedtls_sha1_starts_ret(&ctx); + mbedtls_sha1_update_ret(&ctx, (const unsigned char*)input.c_str(), input.length()); + mbedtls_sha1_finish_ret(&ctx, shaResult); + mbedtls_sha1_free(&ctx); + + // Convert to hexadecimal string + char hexString[41]; + for (int i = 0; i < 20; i++) { + sprintf(&hexString[i*2], "%02x", shaResult[i]); + } + hexString[40] = '\0'; + + return String(hexString); + #endif +} + +#ifdef ESP32 +#include "esp_adc_cal.h" +String generateDeviceFingerprint() { + uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + esp_efuse_mac_get_default((uint8_t*)fp); + fp[1] ^= ESP.getFlashChipSize(); + fp[0] ^= chip_info.full_revision | (chip_info.model << 16); + // mix in ADC calibration data: + esp_adc_cal_characteristics_t ch; + #if SOC_ADC_MAX_BITWIDTH == 13 // S2 has 13 bit ADC + constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_13; + #else + constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_12; + #endif + esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, myBIT_WIDTH, 1100, &ch); + fp[0] ^= ch.coeff_a; + fp[1] ^= ch.coeff_b; + if (ch.low_curve) { + for (int i = 0; i < 8; i++) { + fp[0] ^= ch.low_curve[i]; + } + } + if (ch.high_curve) { + for (int i = 0; i < 8; i++) { + fp[1] ^= ch.high_curve[i]; + } + } + char fp_string[17]; // 16 hex chars + null terminator + sprintf(fp_string, "%08X%08X", fp[1], fp[0]); + return String(fp_string); +} +#else // ESP8266 +String generateDeviceFingerprint() { + uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint + WiFi.macAddress((uint8_t*)&fp); // use MAC address as fingerprint base + fp[0] ^= ESP.getFlashChipId(); + fp[1] ^= ESP.getFlashChipSize() | ESP.getFlashChipVendorId() << 16; + char fp_string[17]; // 16 hex chars + null terminator + sprintf(fp_string, "%08X%08X", fp[1], fp[0]); + return String(fp_string); +} +#endif + +// Generate a device ID based on SHA1 hash of MAC address salted with other unique device info +// Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total) +String getDeviceId() { + static String cachedDeviceId = ""; + if (cachedDeviceId.length() > 0) return cachedDeviceId; + // The device string is deterministic as it needs to be consistent for the same device, even after a full flash erase + // MAC is salted with other consistent device info to avoid rainbow table attacks. + // If the MAC address is known by malicious actors, they could precompute SHA1 hashes to impersonate devices, + // but as WLED developers are just looking at statistics and not authenticating devices, this is acceptable. + // If the usage data was exfiltrated, you could not easily determine the MAC from the device ID without brute forcing SHA1 + + String firstHash = computeSHA1(generateDeviceFingerprint()); + + // Second hash: SHA1 of the first hash + String secondHash = computeSHA1(firstHash); + + // Concatenate first hash + last 2 chars of second hash + cachedDeviceId = firstHash + secondHash.substring(38); + + return cachedDeviceId; +} + diff --git a/wled00/wled.cpp b/wled00/wled.cpp index d6a39a399f..d67f784078 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -1,7 +1,11 @@ #define WLED_DEFINE_GLOBAL_VARS //only in one source file, wled.cpp! #include "wled.h" #include "wled_ethernet.h" -#include +#include "ota_update.h" +#ifdef WLED_ENABLE_AOTA + #define NO_OTA_PORT + #include +#endif #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET) #include "soc/soc.h" @@ -36,8 +40,8 @@ void WLED::reset() void WLED::loop() { - static uint32_t lastHeap = UINT32_MAX; - static unsigned long heapTime = 0; + static uint16_t heapTime = 0; // timestamp for heap check + static uint8_t heapDanger = 0; // counter for consecutive low-heap readings #ifdef WLED_DEBUG static unsigned long lastRun = 0; unsigned long loopMillis = millis(); @@ -65,7 +69,10 @@ void WLED::loop() handleNotifications(); handleTransitions(); #ifdef WLED_ENABLE_DMX - handleDMX(); + handleDMXOutput(); + #endif + #ifdef WLED_ENABLE_DMX_INPUT + dmxInput.update(); #endif #ifdef WLED_DEBUG @@ -84,6 +91,9 @@ void WLED::loop() #ifndef WLED_DISABLE_INFRARED handleIR(); #endif + #ifndef WLED_DISABLE_ESPNOW + handleRemote(); + #endif #ifndef WLED_DISABLE_ALEXA handleAlexa(); #endif @@ -99,11 +109,10 @@ void WLED::loop() if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) // block stuff if WARLS/Adalight is enabled { if (apActive) dnsServer.processNextRequest(); - #ifndef WLED_DISABLE_OTA - if (WLED_CONNECTED && aOtaEnabled && !otaLock && correctPIN) ArduinoOTA.handle(); + #ifdef WLED_ENABLE_AOTA + if (Network.isConnected() && aOtaEnabled && !otaLock && correctPIN) ArduinoOTA.handle(); #endif handleNightlight(); - handlePlaylist(); yield(); #ifndef WLED_DISABLE_HUESYNC @@ -111,6 +120,10 @@ void WLED::loop() yield(); #endif + if (!presetNeedsSaving()) { + handlePlaylist(); + yield(); + } handlePresets(); yield(); @@ -154,22 +167,49 @@ void WLED::loop() // 15min PIN time-out if (strlen(settingsPIN)>0 && correctPIN && millis() - lastEditTime > PIN_TIMEOUT) { correctPIN = false; - createEditHandler(false); } - // reconnect WiFi to clear stale allocations if heap gets too low - if (millis() - heapTime > 15000) { - uint32_t heap = ESP.getFreeHeap(); - if (heap < MIN_HEAP_SIZE && lastHeap < MIN_HEAP_SIZE) { - DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap); - forceReconnect = true; - strip.resetSegments(); // remove all but one segments from memory - } else if (heap < MIN_HEAP_SIZE) { - DEBUG_PRINTLN(F("Heap low, purging segments.")); - strip.purgeSegments(); + // free memory and reconnect WiFi to clear stale allocations if heap is too low for too long, check once every 5s + if ((uint16_t)(millis() - heapTime) > 5000) { + #ifdef ESP8266 + uint32_t heap = getFreeHeapSize(); // ESP8266 needs ~8k of free heap for UI to work properly + #else + #ifdef CONFIG_IDF_TARGET_ESP32C3 + // calling getContiguousFreeHeap() during led update causes glitches on C3 + // this can (probably) be removed once RMT driver for C3 is fixed + unsigned t0 = millis(); + while (strip.isUpdating() && (millis() - t0 < 15)) delay(1); // be nice, but not too nice. Waits up to 15ms + #endif + uint32_t heap = getContiguousFreeHeap(); // ESP32 family needs ~10k of contiguous free heap for UI to work properly + #endif + if (heap < MIN_HEAP_SIZE - 1024) heapDanger+=5; // allow 1k of "wiggle room" for things that do not respect min heap limits + else heapDanger = 0; + switch (heapDanger) { + case 15: // 15 consecutive seconds + DEBUG_PRINTLN(F("Heap low, purging segments")); + strip.purgeSegments(); + strip.setTransition(0); // disable transitions + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + strip.getSegments()[i].setMode(FX_MODE_STATIC); // set static mode to free effect memory + } + errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: segment reset + break; + case 30: // 30 consecutive seconds + DEBUG_PRINTLN(F("Heap low, reset segments")); + strip.resetSegments(); // remove all but one segments from memory + errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: segment reset + break; + case 45: // 45 consecutive seconds + DEBUG_PRINTF_P(PSTR("Heap panic! Reset strip, reset connection\n")); + strip.~WS2812FX(); // deallocate strip and all its memory + new(&strip) WS2812FX(); // re-create strip object, respecting current memory limits + if (!Update.isRunning()) forceReconnect = true; // in case wifi is broken, make sure UI comes back, set disableForceReconnect = true to avert + errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: strip reset + break; + default: + break; } - lastHeap = heap; - heapTime = millis(); + heapTime = (uint16_t)millis(); } //LED settings have been saved, re-init busses @@ -178,58 +218,18 @@ void WLED::loop() doInitBusses = false; DEBUG_PRINTLN(F("Re-init busses.")); bool aligned = strip.checkSegmentAlignment(); //see if old segments match old bus(ses) - BusManager::removeAll(); - unsigned mem = 0; - // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) - bool useParallel = false; - #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) - unsigned digitalCount = 0; - unsigned maxLedsOnBus = 0; - unsigned maxChannels = 0; - for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - if (busConfigs[i] == nullptr) break; - if (!Bus::isDigital(busConfigs[i]->type)) continue; - if (!Bus::is2Pin(busConfigs[i]->type)) { - digitalCount++; - unsigned channels = Bus::getNumberOfChannels(busConfigs[i]->type); - if (busConfigs[i]->count > maxLedsOnBus) maxLedsOnBus = busConfigs[i]->count; - if (channels > maxChannels) maxChannels = channels; - } - } - DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); - // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 - if (maxLedsOnBus <= 300 && digitalCount > 5) { - DEBUG_PRINTF_P(PSTR("Switching to parallel I2S.")); - useParallel = true; - BusManager::useParallelOutput(); - mem = BusManager::memUsage(maxChannels, maxLedsOnBus, 8); // use alternate memory calculation (hse to be used *after* useParallelOutput()) - } - #endif - // create buses/outputs - for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - if (busConfigs[i] == nullptr || (!useParallel && i > 10)) break; - if (useParallel && i < 8) { - // if for some unexplained reason the above pre-calculation was wrong, update - unsigned memT = BusManager::memUsage(*busConfigs[i]); // includes x8 memory allocation for parallel I2S - if (memT > mem) mem = memT; // if we have unequal LED count use the largest - } else - mem += BusManager::memUsage(*busConfigs[i]); // includes global buffer - if (mem <= MAX_LED_MEMORY) BusManager::add(*busConfigs[i]); - delete busConfigs[i]; - busConfigs[i] = nullptr; - } - strip.finalizeInit(); // also loads default ledmap if present - BusManager::setBrightness(bri); // fix re-initialised bus' brightness #4005 + strip.finalizeInit(); // will create buses and also load default ledmap if present if (aligned) strip.makeAutoSegments(); else strip.fixInvalidSegments(); - doSerializeConfig = true; + BusManager::setBrightness(scaledBri(bri)); // fix re-initialised bus' brightness #4005 and #4824 + configNeedsWrite = true; } if (loadLedmap >= 0) { strip.deserializeMap(loadLedmap); loadLedmap = -1; } yield(); - if (doSerializeConfig) serializeConfig(); + if (configNeedsWrite) serializeConfigToFS(); yield(); handleWs(); @@ -252,30 +252,54 @@ void WLED::loop() } #endif - if (doReboot && (!doInitBusses || !doSerializeConfig)) // if busses have to be inited & saved, wait until next iteration + if (doReboot && (!doInitBusses || !configNeedsWrite)) // if busses have to be inited & saved, wait until next iteration reset(); // DEBUG serial logging (every 30s) #ifdef WLED_DEBUG loopMillis = millis() - loopMillis; - if (loopMillis > 30) { - DEBUG_PRINTF_P(PSTR("Loop took %lums.\n"), loopMillis); - DEBUG_PRINTF_P(PSTR("Usermods took %lums.\n"), usermodMillis); - DEBUG_PRINTF_P(PSTR("Strip took %lums.\n"), stripMillis); - } + //if (loopMillis > 30) { + // DEBUG_PRINTF_P(PSTR("Loop took %lums.\n"), loopMillis); + // DEBUG_PRINTF_P(PSTR("Usermods took %lums.\n"), usermodMillis); + // DEBUG_PRINTF_P(PSTR("Strip took %lums.\n"), stripMillis); + //} avgLoopMillis += loopMillis; if (loopMillis > maxLoopMillis) maxLoopMillis = loopMillis; if (millis() - debugTime > 29999) { DEBUG_PRINTLN(F("---DEBUG INFO---")); DEBUG_PRINTF_P(PSTR("Runtime: %lu\n"), millis()); DEBUG_PRINTF_P(PSTR("Unix time: %u,%03u\n"), toki.getTime().sec, toki.getTime().ms); - DEBUG_PRINTF_P(PSTR("Free heap: %u\n"), ESP.getFreeHeap()); #if defined(ARDUINO_ARCH_ESP32) + DEBUG_PRINTLN(F("=== Memory Info ===")); + // Internal DRAM (standard 8-bit accessible heap) + size_t dram_free = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL); + size_t dram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL); + DEBUG_PRINTF_P(PSTR("DRAM 8-bit: Free: %7u bytes | Largest block: %7u bytes\n"), dram_free, dram_largest); + #ifdef BOARD_HAS_PSRAM + size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + size_t psram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM); + DEBUG_PRINTF_P(PSTR("PSRAM: Free: %7u bytes | Largest block: %6u bytes\n"), psram_free, psram_largest); + #endif + #if defined(CONFIG_IDF_TARGET_ESP32) + // 32-bit DRAM (not byte accessible, only available on ESP32) + size_t dram32_free = heap_caps_get_free_size(MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL) - dram_free; // returns all 32bit DRAM, subtract 8bit DRAM + //size_t dram32_largest = heap_caps_get_largest_free_block(MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL); // returns largest DRAM block -> not useful + DEBUG_PRINTF_P(PSTR("DRAM 32-bit: Free: %7u bytes | Largest block: N/A\n"), dram32_free); + #else + // Fast RTC Memory (not available on ESP32) + size_t rtcram_free = heap_caps_get_free_size(MALLOC_CAP_RTCRAM); + size_t rtcram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_RTCRAM); + DEBUG_PRINTF_P(PSTR("RTC RAM: Free: %7u bytes | Largest block: %7u bytes\n"), rtcram_free, rtcram_largest); + #endif if (psramFound()) { DEBUG_PRINTF_P(PSTR("PSRAM: %dkB/%dkB\n"), ESP.getFreePsram()/1024, ESP.getPsramSize()/1024); - if (!psramSafe) DEBUG_PRINTLN(F("Not using PSRAM.")); + #ifndef BOARD_HAS_PSRAM + DEBUG_PRINTLN(F("BOARD_HAS_PSRAM not defined, not using PSRAM.")); + #endif } DEBUG_PRINTF_P(PSTR("TX power: %d/%d\n"), WiFi.getTxPower(), txPower); + #else // ESP8266 + DEBUG_PRINTF_P(PSTR("Free heap/contiguous: %u/%u\n"), getFreeHeapSize(), getContiguousFreeHeap()); #endif DEBUG_PRINTF_P(PSTR("Wifi state: %d\n"), WiFi.status()); #ifndef WLED_DISABLE_ESPNOW @@ -296,6 +320,7 @@ void WLED::loop() DEBUG_PRINTF_P(PSTR("Strip time[ms]:%u/%lu\n"), avgStripMillis/loops, maxStripMillis); } strip.printSize(); + server.printStatus(DEBUGOUT); loops = 0; maxLoopMillis = 0; maxUsermodMillis = 0; @@ -370,7 +395,6 @@ void WLED::setup() #else DEBUG_PRINTLN(F("arduino-esp32 v1.0.x\n")); // we can't say in more detail. #endif - DEBUG_PRINTF_P(PSTR("CPU: %s rev.%d, %d core(s), %d MHz.\n"), ESP.getChipModel(), (int)ESP.getChipRevision(), ESP.getChipCores(), ESP.getCpuFreqMHz()); DEBUG_PRINTF_P(PSTR("FLASH: %d MB, Mode %d "), (ESP.getFlashChipSize()/1024)/1024, (int)ESP.getFlashChipMode()); #ifdef WLED_DEBUG @@ -395,20 +419,20 @@ void WLED::setup() DEBUG_PRINTF_P(PSTR("esp8266 @ %u MHz.\nCore: %s\n"), ESP.getCpuFreqMHz(), ESP.getCoreVersion()); DEBUG_PRINTF_P(PSTR("FLASH: %u MB\n"), (ESP.getFlashChipSize()/1024)/1024); #endif - DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); -#if defined(ARDUINO_ARCH_ESP32) - // BOARD_HAS_PSRAM also means that a compiler flag "-mfix-esp32-psram-cache-issue" was used and so PSRAM is safe to use on rev.1 ESP32 - #if !defined(BOARD_HAS_PSRAM) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) - if (psramFound() && ESP.getChipRevision() < 3) psramSafe = false; - if (!psramSafe) DEBUG_PRINTLN(F("Not using PSRAM.")); - #endif - pDoc = new PSRAMDynamicJsonDocument((psramSafe && psramFound() ? 2 : 1)*JSON_BUFFER_SIZE); - DEBUG_PRINTF_P(PSTR("JSON buffer allocated: %u\n"), (psramSafe && psramFound() ? 2 : 1)*JSON_BUFFER_SIZE); - // if the above fails requestJsonBufferLock() will always return false preventing crashes - if (psramFound()) { +#if defined(BOARD_HAS_PSRAM) + // if JSON buffer allocation fails requestJsonBufferLock() will always return false preventing crashes + if (psramFound() && ESP.getPsramSize()) { + pDoc = new PSRAMDynamicJsonDocument(2 * JSON_BUFFER_SIZE); + DEBUG_PRINTF_P(PSTR("JSON buffer size: %ubytes\n"), (2 * JSON_BUFFER_SIZE)); DEBUG_PRINTF_P(PSTR("PSRAM: %dkB/%dkB\n"), ESP.getFreePsram()/1024, ESP.getPsramSize()/1024); + } else { + pDoc = new DynamicJsonDocument(JSON_BUFFER_SIZE); // Use onboard RAM instead as a fallback } +#endif + +#if defined(ARDUINO_ARCH_ESP32) DEBUG_PRINTF_P(PSTR("TX power: %d/%d\n"), WiFi.getTxPower(), txPower); #endif @@ -423,10 +447,7 @@ void WLED::setup() PinManager::allocatePin(2, true, PinOwner::DMX); #endif - DEBUG_PRINTLN(F("Registering usermods ...")); - registerUsermods(); - - DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); bool fsinit = false; DEBUGFS_PRINTLN(F("Mount FS")); @@ -439,11 +460,9 @@ void WLED::setup() DEBUGFS_PRINTLN(F("FS failed!")); errorFlag = ERR_FS_BEGIN; } -#ifdef WLED_ADD_EEPROM_SUPPORT - else deEEP(); -#else + + handleBootLoop(); // check for bootloop and take action (requires WLED_FS) initPresetsFile(); -#endif updateFSInfo(); // generate module IDs must be done before AP setup @@ -454,9 +473,14 @@ void WLED::setup() WLED_SET_AP_SSID(); // otherwise it is empty on first boot until config is saved multiWiFi.push_back(WiFiConfig(CLIENT_SSID,CLIENT_PASS)); // initialise vector with default WiFi + if(!verifyConfig()) { + if(!restoreConfig()) { + resetConfig(); + } + } DEBUG_PRINTLN(F("Reading config")); - deserializeConfigFromFS(); - DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); + bool needsCfgSave = deserializeConfigFromFS(); + DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); #if defined(STATUSLED) && STATUSLED>=0 if (!PinManager::isPinAllocated(STATUSLED)) { @@ -468,20 +492,25 @@ void WLED::setup() DEBUG_PRINTLN(F("Initializing strip")); beginStrip(); - DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); DEBUG_PRINTLN(F("Usermods setup")); userSetup(); UsermodManager::setup(); - DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); + + if (needsCfgSave) serializeConfigToFS(); // usermods required new parameters; need to wait for strip to be initialised #4752 - if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0) + if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0 && !configBackupExists()) showWelcomePage = true; - WiFi.persistent(false); - #ifdef WLED_USE_ETHERNET - WiFi.onEvent(WiFiEvent); - #endif + #ifndef ESP8266 + WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); + WiFi.persistent(true); // storing credentials in NVM fixes boot-up pause as connection is much faster, is disabled after first connection + #else + WiFi.persistent(false); // on ES8266 using NVM for wifi config has no benefit of faster connection + #endif + WiFi.onEvent(WiFiEvent); WiFi.mode(WIFI_STA); // enable scanning findWiFi(true); // start scanning for available WiFi-s @@ -498,13 +527,13 @@ void WLED::setup() #endif // fill in unique mdns default - if (strcmp(cmDNS, "x") == 0) sprintf_P(cmDNS, PSTR("wled-%*s"), 6, escapedMac.c_str() + 6); + if (strcmp(cmDNS, DEFAULT_MDNS_NAME) == 0) sprintf_P(cmDNS, PSTR("wled-%*s"), 6, escapedMac.c_str() + 6); #ifndef WLED_DISABLE_MQTT if (mqttDeviceTopic[0] == 0) sprintf_P(mqttDeviceTopic, PSTR("wled/%*s"), 6, escapedMac.c_str() + 6); if (mqttClientID[0] == 0) sprintf_P(mqttClientID, PSTR("WLED-%*s"), 6, escapedMac.c_str() + 6); #endif -#ifndef WLED_DISABLE_OTA +#ifdef WLED_ENABLE_AOTA if (aOtaEnabled) { ArduinoOTA.onStart([]() { #ifdef ESP8266 @@ -526,7 +555,10 @@ void WLED::setup() } #endif #ifdef WLED_ENABLE_DMX - initDMX(); + initDMXOutput(); +#endif +#ifdef WLED_ENABLE_DMX_INPUT + dmxInput.init(dmxInputReceivePin, dmxInputTransmitPin, dmxInputEnablePin, dmxInputPort); #endif #ifdef WLED_ENABLE_ADALIGHT @@ -536,24 +568,18 @@ void WLED::setup() // HTTP server page init DEBUG_PRINTLN(F("initServer")); initServer(); - DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); #ifndef WLED_DISABLE_INFRARED // init IR DEBUG_PRINTLN(F("initIR")); initIR(); - DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); #endif // Seed FastLED random functions with an esp random value, which already works properly at this point. -#if defined(ARDUINO_ARCH_ESP32) - const uint32_t seed32 = esp_random(); -#elif defined(ARDUINO_ARCH_ESP8266) - const uint32_t seed32 = RANDOM_REG32; -#else - const uint32_t seed32 = random(std::numeric_limits::max()); -#endif - random16_set_seed((uint16_t)((seed32 & 0xFFFF) ^ (seed32 >> 16))); + const uint32_t seed32 = hw_random(); + random16_set_seed((uint16_t)seed32); #if WLED_WATCHDOG_TIMEOUT > 0 enableWatchdog(); @@ -562,15 +588,22 @@ void WLED::setup() #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET) WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); //enable brownout detector #endif + markOTAvalid(); } void WLED::beginStrip() { // Initialize NeoPixel Strip and button + strip.setTransition(0); // temporarily prevent transitions to reduce segment copies strip.finalizeInit(); // busses created during deserializeConfig() if config existed strip.makeAutoSegments(); strip.setBrightness(0); strip.setShowCallback(handleOverlayDraw); + doInitBusses = false; + + // init offMode and relay + offMode = false; // init to on state to allow proper relay init + handleOnOff(true); // init relay and force off if (turnOnAtBoot) { if (briS > 0) bri = briS; @@ -578,26 +611,24 @@ void WLED::beginStrip() } else { // fix for #3196 if (bootPreset > 0) { - bool oldTransition = fadeTransition; // workaround if transitions are enabled - fadeTransition = false; // ignore transitions temporarily - strip.setColor(0, BLACK); // set all segments black - fadeTransition = oldTransition; // restore transitions - col[0] = col[1] = col[2] = col[3] = 0; // needed for colorUpdated() + // set all segments black (no transition) + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (seg.isActive()) seg.colors[0] = BLACK; + } + colPri[0] = colPri[1] = colPri[2] = colPri[3] = 0; // needed for colorUpdated() } briLast = briS; bri = 0; strip.fill(BLACK); - strip.show(); + if (rlyPin < 0) + strip.show(); // ensure LEDs are off if no relay is used } + colorUpdated(CALL_MODE_INIT); // will not send notification but will initiate transition if (bootPreset > 0) { applyPreset(bootPreset, CALL_MODE_INIT); } - colorUpdated(CALL_MODE_INIT); // will not send notification - // init relay pin - if (rlyPin >= 0) { - pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); - digitalWrite(rlyPin, (rlyMde ? bri : !bri)); - } + strip.setTransition(transitionDelayDefault); // restore transitions } void WLED::initAP(bool resetAP) @@ -639,150 +670,9 @@ void WLED::initAP(bool resetAP) apActive = true; } -bool WLED::initEthernet() -{ -#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - - static bool successfullyConfiguredEthernet = false; - - if (successfullyConfiguredEthernet) { - // DEBUG_PRINTLN(F("initE: ETH already successfully configured, ignoring")); - return false; - } - if (ethernetType == WLED_ETH_NONE) { - return false; - } - if (ethernetType >= WLED_NUM_ETH_TYPES) { - DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); - return false; - } - - DEBUG_PRINTF_P(PSTR("initE: Attempting ETH config: %d\n"), ethernetType); - - // Ethernet initialization should only succeed once -- else reboot required - ethernet_settings es = ethernetBoards[ethernetType]; - managed_pin_type pinsToAllocate[10] = { - // first six pins are non-configurable - esp32_nonconfigurable_ethernet_pins[0], - esp32_nonconfigurable_ethernet_pins[1], - esp32_nonconfigurable_ethernet_pins[2], - esp32_nonconfigurable_ethernet_pins[3], - esp32_nonconfigurable_ethernet_pins[4], - esp32_nonconfigurable_ethernet_pins[5], - { (int8_t)es.eth_mdc, true }, // [6] = MDC is output and mandatory - { (int8_t)es.eth_mdio, true }, // [7] = MDIO is bidirectional and mandatory - { (int8_t)es.eth_power, true }, // [8] = optional pin, not all boards use - { ((int8_t)0xFE), false }, // [9] = replaced with eth_clk_mode, mandatory - }; - // update the clock pin.... - if (es.eth_clk_mode == ETH_CLOCK_GPIO0_IN) { - pinsToAllocate[9].pin = 0; - pinsToAllocate[9].isOutput = false; - } else if (es.eth_clk_mode == ETH_CLOCK_GPIO0_OUT) { - pinsToAllocate[9].pin = 0; - pinsToAllocate[9].isOutput = true; - } else if (es.eth_clk_mode == ETH_CLOCK_GPIO16_OUT) { - pinsToAllocate[9].pin = 16; - pinsToAllocate[9].isOutput = true; - } else if (es.eth_clk_mode == ETH_CLOCK_GPIO17_OUT) { - pinsToAllocate[9].pin = 17; - pinsToAllocate[9].isOutput = true; - } else { - DEBUG_PRINTF_P(PSTR("initE: Failing due to invalid eth_clk_mode (%d)\n"), es.eth_clk_mode); - return false; - } - - if (!PinManager::allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { - DEBUG_PRINTLN(F("initE: Failed to allocate ethernet pins")); - return false; - } - - /* - For LAN8720 the most correct way is to perform clean reset each time before init - applying LOW to power or nRST pin for at least 100 us (please refer to datasheet, page 59) - ESP_IDF > V4 implements it (150 us, lan87xx_reset_hw(esp_eth_phy_t *phy) function in - /components/esp_eth/src/esp_eth_phy_lan87xx.c, line 280) - but ESP_IDF < V4 does not. Lets do it: - [not always needed, might be relevant in some EMI situations at startup and for hot resets] - */ - #if ESP_IDF_VERSION_MAJOR==3 - if(es.eth_power>0 && es.eth_type==ETH_PHY_LAN8720) { - pinMode(es.eth_power, OUTPUT); - digitalWrite(es.eth_power, 0); - delayMicroseconds(150); - digitalWrite(es.eth_power, 1); - delayMicroseconds(10); - } - #endif - - if (!ETH.begin( - (uint8_t) es.eth_address, - (int) es.eth_power, - (int) es.eth_mdc, - (int) es.eth_mdio, - (eth_phy_type_t) es.eth_type, - (eth_clock_mode_t) es.eth_clk_mode - )) { - DEBUG_PRINTLN(F("initC: ETH.begin() failed")); - // de-allocate the allocated pins - for (managed_pin_type mpt : pinsToAllocate) { - PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet); - } - return false; - } - - successfullyConfiguredEthernet = true; - DEBUG_PRINTLN(F("initC: *** Ethernet successfully configured! ***")); - return true; -#else - return false; // Ethernet not enabled for build -#endif -} - -// performs asynchronous scan for available networks (which may take couple of seconds to finish) -// returns configured WiFi ID with the strongest signal (or default if no configured networks available) -int8_t WLED::findWiFi(bool doScan) { - if (multiWiFi.size() <= 1) { - DEBUG_PRINTLN(F("Defaulf WiFi used.")); - return 0; - } - - if (doScan) WiFi.scanDelete(); // restart scan - - int status = WiFi.scanComplete(); // complete scan may take as much as several seconds (usually <3s with not very crowded air) - - if (status == WIFI_SCAN_FAILED) { - DEBUG_PRINTLN(F("WiFi scan started.")); - WiFi.scanNetworks(true); // start scanning in asynchronous mode - } else if (status >= 0) { // status contains number of found networks - DEBUG_PRINT(F("WiFi scan completed: ")); DEBUG_PRINTLN(status); - int rssi = -9999; - unsigned selected = selectedWiFi; - for (int o = 0; o < status; o++) { - DEBUG_PRINT(F(" WiFi available: ")); DEBUG_PRINT(WiFi.SSID(o)); - DEBUG_PRINT(F(" RSSI: ")); DEBUG_PRINT(WiFi.RSSI(o)); DEBUG_PRINTLN(F("dB")); - for (unsigned n = 0; n < multiWiFi.size(); n++) - if (!strcmp(WiFi.SSID(o).c_str(), multiWiFi[n].clientSSID)) { - // find the WiFi with the strongest signal (but keep priority of entry if signal difference is not big) - if ((n < selected && WiFi.RSSI(o) > rssi-10) || WiFi.RSSI(o) > rssi) { - rssi = WiFi.RSSI(o); - selected = n; - } - break; - } - } - DEBUG_PRINT(F("Selected: ")); DEBUG_PRINT(multiWiFi[selected].clientSSID); - DEBUG_PRINT(F(" RSSI: ")); DEBUG_PRINT(rssi); DEBUG_PRINTLN(F("dB")); - return selected; - } - //DEBUG_PRINT(F("WiFi scan running.")); - return status; // scan is still running or there was an error -} - void WLED::initConnection() { - DEBUG_PRINTLN(F("initConnection() called.")); - + DEBUG_PRINTF_P(PSTR("initConnection() called @ %lus.\n"), millis()/1000); #ifdef WLED_ENABLE_WEBSOCKETS ws.onEvent(wsEvent); #endif @@ -796,6 +686,7 @@ void WLED::initConnection() #endif WiFi.disconnect(true); // close old connections + delay(5); // wait for hardware to be ready #ifdef ESP8266 WiFi.setPhyMode(force802_3g ? WIFI_PHY_MODE_11G : WIFI_PHY_MODE_11N); #endif @@ -825,14 +716,60 @@ void WLED::initConnection() if (WLED_WIFI_CONFIGURED) { showWelcomePage = false; - DEBUG_PRINT(F("Connecting to ")); - DEBUG_PRINT(multiWiFi[selectedWiFi].clientSSID); - DEBUG_PRINTLN(F("...")); + DEBUG_PRINTF_P(PSTR("Connecting to %s...\n"), multiWiFi[selectedWiFi].clientSSID); // convert the "serverDescription" into a valid DNS hostname (alphanumeric) char hostname[25]; prepareHostname(hostname); - WiFi.begin(multiWiFi[selectedWiFi].clientSSID, multiWiFi[selectedWiFi].clientPass); // no harm if called multiple times + +#ifdef WLED_ENABLE_WPA_ENTERPRISE + if (multiWiFi[selectedWiFi].encryptionType == WIFI_ENCRYPTION_TYPE_PSK) { + DEBUG_PRINTLN(F("Using PSK")); +#ifdef ESP8266 + wifi_station_set_wpa2_enterprise_auth(0); + wifi_station_clear_enterprise_ca_cert(); + wifi_station_clear_enterprise_cert_key(); + wifi_station_clear_enterprise_identity(); + wifi_station_clear_enterprise_username(); + wifi_station_clear_enterprise_password(); +#endif + uint8_t *bssid = nullptr; + // check if user BSSID is non zero for current WiFi config + for (int i = 0; i < sizeof(multiWiFi[selectedWiFi].bssid); i++) { + if (multiWiFi[selectedWiFi].bssid[i] != 0) { + bssid = multiWiFi[selectedWiFi].bssid; // BSSID set, assign pointer and continue + break; + } + } + WiFi.begin(multiWiFi[selectedWiFi].clientSSID, multiWiFi[selectedWiFi].clientPass, 0, bssid); // no harm if called multiple times + } else { // WIFI_ENCRYPTION_TYPE_ENTERPRISE + DEBUG_PRINTF_P(PSTR("Using WPA2_AUTH_PEAP (Anon: %s, Ident: %s)\n"), multiWiFi[selectedWiFi].enterpriseAnonIdentity, multiWiFi[selectedWiFi].enterpriseIdentity); +#ifdef ESP8266 + struct station_config sta_conf; + os_memset(&sta_conf, 0, sizeof(sta_conf)); + os_memcpy(sta_conf.ssid, multiWiFi[selectedWiFi].clientSSID, 32); + os_memcpy(sta_conf.password, multiWiFi[selectedWiFi].clientPass, 64); + wifi_station_set_config(&sta_conf); + wifi_station_set_wpa2_enterprise_auth(1); + wifi_station_set_enterprise_identity((u8*)(void*)multiWiFi[selectedWiFi].enterpriseAnonIdentity, os_strlen(multiWiFi[selectedWiFi].enterpriseAnonIdentity)); + wifi_station_set_enterprise_username((u8*)(void*)multiWiFi[selectedWiFi].enterpriseIdentity, os_strlen(multiWiFi[selectedWiFi].enterpriseIdentity)); + wifi_station_set_enterprise_password((u8*)(void*)multiWiFi[selectedWiFi].clientPass, os_strlen(multiWiFi[selectedWiFi].clientPass)); + wifi_station_connect(); +#else + WiFi.begin(multiWiFi[selectedWiFi].clientSSID, WPA2_AUTH_PEAP, multiWiFi[selectedWiFi].enterpriseAnonIdentity, multiWiFi[selectedWiFi].enterpriseIdentity, multiWiFi[selectedWiFi].clientPass); +#endif + } +#else // WLED_ENABLE_WPA_ENTERPRISE + uint8_t *bssid = nullptr; + // check if user BSSID is non zero for current WiFi config + for (int i = 0; i < sizeof(multiWiFi[selectedWiFi].bssid); i++) { + if (multiWiFi[selectedWiFi].bssid[i] != 0) { + bssid = multiWiFi[selectedWiFi].bssid; // BSSID set, assign pointer and continue + break; + } + } + WiFi.begin(multiWiFi[selectedWiFi].clientSSID, multiWiFi[selectedWiFi].clientPass, 0, bssid); // no harm if called multiple times +#endif // WLED_ENABLE_WPA_ENTERPRISE #ifdef ARDUINO_ARCH_ESP32 WiFi.setTxPower(wifi_power_t(txPower)); @@ -883,9 +820,8 @@ void WLED::initInterfaces() alexaInit(); #endif -#ifndef WLED_DISABLE_OTA - if (aOtaEnabled) - ArduinoOTA.begin(); +#ifdef WLED_ENABLE_AOTA + if (aOtaEnabled) ArduinoOTA.begin(); #endif // Set up mDNS responder: @@ -915,9 +851,6 @@ void WLED::initInterfaces() e131.begin(e131Multicast, e131Port, e131Universe, E131_MAX_UNIVERSE_COUNT); ddp.begin(false, DDP_DEFAULT_PORT); reconnectHue(); -#ifndef WLED_DISABLE_MQTT - initMqtt(); -#endif interfacesInited = true; wasConnected = true; } @@ -926,7 +859,10 @@ void WLED::handleConnection() { static bool scanDone = true; static byte stacO = 0; - unsigned long now = millis(); + const unsigned long now = millis(); + #ifdef WLED_DEBUG + const unsigned long nowS = now/1000; + #endif const bool wifiConfigured = WLED_WIFI_CONFIGURED; // ignore connection handling if WiFi is configured and scan still running @@ -935,7 +871,7 @@ void WLED::handleConnection() return; if (lastReconnectAttempt == 0 || forceReconnect) { - DEBUG_PRINTLN(F("Initial connect or forced reconnect.")); + DEBUG_PRINTF_P(PSTR("Initial connect or forced reconnect (@ %lus).\n"), nowS); selectedWiFi = findWiFi(); // find strongest WiFi initConnection(); interfacesInited = false; @@ -955,9 +891,8 @@ void WLED::handleConnection() #endif if (stac != stacO) { stacO = stac; - DEBUG_PRINT(F("Connected AP clients: ")); - DEBUG_PRINTLN(stac); - if (!WLED_CONNECTED && wifiConfigured) { // trying to connect, but not connected + DEBUG_PRINTF_P(PSTR("Connected AP clients: %d\n"), (int)stac); + if (!Network.isConnected() && wifiConfigured) { // trying to connect, but not connected if (stac) WiFi.disconnect(); // disable search so that AP can work else @@ -979,6 +914,7 @@ void WLED::handleConnection() initConnection(); interfacesInited = false; scanDone = true; + return; } //send improv failed 6 seconds after second init attempt (24 sec. after provisioning) if (improvActive > 2 && now - lastReconnectAttempt > 6000) { @@ -987,13 +923,13 @@ void WLED::handleConnection() } if (now - lastReconnectAttempt > ((stac) ? 300000 : 18000) && wifiConfigured) { if (improvActive == 2) improvActive = 3; - DEBUG_PRINTLN(F("Last reconnect too old.")); + DEBUG_PRINTF_P(PSTR("Last reconnect (%lus) too old (@ %lus).\n"), lastReconnectAttempt/1000, nowS); if (++selectedWiFi >= multiWiFi.size()) selectedWiFi = 0; // we couldn't connect, try with another network from the list initConnection(); } if (!apActive && now - lastReconnectAttempt > 12000 && (!wasConnected || apBehavior == AP_BEHAVIOR_NO_CONN)) { if (!(apBehavior == AP_BEHAVIOR_TEMPORARY && now > WLED_AP_TIMEOUT)) { - DEBUG_PRINTLN(F("Not connected AP.")); + DEBUG_PRINTF_P(PSTR("Not connected AP (@ %lus).\n"), nowS); initAP(); // start AP only within first 5min } } @@ -1003,13 +939,16 @@ void WLED::handleConnection() dnsServer.stop(); WiFi.softAPdisconnect(true); apActive = false; - DEBUG_PRINTLN(F("Temporary AP disabled.")); + DEBUG_PRINTF_P(PSTR("Temporary AP disabled (@ %lus).\n"), nowS); } } } else if (!interfacesInited) { //newly connected DEBUG_PRINTLN(); DEBUG_PRINT(F("Connected! IP address: ")); DEBUG_PRINTLN(Network.localIP()); + #ifdef ARDUINO_ARCH_ESP32 + esp_wifi_set_storage(WIFI_STORAGE_RAM); // disable further updates of NVM credentials to prevent wear on flash (same as WiFi.persistent(false) but updates immediately, arduino wifi deficiency workaround) + #endif if (improvActive) { if (improvError == 3) sendImprovStateResponse(0x00, true); sendImprovStateResponse(0x04); @@ -1031,7 +970,7 @@ void WLED::handleConnection() } // If status LED pin is allocated for other uses, does nothing -// else blink at 1Hz when WLED_CONNECTED is false (no WiFi, ?? no Ethernet ??) +// else blink at 1Hz when Network.isConnected() is false (no WiFi, ?? no Ethernet ??) // else blink at 2Hz when MQTT is enabled but not connected // else turn the status LED off #if defined(STATUSLED) @@ -1045,7 +984,7 @@ void WLED::handleStatusLED() } #endif - if (WLED_CONNECTED) { + if (Network.isConnected()) { c = RGBW32(0,255,0,0); ledStatusType = 2; } else if (WLED_MQTT_CONNECTED) { diff --git a/wled00/wled.h b/wled00/wled.h index fcbc119784..21b340d94c 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -3,12 +3,11 @@ /* Main sketch, global variable declarations @title WLED project sketch - @version 0.15.0-b7 @author Christian Schwinne */ // version code in format yymmddb (b = daily build) -#define VERSION 2410270 +#define VERSION 2602141 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG @@ -16,12 +15,18 @@ // ESP8266-01 (blue) got too little storage space to work with WLED. 0.10.2 is the last release supporting this unit. // ESP8266-01 (black) has 1MB flash and can thus fit the whole program, although OTA update is not possible. Use 1M(128K SPIFFS). -// 2-step OTA may still be possible: https://github.com/Aircoookie/WLED/issues/2040#issuecomment-981111096 +// 2-step OTA may still be possible: https://github.com/wled-dev/WLED/issues/2040#issuecomment-981111096 // Uncomment some of the following lines to disable features: // Alternatively, with platformio pass your chosen flags to your custom build target in platformio_override.ini // You are required to disable over-the-air updates: //#define WLED_DISABLE_OTA // saves 14kb +#ifdef WLED_ENABLE_AOTA + #if defined(WLED_DISABLE_OTA) + #warning WLED_DISABLE_OTA was defined but it will be ignored due to WLED_ENABLE_AOTA. + #endif + #undef WLED_DISABLE_OTA +#endif // You can choose some of these features to disable: //#define WLED_DISABLE_ALEXA // saves 11kb @@ -65,10 +70,16 @@ //This is generally a terrible idea, but improves boot success on boards with a 3.3v regulator + cap setup that can't provide 400mA peaks //#define WLED_DISABLE_BROWNOUT_DET +#include +#include + // Library inclusions. #include #ifdef ESP8266 #include + #ifdef WLED_ENABLE_WPA_ENTERPRISE + #include "wpa2_enterprise.h" + #endif #include #include #include @@ -114,15 +125,8 @@ #endif #include -#ifdef WLED_ADD_EEPROM_SUPPORT - #include -#endif #include #include -#ifndef WLED_DISABLE_OTA - #define NO_OTA_PORT - #include -#endif #include #include "src/dependencies/time/TimeLib.h" #include "src/dependencies/timezone/Timezone.h" @@ -145,9 +149,13 @@ #endif #endif +#ifdef WLED_ENABLE_DMX_INPUT + #include "dmx_input.h" +#endif + #include "src/dependencies/e131/ESPAsyncE131.h" #ifndef WLED_DISABLE_MQTT -#include "src/dependencies/async-mqtt-client/AsyncMqttClient.h" +#include #endif #define ARDUINOJSON_DECODE_UNICODE 0 @@ -159,16 +167,13 @@ // The following is a construct to enable code to compile without it. // There is a code that will still not use PSRAM though: // AsyncJsonResponse is a derived class that implements DynamicJsonDocument (AsyncJson-v6.h) -#if defined(ARDUINO_ARCH_ESP32) -extern bool psramSafe; +#if defined(BOARD_HAS_PSRAM) struct PSRAM_Allocator { void* allocate(size_t size) { - if (psramSafe && psramFound()) return ps_malloc(size); // use PSRAM if it exists - else return malloc(size); // fallback + return ps_malloc(size); // use PSRAM } void* reallocate(void* ptr, size_t new_size) { - if (psramSafe && psramFound()) return ps_realloc(ptr, new_size); // use PSRAM if it exists - else return realloc(ptr, new_size); // fallback + return ps_realloc(ptr, new_size); // use PSRAM } void deallocate(void* pointer) { free(pointer); @@ -184,10 +189,15 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #include "FastLED.h" #include "const.h" #include "fcn_declare.h" +#ifndef WLED_DISABLE_OTA + #include "ota_update.h" +#endif #include "NodeStruct.h" #include "pin_manager.h" +#include "colors.h" #include "bus_manager.h" #include "FX.h" +#include "wled_metadata.h" #ifndef CLIENT_SSID #define CLIENT_SSID DEFAULT_CLIENT_SSID @@ -214,6 +224,10 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #define WLED_AP_PASS DEFAULT_AP_PASS #endif +#ifndef WLED_PIN + #define WLED_PIN "" +#endif + #ifndef SPIFFS_EDITOR_AIRCOOOKIE #error You are not using the Aircoookie fork of the ESPAsyncWebserver library.\ Using upstream puts your WiFi password at risk of being served by the filesystem.\ @@ -260,28 +274,22 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #define STRINGIFY(X) #X #define TOSTRING(X) STRINGIFY(X) -#ifndef WLED_VERSION - #define WLED_VERSION dev -#endif -#ifndef WLED_RELEASE_NAME - #define WLED_RELEASE_NAME "Custom" -#endif - -// Global Variable definitions -WLED_GLOBAL char versionString[] _INIT(TOSTRING(WLED_VERSION)); -WLED_GLOBAL char releaseString[] _INIT(WLED_RELEASE_NAME); // must include the quotes when defining, e.g -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\" -#define WLED_CODENAME "Kōsen" +#define WLED_CODENAME "Niji" // AP and OTA default passwords (for maximum security change them!) WLED_GLOBAL char apPass[65] _INIT(WLED_AP_PASS); +#ifdef WLED_OTA_PASS +WLED_GLOBAL char otaPass[33] _INIT(WLED_OTA_PASS); +#else WLED_GLOBAL char otaPass[33] _INIT(DEFAULT_OTA_PASS); +#endif // Hardware and pin config #ifndef BTNPIN - #define BTNPIN 0,-1 + #define BTNPIN 0 #endif #ifndef BTNTYPE - #define BTNTYPE BTN_TYPE_PUSH,BTN_TYPE_NONE + #define BTNTYPE BTN_TYPE_PUSH #endif #ifndef RLYPIN WLED_GLOBAL int8_t rlyPin _INIT(-1); @@ -356,8 +364,8 @@ WLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CO #define noWifiSleep wifiOpt.noWifiSleep #define force802_3g wifiOpt.force802_3g #else -WLED_GLOBAL uint8_t selectedWiFi _INIT(0); -WLED_GLOBAL byte apChannel _INIT(1); // 2.4GHz WiFi AP channel (1-13) +WLED_GLOBAL int8_t selectedWiFi _INIT(0); +WLED_GLOBAL byte apChannel _INIT(6); // 2.4GHz WiFi AP channel (1-13) WLED_GLOBAL byte apHide _INIT(0); // hidden AP SSID WLED_GLOBAL byte apBehavior _INIT(AP_BEHAVIOR_BOOT_NO_CONN); // access point opens when no connection after boot by default #ifdef ARDUINO_ARCH_ESP32 @@ -368,15 +376,15 @@ WLED_GLOBAL bool noWifiSleep _INIT(false); WLED_GLOBAL bool force802_3g _INIT(false); #endif // WLED_SAVE_RAM #ifdef ARDUINO_ARCH_ESP32 - #if defined(LOLIN_WIFI_FIX) && (defined(ARDUINO_ARCH_ESP32C3) || defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32S3)) + #if defined(LOLIN_WIFI_FIX) && (defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)) WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_8_5dBm); #else WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_19_5dBm); #endif #endif -#define WLED_WIFI_CONFIGURED (strlen(multiWiFi[0].clientSSID) >= 1 && strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) != 0) +#define WLED_WIFI_CONFIGURED isWiFiConfigured() -#ifdef WLED_USE_ETHERNET +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) #ifdef WLED_ETH_DEFAULT // default ethernet board type if specified WLED_GLOBAL int ethernetType _INIT(WLED_ETH_DEFAULT); // ethernet board type #else @@ -403,9 +411,9 @@ WLED_GLOBAL bool cctICused _INIT(false); // CCT IC used (Athom 15W bulb #endif WLED_GLOBAL bool gammaCorrectCol _INIT(true); // use gamma correction on colors WLED_GLOBAL bool gammaCorrectBri _INIT(false); // use gamma correction on brightness -WLED_GLOBAL float gammaCorrectVal _INIT(2.8f); // gamma correction value +WLED_GLOBAL float gammaCorrectVal _INIT(2.2f); // gamma correction value -WLED_GLOBAL byte col[] _INIT_N(({ 255, 160, 0, 0 })); // current RGB(W) primary color. col[] should be updated if you want to change the color. +WLED_GLOBAL byte colPri[] _INIT_N(({ 255, 160, 0, 0 })); // current RGB(W) primary color. colPri[] should be updated if you want to change the color. WLED_GLOBAL byte colSec[] _INIT_N(({ 0, 0, 0, 0 })); // current RGB(W) secondary color WLED_GLOBAL byte nightlightTargetBri _INIT(0); // brightness after nightlight is over @@ -460,7 +468,15 @@ WLED_GLOBAL bool arlsForceMaxBri _INIT(false); // enable to f WLED_GLOBAL uint16_t DMXStart _INIT(10); // start address of the first fixture WLED_GLOBAL uint16_t DMXStartLED _INIT(0); // LED from which DMX fixtures start #endif -WLED_GLOBAL uint16_t e131Universe _INIT(1); // settings for E1.31 (sACN) protocol (only DMX_MODE_MULTIPLE_* can span over consecutive universes) +#ifdef WLED_ENABLE_DMX_INPUT + WLED_GLOBAL int dmxInputTransmitPin _INIT(0); + WLED_GLOBAL int dmxInputReceivePin _INIT(0); + WLED_GLOBAL int dmxInputEnablePin _INIT(0); + WLED_GLOBAL int dmxInputPort _INIT(2); + WLED_GLOBAL DMXInput dmxInput; +#endif + +WLED_GLOBAL uint16_t e131Universe _INIT(1); // settings for E1.31 (sACN) protocol (only DMX_MODE_MULTIPLE_* can span over consequtive universes) WLED_GLOBAL uint16_t e131Port _INIT(5568); // DMX in port. E1.31 default is 5568, Art-Net is 6454 WLED_GLOBAL byte e131Priority _INIT(0); // E1.31 port priority (if != 0 priority handling is active) WLED_GLOBAL E131Priority highPriority _INIT(3); // E1.31 highest priority tracking, init = timeout in seconds @@ -483,10 +499,10 @@ WLED_GLOBAL unsigned long lastMqttReconnectAttempt _INIT(0); // used for other #endif WLED_GLOBAL AsyncMqttClient *mqtt _INIT(NULL); WLED_GLOBAL bool mqttEnabled _INIT(false); -WLED_GLOBAL char mqttStatusTopic[40] _INIT(""); // this must be global because of async handlers -WLED_GLOBAL char mqttDeviceTopic[MQTT_MAX_TOPIC_LEN+1] _INIT(""); // main MQTT topic (individual per device, default is wled/mac) -WLED_GLOBAL char mqttGroupTopic[MQTT_MAX_TOPIC_LEN+1] _INIT("wled/all"); // second MQTT topic (for example to group devices) -WLED_GLOBAL char mqttServer[MQTT_MAX_SERVER_LEN+1] _INIT(""); // both domains and IPs should work (no SSL) +WLED_GLOBAL char mqttStatusTopic[MQTT_MAX_TOPIC_LEN + 8] _INIT(""); // this must be global because of async handlers +WLED_GLOBAL char mqttDeviceTopic[MQTT_MAX_TOPIC_LEN + 1] _INIT(""); // main MQTT topic (individual per device, default is wled/mac) +WLED_GLOBAL char mqttGroupTopic[MQTT_MAX_TOPIC_LEN + 1] _INIT("wled/all"); // second MQTT topic (for example to group devices) +WLED_GLOBAL char mqttServer[MQTT_MAX_SERVER_LEN + 1] _INIT(""); // both domains and IPs should work (no SSL) WLED_GLOBAL char mqttUser[41] _INIT(""); // optional: username for MQTT auth WLED_GLOBAL char mqttPass[65] _INIT(""); // optional: password for MQTT auth WLED_GLOBAL char mqttClientID[41] _INIT(""); // override the client ID @@ -516,7 +532,8 @@ WLED_GLOBAL bool serialCanTX _INIT(false); WLED_GLOBAL bool enableESPNow _INIT(false); // global on/off for ESP-NOW WLED_GLOBAL byte statusESPNow _INIT(ESP_NOW_STATE_UNINIT); // state of ESP-NOW stack (0 uninitialised, 1 initialised, 2 error) WLED_GLOBAL bool useESPNowSync _INIT(false); // use ESP-NOW wireless technology for sync -WLED_GLOBAL char linked_remote[13] _INIT(""); // MAC of ESP-NOW remote (Wiz Mote) +//WLED_GLOBAL char linked_remote[13] _INIT(""); // MAC of ESP-NOW remote (Wiz Mote) +WLED_GLOBAL std::vector> linked_remotes; // MAC of ESP-NOW remotes (Wiz Mote) WLED_GLOBAL char last_signal_src[13] _INIT(""); // last seen ESP-NOW sender #endif @@ -536,7 +553,7 @@ WLED_GLOBAL byte currentTimezone _INIT(WLED_TIMEZONE); // Timezone ID. Refer WLED_GLOBAL int utcOffsetSecs _INIT(WLED_UTC_OFFSET); // Seconds to offset from UTC before timzone calculation WLED_GLOBAL byte overlayCurrent _INIT(0); // 0: no overlay 1: analog clock 2: was single-digit clock 3: was cronixie -WLED_GLOBAL byte overlayMin _INIT(0), overlayMax _INIT(DEFAULT_LED_COUNT - 1); // boundaries of overlay mode +WLED_GLOBAL uint16_t overlayMin _INIT(0), overlayMax _INIT(DEFAULT_LED_COUNT - 1); // boundaries of overlay mode WLED_GLOBAL byte analogClock12pixel _INIT(0); // The pixel in your strip where "midnight" would be WLED_GLOBAL bool analogClockSecondsTrail _INIT(false); // Display seconds as trail of LEDs instead of a single pixel @@ -551,16 +568,22 @@ WLED_GLOBAL byte countdownMin _INIT(0) , countdownSec _INIT(0); WLED_GLOBAL byte macroNl _INIT(0); // after nightlight delay over WLED_GLOBAL byte macroCountdown _INIT(0); WLED_GLOBAL byte macroAlexaOn _INIT(0), macroAlexaOff _INIT(0); -WLED_GLOBAL byte macroButton[WLED_MAX_BUTTONS] _INIT({0}); -WLED_GLOBAL byte macroLongPress[WLED_MAX_BUTTONS] _INIT({0}); -WLED_GLOBAL byte macroDoublePress[WLED_MAX_BUTTONS] _INIT({0}); // Security CONFIG -WLED_GLOBAL bool otaLock _INIT(false); // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks -WLED_GLOBAL bool wifiLock _INIT(false); // prevents access to WiFi settings when OTA lock is enabled -WLED_GLOBAL bool aOtaEnabled _INIT(true); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on -WLED_GLOBAL char settingsPIN[5] _INIT(""); // PIN for settings pages -WLED_GLOBAL bool correctPIN _INIT(true); +#ifdef WLED_OTA_PASS +WLED_GLOBAL bool otaLock _INIT(true); // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks +#else +WLED_GLOBAL bool otaLock _INIT(false); // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks +#endif +WLED_GLOBAL bool wifiLock _INIT(false); // prevents access to WiFi settings when OTA lock is enabled +#ifdef WLED_ENABLE_AOTA +WLED_GLOBAL bool aOtaEnabled _INIT(true); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on +#else +WLED_GLOBAL bool aOtaEnabled _INIT(false); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on +#endif +WLED_GLOBAL bool otaSameSubnet _INIT(true); // prevent OTA updates from other subnets (e.g. internet) if no PIN is set +WLED_GLOBAL char settingsPIN[5] _INIT(WLED_PIN); // PIN for settings pages +WLED_GLOBAL bool correctPIN _INIT(!strlen(settingsPIN)); WLED_GLOBAL unsigned long lastEditTime _INIT(0); WLED_GLOBAL uint16_t userVar0 _INIT(0), userVar1 _INIT(0); //available for use in usermod @@ -568,6 +591,7 @@ WLED_GLOBAL uint16_t userVar0 _INIT(0), userVar1 _INIT(0); //available for use i // internal global variable declarations // wifi WLED_GLOBAL bool apActive _INIT(false); +WLED_GLOBAL byte apClients _INIT(0); WLED_GLOBAL bool forceReconnect _INIT(false); WLED_GLOBAL unsigned long lastReconnectAttempt _INIT(0); WLED_GLOBAL bool interfacesInited _INIT(false); @@ -575,15 +599,15 @@ WLED_GLOBAL bool wasConnected _INIT(false); // color WLED_GLOBAL byte lastRandomIndex _INIT(0); // used to save last random color so the new one is not the same +WLED_GLOBAL std::vector customPalettes; // custom palettes +WLED_GLOBAL uint8_t paletteBlend _INIT(0); // determines blending and wrapping of palette: 0: blend, wrap if moving (SEGMENT.speed>0); 1: blend, always wrap; 2: blend, never wrap; 3: don't blend or wrap // transitions -WLED_GLOBAL bool fadeTransition _INIT(true); // enable crossfading brightness/color -WLED_GLOBAL bool modeBlending _INIT(true); // enable effect blending +WLED_GLOBAL uint8_t blendingStyle _INIT(0); // effect blending/transitionig style WLED_GLOBAL bool transitionActive _INIT(false); WLED_GLOBAL uint16_t transitionDelay _INIT(750); // global transition duration WLED_GLOBAL uint16_t transitionDelayDefault _INIT(750); // default transition time (stored in cfg.json) WLED_GLOBAL unsigned long transitionStartTime; -WLED_GLOBAL float tperLast _INIT(0.0f); // crossfade transition progress, 0.0f - 1.0f WLED_GLOBAL bool jsonTransitionOnce _INIT(false); // flag to override transitionDelay (playlist, JSON API: "live" & "seg":{"i"} & "tt") WLED_GLOBAL uint8_t randomPaletteChangeTime _INIT(5); // amount of time [s] between random palette changes (min: 1s, max: 255s) WLED_GLOBAL bool useHarmonicRandomPalette _INIT(true); // use *harmonic* random palette generation (nicer looking) or truly random @@ -609,13 +633,32 @@ WLED_GLOBAL byte briLast _INIT(128); // brightness before WLED_GLOBAL byte whiteLast _INIT(128); // white channel before turned off. Used for toggle function in ir.cpp // button -WLED_GLOBAL int8_t btnPin[WLED_MAX_BUTTONS] _INIT({BTNPIN}); -WLED_GLOBAL byte buttonType[WLED_MAX_BUTTONS] _INIT({BTNTYPE}); +struct Button { + unsigned long pressedTime; // time button was pressed + unsigned long waitTime; // time to wait for next button press + int8_t pin; // pin number + struct { + uint8_t type : 6; // button type (push, long, double, etc.) + bool pressedBefore : 1; // button was pressed before + bool longPressed : 1; // button was long pressed + }; + uint8_t macroButton; // macro/preset to call on button press + uint8_t macroLongPress; // macro/preset to call on long press + uint8_t macroDoublePress; // macro/preset to call on double press + + Button(int8_t p, uint8_t t, uint8_t mB = 0, uint8_t mLP = 0, uint8_t mDP = 0) + : pressedTime(0) + , waitTime(0) + , pin(p) + , type(t) + , pressedBefore(false) + , longPressed(false) + , macroButton(mB) + , macroLongPress(mLP) + , macroDoublePress(mDP) {} +}; +WLED_GLOBAL std::vector